/*
 * Copyright (C) 2023-2025  Yomitan Authors
 * Copyright (C) 2021-2022  Yomichan Authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import {DisplayContentManager} from '../display/display-content-manager.js';
import {getLanguageFromText} from '../language/text-utilities.js';
import {AnkiTemplateRendererContentManager} from '../templates/anki-template-renderer-content-manager.js';

export class StructuredContentGenerator {
    /**
     * @param {import('./display-content-manager.js').DisplayContentManager|import('../templates/anki-template-renderer-content-manager.js').AnkiTemplateRendererContentManager} contentManager
     * @param {Document} document
     * @param {Window} window
     */
    constructor(contentManager, document, window) {
        /** @type {import('./display-content-manager.js').DisplayContentManager|import('../templates/anki-template-renderer-content-manager.js').AnkiTemplateRendererContentManager} */
        this._contentManager = contentManager;
        /** @type {Document} */
        this._document = document;
        /** @type {Window} */
        this._window = window;
    }

    /**
     * @param {HTMLElement} node
     * @param {import('structured-content').Content} content
     * @param {string} dictionary
     */
    appendStructuredContent(node, content, dictionary) {
        node.classList.add('structured-content');
        this._appendStructuredContent(node, content, dictionary, null);
    }

    /**
     * @param {import('structured-content').Content} content
     * @param {string} dictionary
     * @returns {HTMLElement}
     */
    createStructuredContent(content, dictionary) {
        const node = this._createElement('span', 'structured-content');
        this._appendStructuredContent(node, content, dictionary, null);
        return node;
    }

    /**
     * @param {import('structured-content').ImageElement|import('dictionary-data').TermGlossaryImage} data
     * @param {string} dictionary
     * @returns {HTMLAnchorElement}
     */
    createDefinitionImage(data, dictionary) {
        const {
            path,
            width = 100,
            height = 100,
            preferredWidth,
            preferredHeight,
            title,
            pixelated,
            imageRendering,
            appearance,
            background,
            collapsed,
            collapsible,
            verticalAlign,
            border,
            borderRadius,
            sizeUnits,
        } = data;

        const hasPreferredWidth = (typeof preferredWidth === 'number');
        const hasPreferredHeight = (typeof preferredHeight === 'number');
        const invAspectRatio = (
            hasPreferredWidth && hasPreferredHeight ?
                preferredHeight / preferredWidth :
                height / width
        );
        const usedWidth = (
            hasPreferredWidth ?
                preferredWidth :
                (hasPreferredHeight ? preferredHeight / invAspectRatio : width)
        );

        const node = /** @type {HTMLAnchorElement} */ (this._createElement('a', 'gloss-image-link'));
        node.target = '_blank';
        node.rel = 'noreferrer noopener';

        const imageContainer = this._createElement('span', 'gloss-image-container');
        node.appendChild(imageContainer);

        const aspectRatioSizer = this._createElement('span', 'gloss-image-sizer');
        imageContainer.appendChild(aspectRatioSizer);

        const imageBackground = this._createElement('span', 'gloss-image-background');
        imageContainer.appendChild(imageBackground);

        const overlay = this._createElement('span', 'gloss-image-container-overlay');
        imageContainer.appendChild(overlay);

        const linkText = this._createElement('span', 'gloss-image-link-text');
        linkText.textContent = 'Image';
        node.appendChild(linkText);

        if (this._contentManager instanceof DisplayContentManager) {
            node.addEventListener('click', () => {
                if (this._contentManager instanceof DisplayContentManager) {
                    void this._contentManager.openMediaInTab(path, dictionary, this._window);
                }
            });
        }

        node.dataset.path = path;
        node.dataset.dictionary = dictionary;
        node.dataset.imageLoadState = 'not-loaded';
        node.dataset.hasAspectRatio = 'true';
        node.dataset.imageRendering = typeof imageRendering === 'string' ? imageRendering : (pixelated ? 'pixelated' : 'auto');
        node.dataset.appearance = typeof appearance === 'string' ? appearance : 'auto';
        node.dataset.background = typeof background === 'boolean' ? `${background}` : 'true';
        node.dataset.collapsed = typeof collapsed === 'boolean' ? `${collapsed}` : 'false';
        node.dataset.collapsible = typeof collapsible === 'boolean' ? `${collapsible}` : 'true';
        if (typeof verticalAlign === 'string') {
            node.dataset.verticalAlign = verticalAlign;
        }
        if (typeof sizeUnits === 'string' && (hasPreferredWidth || hasPreferredHeight)) {
            node.dataset.sizeUnits = sizeUnits;
        }

        aspectRatioSizer.style.paddingTop = `${invAspectRatio * 100}%`;

        if (typeof border === 'string') { imageContainer.style.border = border; }
        if (typeof borderRadius === 'string') { imageContainer.style.borderRadius = borderRadius; }
        imageContainer.style.width = `${usedWidth}em`;
        if (typeof title === 'string') {
            imageContainer.title = title;
        }

        if (this._contentManager !== null) {
            const image = this._contentManager instanceof DisplayContentManager ?
                /** @type {HTMLCanvasElement} */ (this._createElement('canvas', 'gloss-image')) :
                /** @type {HTMLImageElement} */ (this._createElement('img', 'gloss-image'));
            if (sizeUnits === 'em' && (hasPreferredWidth || hasPreferredHeight)) {
                const emSize = 14; // We could Number.parseFloat(getComputedStyle(document.documentElement).fontSize); here for more accuracy but it would cause a layout and be extremely slow; possible improvement would be to calculate and cache the value
                const scaleFactor = 2 * this._window.devicePixelRatio;
                image.style.width = `${usedWidth}em`;
                image.style.height = `${usedWidth * invAspectRatio}em`;
                image.width = usedWidth * emSize * scaleFactor;
            } else {
                image.width = usedWidth;
            }
            image.height = image.width * invAspectRatio;

            // Anki will not render images correctly without specifying to use 100% width and height
            image.style.width = '100%';
            image.style.height = '100%';

            imageContainer.appendChild(image);

            if (this._contentManager instanceof DisplayContentManager) {
                this._contentManager.loadMedia(
                    path,
                    dictionary,
                    (/** @type {HTMLCanvasElement} */(image)).transferControlToOffscreen(),
                );
            } else if (this._contentManager instanceof AnkiTemplateRendererContentManager) {
                this._contentManager.loadMedia(
                    path,
                    dictionary,
                    (url) => {
                        this._setImageData(node, /** @type {HTMLImageElement} */ (image), imageBackground, url, false);
                    },
                    () => {
                        this._setImageData(node, /** @type {HTMLImageElement} */ (image), imageBackground, null, true);
                    },
                );
            }
        }

        return node;
    }

    // Private

    /**
     * @param {HTMLElement} container
     * @param {import('structured-content').Content|undefined} content
     * @param {string} dictionary
     * @param {?string} language
     */
    _appendStructuredContent(container, content, dictionary, language) {
        if (typeof content === 'string') {
            if (content.length > 0) {
                container.appendChild(this._createTextNode(content));
                if (language === null) {
                    const language2 = getLanguageFromText(content, language);
                    if (language2 !== null) {
                        container.lang = language2;
                    }
                }
            }
            return;
        }
        if (!(typeof content === 'object' && content !== null)) {
            return;
        }
        if (Array.isArray(content)) {
            for (const item of content) {
                this._appendStructuredContent(container, item, dictionary, language);
            }
            return;
        }
        const node = this._createStructuredContentGenericElement(content, dictionary, language);
        if (node !== null) {
            container.appendChild(node);
        }
    }

    /**
     * @param {string} tagName
     * @param {string} className
     * @returns {HTMLElement}
     */
    _createElement(tagName, className) {
        const node = this._document.createElement(tagName);
        node.className = className;
        return node;
    }

    /**
     * @param {string} data
     * @returns {Text}
     */
    _createTextNode(data) {
        return this._document.createTextNode(data);
    }

    /**
     * @param {HTMLElement} element
     * @param {import('structured-content').Data} data
     */
    _setElementDataset(element, data) {
        for (let [key, value] of Object.entries(data)) {
            if (key.length > 0) {
                key = `${key[0].toUpperCase()}${key.substring(1)}`;
            }
            key = `sc${key}`;
            try {
                element.dataset[key] = value;
            } catch (e) {
                // DOMException if key is malformed
            }
        }
    }

    /**
     * @param {HTMLAnchorElement} node
     * @param {HTMLImageElement} image
     * @param {HTMLElement} imageBackground
     * @param {?string} url
     * @param {boolean} unloaded
     */
    _setImageData(node, image, imageBackground, url, unloaded) {
        if (url !== null) {
            image.src = url;
            node.href = url;
            node.dataset.imageLoadState = 'loaded';
            imageBackground.style.setProperty('--image', `url("${url}")`);
        } else {
            image.removeAttribute('src');
            node.removeAttribute('href');
            node.dataset.imageLoadState = unloaded ? 'unloaded' : 'load-error';
            imageBackground.style.removeProperty('--image');
        }
    }

    /**
     * @param {import('structured-content').Element} content
     * @param {string} dictionary
     * @param {?string} language
     * @returns {?HTMLElement}
     */
    _createStructuredContentGenericElement(content, dictionary, language) {
        const {tag} = content;
        switch (tag) {
            case 'br':
                return this._createStructuredContentElement(tag, content, dictionary, language, 'simple', false, false);
            case 'ruby':
            case 'rt':
            case 'rp':
                return this._createStructuredContentElement(tag, content, dictionary, language, 'simple', true, false);
            case 'table':
                return this._createStructuredContentTableElement(tag, content, dictionary, language);
            case 'thead':
            case 'tbody':
            case 'tfoot':
            case 'tr':
                return this._createStructuredContentElement(tag, content, dictionary, language, 'table', true, false);
            case 'th':
            case 'td':
                return this._createStructuredContentElement(tag, content, dictionary, language, 'table-cell', true, true);
            case 'div':
            case 'span':
            case 'ol':
            case 'ul':
            case 'li':
            case 'details':
            case 'summary':
                return this._createStructuredContentElement(tag, content, dictionary, language, 'simple', true, true);
            case 'img':
                return this.createDefinitionImage(content, dictionary);
            case 'a':
                return this._createLinkElement(content, dictionary, language);
        }
        return null;
    }

    /**
     * @param {string} tag
     * @param {import('structured-content').UnstyledElement} content
     * @param {string} dictionary
     * @param {?string} language
     * @returns {HTMLElement}
     */
    _createStructuredContentTableElement(tag, content, dictionary, language) {
        const container = this._createElement('div', 'gloss-sc-table-container');
        const table = this._createStructuredContentElement(tag, content, dictionary, language, 'table', true, false);
        container.appendChild(table);
        return container;
    }

    /**
     * @param {string} tag
     * @param {import('structured-content').StyledElement|import('structured-content').UnstyledElement|import('structured-content').TableElement|import('structured-content').LineBreak} content
     * @param {string} dictionary
     * @param {?string} language
     * @param {'simple'|'table'|'table-cell'} type
     * @param {boolean} hasChildren
     * @param {boolean} hasStyle
     * @returns {HTMLElement}
     */
    _createStructuredContentElement(tag, content, dictionary, language, type, hasChildren, hasStyle) {
        const node = this._createElement(tag, `gloss-sc-${tag}`);
        const {data, lang} = content;
        if (typeof data === 'object' && data !== null) { this._setElementDataset(node, data); }
        if (typeof lang === 'string') {
            node.lang = lang;
            language = lang;
        }
        switch (type) {
            case 'table-cell':
                {
                    const cell = /** @type {HTMLTableCellElement} */ (node);
                    const {colSpan, rowSpan} = /** @type {import('structured-content').TableElement} */ (content);
                    if (typeof colSpan === 'number') { cell.colSpan = colSpan; }
                    if (typeof rowSpan === 'number') { cell.rowSpan = rowSpan; }
                }
                break;
        }
        if (hasStyle) {
            const {style, title, open} = /** @type {import('structured-content').StyledElement} */ (content);
            if (typeof style === 'object' && style !== null) {
                this._setStructuredContentElementStyle(node, style);
            }
            if (typeof title === 'string') { node.title = title; }
            if (typeof open === 'boolean' && open) { node.setAttribute('open', ''); }
        }
        if (hasChildren) {
            this._appendStructuredContent(node, content.content, dictionary, language);
        }
        return node;
    }

    /**
     * @param {HTMLElement} node
     * @param {import('structured-content').StructuredContentStyle} contentStyle
     */
    _setStructuredContentElementStyle(node, contentStyle) {
        const {style} = node;
        const {
            fontStyle,
            fontWeight,
            fontSize,
            color,
            background,
            backgroundColor,
            textDecorationLine,
            textDecorationStyle,
            textDecorationColor,
            borderColor,
            borderStyle,
            borderRadius,
            borderWidth,
            clipPath,
            verticalAlign,
            textAlign,
            textEmphasis,
            textShadow,
            margin,
            marginTop,
            marginLeft,
            marginRight,
            marginBottom,
            padding,
            paddingTop,
            paddingLeft,
            paddingRight,
            paddingBottom,
            wordBreak,
            whiteSpace,
            cursor,
            listStyleType,
        } = contentStyle;
        if (typeof fontStyle === 'string') { style.fontStyle = fontStyle; }
        if (typeof fontWeight === 'string') { style.fontWeight = fontWeight; }
        if (typeof fontSize === 'string') { style.fontSize = fontSize; }
        if (typeof color === 'string') { style.color = color; }
        if (typeof background === 'string') { style.background = background; }
        if (typeof backgroundColor === 'string') { style.backgroundColor = backgroundColor; }
        if (typeof verticalAlign === 'string') { style.verticalAlign = verticalAlign; }
        if (typeof textAlign === 'string') { style.textAlign = textAlign; }
        if (typeof textEmphasis === 'string') { style.textEmphasis = textEmphasis; }
        if (typeof textShadow === 'string') { style.textShadow = textShadow; }
        if (typeof textDecorationLine === 'string') {
            style.textDecoration = textDecorationLine;
        } else if (Array.isArray(textDecorationLine)) {
            style.textDecoration = textDecorationLine.join(' ');
        }
        if (typeof textDecorationStyle === 'string') {
            style.textDecorationStyle = textDecorationStyle;
        }
        if (typeof textDecorationColor === 'string') {
            style.textDecorationColor = textDecorationColor;
        }
        if (typeof borderColor === 'string') { style.borderColor = borderColor; }
        if (typeof borderStyle === 'string') { style.borderStyle = borderStyle; }
        if (typeof borderRadius === 'string') { style.borderRadius = borderRadius; }
        if (typeof borderWidth === 'string') { style.borderWidth = borderWidth; }
        if (typeof clipPath === 'string') { style.clipPath = clipPath; }
        if (typeof margin === 'string') { style.margin = margin; }
        if (typeof marginTop === 'number') { style.marginTop = `${marginTop}em`; }
        if (typeof marginTop === 'string') { style.marginTop = marginTop; }
        if (typeof marginLeft === 'number') { style.marginLeft = `${marginLeft}em`; }
        if (typeof marginLeft === 'string') { style.marginLeft = marginLeft; }
        if (typeof marginRight === 'number') { style.marginRight = `${marginRight}em`; }
        if (typeof marginRight === 'string') { style.marginRight = marginRight; }
        if (typeof marginBottom === 'number') { style.marginBottom = `${marginBottom}em`; }
        if (typeof marginBottom === 'string') { style.marginBottom = marginBottom; }
        if (typeof padding === 'string') { style.padding = padding; }
        if (typeof paddingTop === 'string') { style.paddingTop = paddingTop; }
        if (typeof paddingLeft === 'string') { style.paddingLeft = paddingLeft; }
        if (typeof paddingRight === 'string') { style.paddingRight = paddingRight; }
        if (typeof paddingBottom === 'string') { style.paddingBottom = paddingBottom; }
        if (typeof wordBreak === 'string') { style.wordBreak = wordBreak; }
        if (typeof whiteSpace === 'string') { style.whiteSpace = whiteSpace; }
        if (typeof cursor === 'string') { style.cursor = cursor; }
        if (typeof listStyleType === 'string') { style.listStyleType = listStyleType; }
    }

    /**
     * @param {import('structured-content').LinkElement} content
     * @param {string} dictionary
     * @param {?string} language
     * @returns {HTMLAnchorElement}
     */
    _createLinkElement(content, dictionary, language) {
        let {href} = content;
        const internal = href.startsWith('?');
        if (internal) {
            href = `${location.protocol}//${location.host}/search.html${href.length > 1 ? href : ''}`;
        }

        const node = /** @type {HTMLAnchorElement} */ (this._createElement('a', 'gloss-link'));
        node.dataset.external = `${!internal}`;

        const text = this._createElement('span', 'gloss-link-text');
        node.appendChild(text);

        const {lang} = content;
        if (typeof lang === 'string') {
            node.lang = lang;
            language = lang;
        }

        this._appendStructuredContent(text, content.content, dictionary, language);

        if (!internal) {
            const icon = this._createElement('span', 'gloss-link-external-icon icon');
            icon.dataset.icon = 'external-link';
            node.appendChild(icon);
        }

        this._contentManager.prepareLink(node, href, internal);
        return node;
    }
}
