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

'use strict';

const TypedocConverter = require( './typedocconverter' );
const ReflectionKind = require( './reflectionkind' );

/**
 * An abstract parser class providing basic utils for converting Typedoc reflection to JSDoc doclet.
 */
module.exports = class AbstractParser {
	/**
	 * @abstract
	 * @method canParse
	 * @param {BaseReflection} item
	 * @returns {Boolean}
	 */

	/**
	 * @abstract
	 * @method parse
	 * @param {BaseReflection} item
	 * @param {String|null} [parentName=null]
	 * @returns {Object}
	 */

	/**
	 * Coverts the item's comment from Markdown to HTML.
	 *
	 * @param {BaseReflection} item
	 * @returns {String}
	 */
	getComment( item ) {
		if ( item.comment && Array.isArray( item.comment.summary ) ) {
			return TypedocConverter.convertComment( item.comment.summary );
		}

		return '';
	}

	/**
	 * @param {BaseReflection} item
	 * @param {String|null} [parentName=null]
	 * @returns {String}
	 */
	getLongName( item, parentName = null ) {
		let separator;

		if ( item.isCKEditor5Event ) {
			separator = '#event:';
		}

		switch ( item.kind ) {
			case ReflectionKind.Module:
				separator = ':';
				parentName = 'module';
				break;

			case ReflectionKind.Variable:
			case ReflectionKind.Function:
			case ReflectionKind.Class:
			case ReflectionKind.Interface:
			case ReflectionKind.TypeAlias:
				separator = '~';
				break;

			default:
				if ( !separator ) {
					separator = this.getScope( item ) === 'static' ? '.' : '#';
				}
		}

		return `${ parentName }${ separator }${ item.name }`;
	}

	/**
	 * Prepares the name for the computed property.
	 *
	 * @param {ComputedPropertyReflection} item
	 * @returns {String}
	 */
	getComputedName( item ) {
		return TypedocConverter.getComputedName( item );
	}

	/**
	 * Returns the access type for the item.
	 *
	 * @param {BaseReflection} item
	 * @returns {'public'|'internal'|'protected'|'private'}
	 */
	getVisibility( item ) {
		const flags = item.flags || {};

		if ( this.isInternal( item ) ) {
			return 'internal';
		}

		if ( flags.isProtected ) {
			return 'protected';
		}

		if ( flags.isPrivate ) {
			return 'private';
		}

		return 'public';
	}

	/**
	 * Returns the item's kind.
	 *
	 * @param {BaseReflection} item
	 * @returns {String}
	 */
	getKind( item ) {
		if ( item.isCKEditor5Event ) {
			return 'event';
		}
		if ( item.isCKEditor5Error ) {
			return 'error';
		}

		switch ( item.kind ) {
			case ReflectionKind.Module:
				return 'module';

			case ReflectionKind.Class:
				return 'class';

			case ReflectionKind.Function:
			case ReflectionKind.Method:
			case ReflectionKind.Constructor:
				return 'function';

			case ReflectionKind.Interface:
				return 'interface';

			case ReflectionKind.Accessor:
			case ReflectionKind.Property:
			case ReflectionKind.IndexSignature:
				return 'member';

			case ReflectionKind.Variable:
				return 'constant';

			case ReflectionKind.TypeAlias:
				return 'typedef';
		}
	}

	/**
	 * Returns the item's scope.
	 *
	 * @param {BaseReflection} item
	 * @returns {'instance'|'static'}
	 */
	getScope( item ) {
		const flags = item.flags || {};

		if ( flags.isStatic ) {
			return 'static';
		}

		return 'instance';
	}

	/**
	 * Returns the `extraId` combined from the item's scope, kind, name and optional variation.
	 *
	 * @param {BaseReflection} item
	 * @returns {String}
	 */
	getExtraId( item ) {
		const staticModifier = this.getScope( item ) === 'static' ? 'static-' : '';

		return staticModifier + this.getKind( item ) + '-' + item.name;
	}

	/**
	 * Returns the long names of the events that are fired from the signature.
	 *
	 * @param {BaseReflection} signature
	 * @param {String} parentName
	 * @returns {Array.<String>|null}
	 */
	getFiredEvents( signature, parentName ) {
		if ( !signature.comment || !signature.comment.blockTags ) {
			return null;
		}

		return signature.comment.blockTags
			.filter( blockTag => blockTag.tag === '@fires' )
			.map( blockTag => {
				const content = blockTag.content[ 0 ].text;

				if ( content.startsWith( 'module:' ) ) {
					return content;
				}

				return `${ parentName }#event:${ content }`;
			} );
	}

	/**
	 * @param {BaseReflection} signature
	 * @param {String} parentName
	 * @returns {Array.<Object>|null}
	 */
	getFiredBy( signature, parentName ) {
		if ( !signature.comment ) {
			return null;
		}

		if ( !signature.comment.blockTags ) {
			return null;
		}

		const eventTags = signature.comment.blockTags.filter( blockTag => blockTag.tag === '@eventName' );

		if ( !eventTags.length ) {
			return null;
		}

		return eventTags
			.map( eventTag => {
				const textContent = eventTag.content.find( item => item.kind === 'text' );

				if ( !textContent ) {
					return null;
				}

				const [ , className, eventName ] = textContent.text.match( /~([A-Za-z]+)#([A-Za-z:_{}]+)$/ );
				const pathPrefix = textContent.text.startsWith( 'module:' ) ? textContent.text.split( '~' )[ 0 ] : parentName;
				const url = `${ pathPrefix }~${ className }#event:${ eventName }`;
				const name = `${ className }#${ eventName }`;

				return { url, name };
			} )
			.filter( Boolean );
	}

	/**
	 * Returns the URL to GitHub with selected line, where the given doclet is defined.
	 * The URL includes a commit hash which was used to build the API docs.
	 *
	 * @param {BaseReflection} item
	 * @param {Number} sourceIndex
	 * @returns {Object|null}
	 */
	getFile( item, sourceIndex = 0 ) {
		if ( !item.sources ) {
			return null;
		}

		const { url } = item.sources[ sourceIndex ];

		if ( !url ) {
			return null;
		}

		return { url };
	}

	/**
	 * Returns links from the "@see" tags.
	 *
	 * @param {BaseReflection} item
	 * @param {String|null} [parentName=null]
	 * @returns {Array.<String>}
	 */
	getRelated( item, parentName ) {
		if ( !item.comment || !item.comment.blockTags ) {
			return null;
		}

		return item.comment.blockTags
			.filter( blockTag => blockTag.tag === '@see' )
			.flatMap( blockTag => {
				const items = blockTag.content
					.map( ( { text } ) => text.trim() )
					// To remove list markers.
					.filter( text => text.length > 1 );

				if ( !parentName ) {
					return items;
				}

				return items.map( text => {
					// A full reference to a module.
					if ( text.startsWith( 'module:' ) ) {
						return text;
					}

					// A short reference to an existing item in the `parentName` module.
					if ( text.startsWith( '~' ) ) {
						const [ moduleName ] = parentName.split( '~' );

						return moduleName + text;
					}

					return parentName + text;
				} );
			} );
	}

	/**
	 * Checks if the item is readonly.
	 *
	 * The readonly state can be marked either by a TypeScript `readonly` keyword, or by the `@readonly` JSDoc keyword.
	 *
	 * @param {BaseReflection} item
	 * @returns {Boolean}
	 */
	isReadonly( item ) {
		if ( item.flags && item.flags.isReadonly ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if the item is observable.
	 *
	 * The observable state can be marked by the `@observable` JSDoc keyword.
	 *
	 * @param {BaseReflection} item
	 * @returns {Boolean}
	 */
	isObservable( item ) {
		if ( item.comment && item.comment.blockTags ) {
			return item.comment.blockTags.some( tag => tag.tag === '@observable' );
		}

		return false;
	}

	/**
	 * Checks if the item is marked as an optional.
	 *
	 * @param {BaseReflection} item
	 * @returns {Boolean}
	 */
	isOptional( item ) {
		if ( item.flags && item.flags.isOptional ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if the member is marked as `internal` one.
	 *
	 * The `internal` state means the same as `protected`, but with one exception: the `internal` member can be used from outside the class
	 * in which it is defined (or its derivatives), as opposed to the `protected` state.
	 *
	 * @param {BaseReflection} item
	 * @returns {Boolean}
	 */
	isInternal( item ) {
		if ( item.comment && item.comment.modifierTags ) {
			return item.comment.modifierTags.some( tag => tag === '@internal' );
		}

		return false;
	}

	/**
	 * Checks if the member is marked by the `@deprecated` JSDoc tag.
	 *
	 * @param {BaseReflection} item
	 * @returns {Boolean}
	 */
	isDeprecated( item ) {
		if ( item.comment && item.comment.blockTags ) {
			return item.comment.blockTags.some( tag => tag.tag === '@deprecated' );
		}

		return false;
	}

	/**
	 * Checks if the member contains the `@skipSource` annotation.
	 *
	 * Note: The `skipSource` flag can be set to `true` even if the member does not contain the annotation.
	 * It happens when the member has not specified `file` property. In such a case, Umberto does not know
	 * where to point to a source and decides to skip generating the "See source" button.
	 *
	 * @param {BaseReflection} item
	 * @returns {Boolean}
	 */
	shouldSkipSource( item ) {
		if ( item.comment && item.comment.modifierTags ) {
			return item.comment.modifierTags.some( tag => tag === '@skipSource' );
		}

		return false;
	}

	/**
	 * Returns a name specified in the `@label` annotation.
	 *
	 * @protected
	 * @param {BaseReflection} item
	 * @returns {String}
	 */
	_getLabelName( item ) {
		if ( item.comment && item.comment.blockTags ) {
			const labelComment = item.comment.blockTags.find( tag => tag.tag === '@label' );

			if ( !labelComment ) {
				return '';
			}

			return labelComment.content[ 0 ].text;
		}

		return '';
	}
};
