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

'use strict';

const HtmlFile = require( '../api-builder/classes/html-file' );
const upath = require( 'upath' );
const { URL, resolve: urlResolve } = require( 'url' );
const beautifyHtml = require( 'js-beautify' ).html;
const macroReplacer = require( '../tasks/macro-replacer' );
const getDocSearchConfig = require( '../helpers/get-docsearch-config' );
const githubUrlUtils = require( '../helpers/github-url' );

module.exports = class SdkBuilder {
	constructor( {
		sdkCollection,
		template,
		BASE_PATH,
		startPage,
		config,
		destinationPath,
		canonicalUrlBeginning,
		macrosVariables,
		docSearch
	} = {} ) {
		this.sdkCollection = sdkCollection;
		this.template = template;
		this.BASE_PATH = BASE_PATH;
		this.startPage = startPage;
		this.projectConfig = config;
		this.destinationPath = destinationPath;
		this.renderedFiles = [];
		this.canonicalUrlBeginning = canonicalUrlBeginning;
		this.macrosVariables = macrosVariables;
		this._docSearch = docSearch;
	}

	buildSdk() {
		const tools = ( () => {
			// Tools object used during SDK building.
			const version = this.projectConfig.version;
			const cke4path = `http://cdn.ckeditor.com/${ version }/full-all/`;
			const docsUrl = 'https://ckeditor.com/docs';
			const ckeditorPath = `${ docsUrl }/vendors/${ version }/ckeditor/ckeditor.js`;
			const sdkGroup = this.projectConfig.groups.find( g => g.id === 'sdk' );

			const variables = Object.assign( {}, this.macrosVariables, {
				CKEDITOR_VERSION: version,
				CKEDITOR_EXAMPLES_SLUG: sdkGroup.slug
			} );

			return {
				replaceMacros( source ) {
					return macroReplacer( { fullText: source, variables, pagePath: 'unknown' } );
				},
				makeFullPaths( source, currentPagePath ) {
					const pagePath = `${ docsUrl }/${ currentPagePath }`;

					let match;
					let regexp;

					// Make full path from strings `CKEDITOR.basePath + 'contents.css'`.
					let ret = source.replace( /CKEDITOR\.basePath \+ 'contents\.css'/g, `'${ cke4path }contents.css'` );

					// Make full path from 2nd argument of method `CKEDITOR.plugins.addExternal( name, path, filename )`.
					regexp = /CKEDITOR\.plugins\.addExternal\([^,]+,\s'([^']+)'[^)]+\)/g;

					while ( ( match = regexp.exec( ret ) ) ) {
						ret = ret.replace( match[ 1 ], urlResolve( ckeditorPath, match[ 1 ] ) );
					}

					// Syntax of `CKEDITOR.replace()` or `CKEDITOR.inline()` functions is always the same:
					// The first argument is a string, the second is an object.
					// We need to replace paths in the object so we need to extract it from the source snippet.
					// This should be parsed by AST but... For now we use strings and we need to handle it.
					const objectToReplace = ret.substring( ret.indexOf( '{' ) + 1, ret.lastIndexOf( '} );' ) );

					// At the end we need to replace the object with updated paths, so we need a copy of the object (as string).
					// All changes will be applied here.
					let changedData = objectToReplace.slice();

					const assetsRegExp = /'(assets\/[^']+?|\.\.\/[^']+?)'/g;
					let assetsMatch;

					while ( ( assetsMatch = assetsRegExp.exec( ret ) ) ) {
						// Paths in SDK assets are relative to the documentation pages URLs. See #881.
						// Let's remove the `{page}.html` string from the URL and append the asset path at the end.
						// It should be enough for generating proper URLs.
						const resourcePath = pagePath.split( '/' ).slice( 0, -1 ).join( '/' ) + '/' + assetsMatch[ 1 ];

						// However, the URL can still be invalid, e.g. contains "../".
						// Let's normalize the URL in order to remove "directory up" symbol from URLs. E.g.:
						// After merging: 'https://example.com/assets/foo/bar' and '../../bar/foo.jpg'
						// we want to have: 'https://example.com/assets/bar/foo.jpg' instead of:
						// 'https://example.com/assets/foo/bar/../bar/foo.jpg'.
						// See: #885.
						changedData = changedData.replace( assetsMatch[ 1 ], new URL( resourcePath ).toString() );
					}

					// And after all, let's replace the original object with updated paths.
					ret = ret.replace( objectToReplace, changedData );

					// Make full path inside `$.getJSON()` query.
					regexp = /\$\.getJSON\(\s'([^']+?)'\s?\)/g;

					while ( ( match = regexp.exec( ret ) ) ) {
						ret = ret.replace( match[ 1 ], urlResolve( pagePath, match[ 1 ] ) );
					}

					return ret;
				}
			};
		} )();

		const views = [];
		const sdkGroup = this.projectConfig.groups.find( g => g.id === 'sdk' );
		const slug = sdkGroup.slug;
		const projectSlug = this.projectConfig.slug;

		this.createIndexPageNavigation();

		// Process each object representing each html file in examples.
		for ( const sdkItemName in this.sdkCollection ) {
			const sdkItem = this.sdkCollection[ sdkItemName ];
			const currentPagePath = upath.join( this.BASE_PATH, slug, sdkItem.name + '.html' );
			const sdkSamples = beautifySdkSamples( {
				sdkItem,
				ckeditorVersion: this.projectConfig.version,
				currentPagePath
			} );

			const page = {
				path: currentPagePath,
				BASE_PATH: this.BASE_PATH,
				projectfullname: this.projectConfig.name,
				groupId: 'sdk',
				groupSlug: slug,
				projectName: this.projectConfig.slug,
				title: sdkItem.title,
				isSdk: true,
				startPage: this.projectConfig.startPage ? '/' + this.projectConfig.startPage : '/index.html',
				presetVersion: sdkItem.presetVersion,
				ckeditorVersion: this.projectConfig.version,
				sdkSamples,
				issuesUrl: githubUrlUtils.getIssueUrl( this.projectConfig ),
				contributeUrl: githubUrlUtils.getContributeUrl( this.projectConfig, sdkGroup.sourceDir, sdkItem.name ),
				order: sdkItem && sdkItem.meta ? sdkItem.meta.order : 0,
				canonicalUrlBeginning: this.canonicalUrlBeginning
			};

			const projectLocals = {
				latestBasePath: upath.join( projectSlug, 'latest' ),
				groups: this.projectConfig.groups,
				projectSlug,
				docSearchConfig: getDocSearchConfig( this._docSearch, {
					groups: this.projectConfig.groups,
					slug: this.projectConfig.slug,
					customRanking: this.projectConfig.docsearch ? this.projectConfig.docsearch.customRanking : []
				} )
			};

			views.push( new HtmlFile( this.template.renderTemplate(
				'page',
				{
					page,
					config: this.projectConfig,
					projectLocals,
					content: tools.replaceMacros( sdkItem.content ),
					getPages: ( function( pages ) {
						return function( category ) {
							return pages
								.filter( element => element.meta && element.meta.category && element.meta.category === category )
								.sort( ( a, b ) => {
									const aOrder = a.meta && a.meta.order ? parseInt( a.meta.order, 10 ) : 0;
									const bOrder = b.meta && b.meta.order ? parseInt( b.meta.order, 10 ) : 0;

									if ( aOrder === bOrder ) {
										return ( a.title || a.name ).localeCompare( b.title || b.name );
									} else {
										return aOrder - bOrder;
									}
								} );
						};
					}( this.sdkCollection ) ),
					docSearchConfig: getDocSearchConfig( this._docSearch )
				}
			), upath.join( this.destinationPath, slug, sdkItem.name + '.html' ) ) );
		}
		this.renderedFiles = views;

		function beautifySdkSamples( { sdkItem, ckeditorVersion, currentPagePath } ) {
			if ( !sdkItem.sdkSamples ) {
				return null;
			}

			const presetVersion = sdkItem.presetVersion || 'standard-all';
			const sdkSamples = sdkItem.sdkSamples.map( sample => {
				const bodyBlocks = sample.bodyItems.map( transformBuildingParts );
				const headBlocks = sample.headItems.map( transformBuildingParts );
				const flags = sample.flags;
				const code = flags.template === 'empty' ? `${ [ ...headBlocks, ...bodyBlocks ].join( '\n' ) }` : `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<title>${ sample.name }</title>
<script src="https://cdn.ckeditor.com/${ ckeditorVersion }/${ presetVersion }/ckeditor.js"></script>
${ headBlocks.join( '\n ' ) }</head>
<body>
${ bodyBlocks.join( '\n' ) }
</body>
</html>`;
				return {
					name: sample.name,
					code: flags.template === 'empty' ? code : beautifyHtml( code, { indent_size: 2 } ),
					codeClass: flags.highlighter
				};
			} );
			return sdkSamples;

			function transformBuildingParts( item ) {
				let ret = item.preserveWhitespace ? item.source : item.source.replace( /\t/g, '' );

				if ( item.short && sdkGroup.shortEditorContent ) {
					ret = ret.replace( /\{%SHORT_EDITOR_CONTENT%\}/g, sdkGroup.shortEditorContent );
				}

				ret = tools.replaceMacros( ret );
				ret = tools.makeFullPaths( ret, currentPagePath );
				return ret;
			}
		}
	}

	getFiles() {
		return this.renderedFiles;
	}

	createIndexPageNavigation() {
		this.navigationHtml = this.template.renderTemplate(
			'_partial/navtree',
			{
				data: this.sdkCollection,
				config: this.projectConfig,
				getPages: ( function( pages ) {
					return function( category ) {
						return pages
							.filter( element => element.meta && element.meta.category && element.meta.category === category )
							.sort( ( a, b ) => {
								const aOrder = a.meta && a.meta.order ? parseInt( a.meta.order, 10 ) : 0;
								const bOrder = b.meta && b.meta.order ? parseInt( b.meta.order, 10 ) : 0;

								if ( aOrder === bOrder ) {
									return ( a.title || a.name ).localeCompare( b.title || b.name );
								} else {
									return aOrder - bOrder;
								}
							} );
					};
				}( this.sdkCollection ) )
			}
		);
	}
};
