/**
 * @license Copyright (c) 2017-2025, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md.
 */

'use strict';

const DataCollection = require( './data-collection' );
const jsdoc2umberto = require( './converters/jsdoc2umberto' );
const jsduck2umberto = require( './converters/jsduck2umberto' );
const typedoc2umberto = require( './converters/typedoc2umberto' );
const relationFixer = require( './middlewares/relation-fixer' );
const converters = new Map( [
	[ 'jsdoc', jsdoc2umberto ],
	[ 'jsduck', jsduck2umberto ],
	[ 'typedoc', typedoc2umberto ]
] );

/**
 * Converts API data generated by various documentation tools into umberto-source-format.
 * umberto-source-format is pretty much equal to JSDoc format.
 */
module.exports = class DataProvider {
	/**
	 * Converts data to umberto-source-format.
	 *
	 * @param {String} type Data type to be converted.
	 * @param {Array<JSON>} data Data to be converted.
	 * @returns {DataCollection} data collection.
	 */
	convert( type, data, {
		middlewares = []
	} = {} ) {
		const converter = converters.get( type );

		if ( !converter ) {
			console.error( 'A converter for docs type of ', type, ' is not available.' );

			return null;
		}

		let convertedData = [];

		for ( const d of data ) {
			const converterOutput = converter( d );

			if ( Array.isArray( converterOutput ) ) {
				convertedData.push( ...converterOutput );
			}
		}

		convertedData = convertedData.filter( doclet => {
			return !doclet.undocumented && !doclet.ignore && doclet.memberof !== '<anonymous>';
		} );

		// Function operate directly on convertedData to avoid unnecessary copying large Array.
		if ( type === 'jsdoc' ) {
			removeDuplicates( convertedData );
		}

		if ( middlewares.indexOf( 'relation-fixer' ) !== -1 ) {
			convertedData = relationFixer( convertedData );
		}

		const dataCollection = new DataCollection();

		// Group doclets in different collections for easier further access.
		for ( const doclet of convertedData ) {
			// Doclets grouped by 'memberof' property. Additionally grouped by doclet's kind ('class', 'member' etc).
			dataCollection.add( `memberof:${ doclet.memberof }`, doclet, doclet.kind );
			// Doclets grouped by kind.
			dataCollection.add( doclet.kind, doclet );
			// Doclets grouped by 'ln:longname'.
			dataCollection.add( `ln:${ doclet.longname }`, doclet );
			// Doclets stored with id as a key.
			dataCollection.add( `id:${ doclet.memberof }#${ doclet.extraId }`, doclet );
		}

		return dataCollection;
	}
};

function removeDuplicates( doclets ) {
	const longnames = doclets.map( x => x.longname );
	const duplicatedLongnames = [];

	longnames.forEach( ( item, i ) => {
		if ( longnames.indexOf( item ) !== i && duplicatedLongnames.indexOf( item ) === -1 ) {
			duplicatedLongnames.push( item );
		}
	} );

	// Get duplicates for given longname
	for ( const longname of duplicatedLongnames ) {
		const duplicates = [];

		doclets.map( ( doclet, i ) => {
			if ( doclet.longname === longname ) {
				duplicates.push( { [ i ]: doclet } );
			}
		} );

		const descendantSortedIndexToRemove = getIndexesToRemove( duplicates ).sort( function( a, b ) {
			return b - a;
		} );

		for ( const index of descendantSortedIndexToRemove ) {
			doclets.splice( index, 1 );
		}
	}

	function getIndexesToRemove( duplicates ) {
		const indexes = duplicates.map( x => Object.keys( x ) );
		const removeCandidate = {
			index: null,
			type: null
		};

		for ( const item of duplicates ) {
			const key = Object.keys( item )[ 0 ];

			if ( item[ key ].mixed !== undefined && item[ key ].inherited !== undefined ) {
				removeCandidate.index = key;
				removeCandidate.type = 'inherited,mixed';
				break;
			} else if ( removeCandidate.type === null && item[ key ].mixed !== undefined ) {
				removeCandidate.index = key;
				removeCandidate.type = 'mixed';
			} else {
				removeCandidate.index = key;
			}
		}

		indexes.splice( indexes.indexOf( removeCandidate.index ), 1 );

		return indexes.map( x => parseInt( x, 10 ) );
	}
}
