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

import { BaseComponent } from './base-component';
import { highlightCode } from '../helpers/highlight-code';
import { findCodeLang } from '../helpers/find-code-lang';
import { copyToClipboard } from '../helpers/copy-to-clipboard';
import { decodeHtmlEntities } from '../helpers/decode-html-entities';

/**
 * Component for managing code block highlighting.
 * Handles syntax highlighting for a single code block in the documentation.
 */
export class CodeBlock extends BaseComponent {
	/**
	 * Attaches the CodeBlock component to all matching elements on the page.
	 * It initializes the component for each element.
	 * This method should be called when the DOM is fully loaded.
	 */
	static attach() {
		const codeBlocks = document.querySelectorAll( '.c-code-block' );

		for ( const codeBlock of codeBlocks ) {
			codeBlock.codeBlock = new CodeBlock( codeBlock ).init();
		}
	}

	/**
	 * Creates the CodeBlock component.
	 *
	 * @param element - The pre element containing the code block.
	 */
	constructor( element ) {
		super( {
			status: element.getAttribute( 'data-status' ) || 'loading'
		} );

		this.elements = {
			root: element,
			copyButton: element.querySelector( '.c-code-block__copy-button' )
		};

		this.config = {
			lang: findCodeLang( element.querySelector( 'code' ) ) || 'plain',
			highlightOnMount: element.getAttribute( 'data-highlight-on-mount' ) === 'true',
			copyable: element.dataset.copyable !== 'false'
		};

		this.originalText = element.querySelector( 'code' ).innerText;

		// Track the current highlighting operation
		this.highlightPromise = Promise.resolve();

		// Keep tooltip id
		const tooltipId = this.elements.copyButton?.getAttribute( 'aria-labelledby' );

		if ( tooltipId ) {
			const tooltip = document.getElementById( tooltipId );
			const tooltipText = tooltip?.querySelector( '.c-tooltip-popover__text' );

			if ( tooltipText ) {
				this.elements.tooltipText = tooltipText;
				this.originalTooltipText = tooltipText.textContent;
			}
		}
	}

	/**
	 * Initializes the component by processing the code block.
	 */
	init() {
		this._updateScrollClass();
		this._initCopyButton();
		this.render();

		if ( this.config.highlightOnMount ) {
			this._highlight();
		}

		window.addEventListener( 'resize', this.updateSize.bind( this ) );

		return this;
	}

	/**
	 * Method called after code block is being shown on tab activation.
	 */
	updateSize() {
		this._updateScrollClass();
	}

	/**
	 * Renders the component based on the current state.
	 * This component doesn't require visual updates after state changes.
	 */
	render( prevState, state = this.state ) {
		this.elements.root.setAttribute( 'data-status', state.status );
	}

	/**
	 * Sets the status of the code block.
	 */
	setStatus( status ) {
		this.setState( { status } );
	}

	/**
	 * Highlights the code block with the specified language.
	 *
	 * @param code - The code to highlight.
	 */
	async setCode( code ) {
		await this.highlightPromise;

		const codeElement = this.elements.root.querySelector( 'code' );

		if ( codeElement ) {
			codeElement.textContent = code;
		} else {
			return;
		}

		this.originalText = code;

		return this._highlight( code );
	}

	/**
	 * Highlights the code block with syntax highlighting.
	 * Ensures that previous highlighting operations complete before starting a new one.
	 */
	async _highlight( code = this.originalText ) {
		const { lang } = this.config;

		// If no language is specified, do not highlight
		if ( [ 'none', 'plaintext', 'plain', 'raw' ].includes( lang ) ) {
			this.setStatus( 'ready' );
			this._updateScrollClass();
			return;
		}

		// Wait for any previous highlight operation to complete
		await this.highlightPromise;

		// Create a new promise for this highlighting operation
		this.highlightPromise = ( async () => {
			const { root: element } = this.elements;
			const preElement = element.querySelector( '.c-code-block__pre, pre' );

			if ( !preElement ) {
				return;
			}

			// Get highlighted HTML
			this.setStatus( 'loading' );

			try {
				const unescapedCode = decodeHtmlEntities( code );
				const highlightedHTML = await highlightCode( unescapedCode, lang );

				preElement.outerHTML = highlightedHTML;
				element
					.querySelector( '.c-code-block__pre, pre' )
					.classList.add( 'c-code-block__pre' );
			} finally {
				this.highlightPromise = null;
				this.setStatus( 'ready' );
				this._updateScrollClass();
			}
		} )();

		// Return the promise to allow callers to await completion
		return this.highlightPromise;
	}

	/**
	 * Updates the scroll class based on whether the code block has scroll.
	 */
	_updateScrollClass() {
		const { root } = this.elements;
		const preElement = root.querySelector( '.c-code-block__pre, pre' );

		if ( !preElement ) {
			return;
		}

		root.classList.toggle( 'c-code-block--has-vertical-scroll', preElement.scrollHeight > preElement.clientHeight );
	}

	/**
	 * Initializes the copy button functionality if the code block is copyable.
	 */
	_initCopyButton() {
		const { copyButton, tooltipText } = this.elements;

		if ( !copyButton ) {
			return;
		}

		copyButton.addEventListener( 'click', async () => {
			try {
				const code = decodeHtmlEntities( this.originalText );

				await copyToClipboard( code );

				copyButton.classList.add( 'c-code-block__copy-button--success' );

				if ( tooltipText ) {
					tooltipText.textContent = 'Copied!';
				}

				setTimeout( () => {
					copyButton.classList.remove( 'c-code-block__copy-button--success' );

					if ( tooltipText && this.originalTooltipText ) {
						tooltipText.textContent = this.originalTooltipText;
					}
				}, 2000 );
			} catch ( error ) {
				console.error( 'Failed to copy code:', error );
			}
		} );
	}
}
