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

'use strict';

const { styleText } = require( 'util' );
const upath = require( 'upath' );
const fs = require( 'fs-extra' );
const { globSync } = require( 'glob' );
const Hexo = require( '../hexo-shim' );

const getHexoConfig = require( './tasks/get-hexo-config' );
const extendConfig = require( './hexo/filter/extend-config' );

/**
 * Class used to initialize and control hexo.
 */
class HexoManager {
	constructor() {
		this.isInit = false;
		this.hexo = null;
		this.config = null;
		this.isReady = false;
		this.isWatched = false;
		this.originPaths = [];
	}

	/**
	 * Creates Hexo instance and applies Umberto's hexo config to it.
	 */
	init( options ) {
		if ( this.isInit ) {
			console.log( 'Hexo manager already initialized' );

			return;
		}
		this.isWatched = options.watched;
		this.ignoredProjects = options.ignoredProjects;
		this.config = getHexoConfig();
		this.hexo = new Hexo( upath.join( __dirname, '..' ), {
			config: 'hexo-config.json',
			silent: options.silent,
			debug: false
		} );

		// Enforce hexo to show errors.
		this.hexo.log.level = 50;

		extendConfig( this.hexo, this.config );
		this.isInit = true;
	}

	getPublicDir() {
		if ( !this.isInit ) {
			throw new Error( 'Hexo manager is not initialized' );
		}

		return this.config.public_dir;
	}

	getSourceDir() {
		if ( !this.isInit ) {
			throw new Error( 'Hexo manager is not initialized' );
		}

		return upath.join( __dirname, '..', this.config.source_dir );
	}

	getThemePath() {
		if ( !this.isInit ) {
			throw new Error( 'Hexo manager is not initialized' );
		}

		return upath.join( __dirname, '../themes', this.config.theme );
	}

	/**
	 * Initializes Hexo instance.
	 *
	 * @returns {Promise}
	 */
	hexoInit() {
		this.isReady = true;

		return this.hexo.init().then( () => {
			const renderer = this.hexo.render.renderer.get( 'md' );

			// Disable Nunjucks templating language in the Markdown renderer.
			// See https://github.com/ckeditor/ckeditor5/issues/14189.
			if ( renderer ) {
				renderer.disableNunjucks = true;
				this.hexo.extend.renderer.register( 'md', 'html', renderer );
			}
		} );
	}

	/**
	 * Run Hexo generate method.
	 *
	 * @returns {Promise<>}
	 */
	generate( { watch = false, concurrency } = {} ) {
		if ( !this.isInit ) {
			throw new Error( 'Hexo manager is not initialized' );
		}

		return this.hexo.call( 'generate', { watch, concurrency } )
			.then( () => {
				return this.hexo.exit();
			} )
			.catch( err => {
				console.log(
					`${ styleText( 'red', 'Error:' ) } ${ styleText( 'redBright', 'There was something wrong during hexo generation.' ) }`
				);
				this.hexo.exit( err );

				throw err;
			} );
	}

	/**
	 * Run Hexo clean method, which clears the build directory.
	 *
	 * @returns {Promise}
	 */
	clean() {
		if ( !this.isInit ) {
			throw new Error( 'Hexo manager is not initialized' );
		}

		console.log( 'Clean build directory.' );
		// hexo.clean use fs.rmdir, which throw an error when symlinks is removed. We need to get rid of them first.
		this.cleanSymLinks();

		return this.hexo.call( 'clean', {} );
	}

	cleanSymLinks() {
		const rootPath = this.config.public_dir;
		globSync( upath.join( rootPath, '**', 'latest' ) )
			.concat( globSync( upath.join( rootPath, 'latest' ) ) )
			.map( p => upath.normalize( p ) )
			.forEach( p => {
				const stat = fs.lstatSync( p );

				if ( stat.isSymbolicLink() ) {
					fs.unlinkSync( p );
				}
			} );
	}

	/**
	 * Clears Hexo source directory, so that pages are generated without any cache involved.
	 *
	 * @returns {Promise}
	 */
	emptySourceDir() {
		if ( !this.isInit ) {
			throw new Error( 'Hexo manager is not initialized' );
		}

		console.log( 'Emptying Hexo source directory.' );

		return new Promise( ( resolve, reject ) => {
			fs.emptyDir( this.getSourceDir(), err => {
				if ( err ) {
					reject();
				}

				if ( fs.existsSync( upath.join( __dirname, '../db.json' ) ) ) {
					fs.unlinkSync( upath.join( __dirname, '../db.json' ) );
				}

				resolve();
			} );
		} );
	}

	/**
	 * Adds data to hexo instance so that it can be used in hexo filters.
	 *
	 * @param options
	 * @returns {Promise}
	 */
	addCommonOptions( options ) {
		if ( !this.isInit ) {
			throw new Error( 'Hexo manager is not initialized' );
		}

		if ( !this.hexo.projectGlobals ) {
			this.hexo.projectGlobals = {};

			if ( !this.hexo.projectGlobals.common ) {
				this.hexo.projectGlobals.common = {};
			}

			Object.assign( this.hexo.projectGlobals.common, options, {
				// The `_snippetsData` collection is used in `src/tasks/build-snippets.js` in order to build snippets.
				// Required data are being added in hexo `after_post_render` filter: `scripts/filter/after-post-render/snippets.js`.
				/** @type {Map.<String, Set.<Snippet>>} */
				_snippetsData: new Map(),

				// The `_copiedFiles` collection is being built in `src/tasks/copy-project-docs.js` in order to find the source
				// paths of copied files while processing snippets (see: `scripts/filter/after-post-render/snippets.js`).
				/** @type {Map.<String, Map.<String, String>>} */
				_copiedFiles: new Map(),

				// The `_disabledFilters` collection contains names of filters that won't be executed.
				// Unfortunately, the mechanism is not merged into Hexo core, so when writing a filter, you need to verify
				// manually whether it should be executed. An example usage you can find in the `scripts/utils/parselinks.js` file.
				/** @type {Set.<String>} */
				_disabledFilters: new Set()
			} );
		}

		return Promise.resolve();
	}

	addProjectGlobalData( project, key, value ) {
		if ( !this.hexo.projectGlobals[ project ] ) {
			this.hexo.projectGlobals[ project ] = {};
		}

		if ( !this.hexo.projectGlobals[ project ][ key ] ) {
			this.hexo.projectGlobals[ project ][ key ] = value;
		}
	}

	addOriginPath( originPath, dstPath ) {
		this.originPaths.push( { originPath, dstPath } );
	}

	getOriginPaths() {
		return this.originPaths;
	}
}

module.exports = new HexoManager();
