/**
 * Markup Parser Module
 * Parses author-facing story markup into renderable blocks and timed cue metadata.
 */
import { BaseModule } from './base-module.js';

class MarkupParserModule extends BaseModule {
    constructor() {
        super('markup-parser', 'Markup Parser');
        this.dependencies = ['game-config'];
        this.assetRoots = {
            images: '/images/',
            music: '/music/',
            sounds: '/sounds/'
        };

        this.bindMethods([
            'parse',
            'parseParagraph',
            'parseInline',
            'parseImageOptions',
            'parseSfxOptions',
            'parseMusicOptions',
            'markdownToHtml',
            'markdownToPlainText',
            'smartypants',
            'applyLocaleTypography',
            'getTypographyLocale',
            'normalizeDialogueQuotes',
            'escapeHtml',
            'normalizeParagraph',
            'buildParagraphBlock',
            'resolveAssetUrl'
        ]);
    }

    async initialize() {
        this.reportProgress(100, "Markup parser ready");
        return true;
    }

    parse(input) {
        const text = String(input || '').replace(/\r\n?/g, '\n');
        const blocks = [];
        let paragraphBuffer = [];

        const flushParagraph = () => {
            if (paragraphBuffer.length === 0) return;

            const raw = paragraphBuffer.join('\n');
            paragraphBuffer = [];
            const paragraph = this.parseParagraph(raw);
            if (!paragraph.text) return;

            const role = 'body';
            blocks.push(this.buildParagraphBlock(paragraph, role));
        };

        text.split('\n').forEach((line) => {
            const trimmed = line.trim();

            if (!trimmed) {
                flushParagraph();
                return;
            }

            paragraphBuffer.push(line);
        });

        flushParagraph();

        return blocks;
    }

    parseImageOptions(optionText) {
        const options = {
            size: 'landscape',
            leadInSeconds: 0
        };

        String(optionText || 'landscape')
            .split(/[,\s]+/)
            .map(token => token.trim())
            .filter(Boolean)
            .forEach((token, index) => {
                const lower = token.toLowerCase();
                const [key, value] = lower.split('=');

                if (['landscape', 'widescreen', 'portrait', 'square'].includes(lower)) {
                    options.size = lower === 'widescreen' ? 'landscape' : lower;
                } else if (['lead', 'lead-in', 'leadins', 'lead-in-seconds', 'delay', 'intro', 'pause', 'wait', 'hold'].includes(key)) {
                    const seconds = Number(value);
                    options.leadInSeconds = Number.isFinite(seconds) ? Math.max(0, seconds) : 0;
                } else if (/^\d+(\.\d+)?s?$/.test(lower)) {
                    options.leadInSeconds = Number(lower.replace(/s$/, ''));
                } else if (index === 0) {
                    console.warn(`MarkupParser: Unknown image size "${token}", using landscape`);
                }
            });

        return options;
    }

    parseMusicOptions(optionText) {
        const options = {
            mode: 'crossfade',
            loop: true,
            leadInSeconds: 0
        };

        String(optionText || '')
            .split(/[,\s]+/)
            .map(token => token.trim())
            .filter(Boolean)
            .forEach(token => {
                const lower = token.toLowerCase();
                const [key, value] = lower.split('=');

                if (['queue', 'crossfade', 'cut'].includes(lower)) {
                    options.mode = lower;
                } else if (['loop', 'looped', 'repeat'].includes(lower)) {
                    options.loop = true;
                } else if (['once', 'single', 'no-loop', 'noloop'].includes(lower)) {
                    options.loop = false;
                } else if (key === 'loop') {
                    options.loop = !['false', '0', 'no', 'once'].includes(value);
                } else if (['lead', 'lead-in', 'leadins', 'lead-in-seconds', 'delay', 'intro'].includes(key)) {
                    const seconds = Number(value);
                    options.leadInSeconds = Number.isFinite(seconds) ? Math.max(0, seconds) : 0;
                } else if (/^\d+(\.\d+)?s?$/.test(lower)) {
                    options.leadInSeconds = Number(lower.replace(/s$/, ''));
                }
            });

        return options;
    }

    parseSfxOptions(optionText) {
        const options = {
            maxDurationSeconds: 0,
            endMode: 'stop',
            fadeDurationSeconds: 2
        };

        String(optionText || '')
            .split(/[,\s]+/)
            .map(token => token.trim())
            .filter(Boolean)
            .forEach(token => {
                const lower = token.toLowerCase();
                const [key, value] = lower.split('=');

                if (['fade', 'fadeout', 'fade-out'].includes(lower)) {
                    options.endMode = 'fade';
                } else if (['stop', 'cut', 'halt'].includes(lower)) {
                    options.endMode = 'stop';
                } else if (['max', 'duration', 'max-duration', 'limit', 'stop-after', 'fade-after'].includes(key)) {
                    const seconds = Number(value);
                    options.maxDurationSeconds = Number.isFinite(seconds) ? Math.max(0, seconds) : 0;
                    if (key === 'fade-after') options.endMode = 'fade';
                } else if (/^\d+(\.\d+)?s?$/.test(lower)) {
                    options.maxDurationSeconds = Number(lower.replace(/s$/, ''));
                } else if (key === 'mode' && ['fade', 'fadeout', 'fade-out', 'stop', 'cut'].includes(value)) {
                    options.endMode = value.startsWith('fade') ? 'fade' : 'stop';
                } else if (['fade-duration', 'fade-time', 'fade'].includes(key)) {
                    const seconds = Number(value);
                    if (Number.isFinite(seconds)) {
                        options.fadeDurationSeconds = Math.max(0.1, seconds);
                        options.endMode = 'fade';
                    }
                }
            });

        return options;
    }

    parseParagraph(rawText) {
        const inline = this.parseInline(this.normalizeParagraph(rawText));
        return {
            text: this.markdownToPlainText(inline.text),
            layoutText: this.markdownToHtml(inline.text),
            cueMarkers: inline.cueMarkers
        };
    }

    buildParagraphBlock(paragraph, role) {
        return {
            type: 'paragraph',
            text: paragraph.text,
            layoutText: paragraph.layoutText,
            cueMarkers: paragraph.cueMarkers,
            role,
            isFirstParagraphInChapter: role === 'chapter-first' || role === 'textblock-first',
            dropCap: role === 'chapter-first',
            addTopSpace: role === 'textblock-first'
        };
    }

    parseInline(text) {
        return {
            text: String(text || '').replace(/\s{2,}/g, ' ').trim(),
            cueMarkers: []
        };
    }

    markdownToHtml(text) {
        const escaped = this.smartypants(this.escapeHtml(text));

        return escaped
            .replace(/\*\*\*([^*]+?)\*\*\*/g, '<strong><em>$1</em></strong>')
            .replace(/___([^_]+?)___/g, '<strong><em>$1</em></strong>')
            .replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>')
            .replace(/__([^_]+?)__/g, '<strong>$1</strong>')
            .replace(/\*([^*\s][^*]*?)\*/g, '<em>$1</em>')
            .replace(/_([^_\s][^_]*?)_/g, '<em>$1</em>');
    }

    markdownToPlainText(text) {
        const plain = String(text || '')
            .replace(/\*\*\*([^*]+?)\*\*\*/g, '$1')
            .replace(/___([^_]+?)___/g, '$1')
            .replace(/\*\*([^*]+?)\*\*/g, '$1')
            .replace(/__([^_]+?)__/g, '$1')
            .replace(/\*([^*\s][^*]*?)\*/g, '$1')
            .replace(/_([^_\s][^_]*?)_/g, '$1');

        return this.smartypants(plain).replace(/\s{2,}/g, ' ').trim();
    }

    smartypants(text) {
        const result = String(text)
            .replace(/---/g, '\u2014')
            .replace(/--/g, '\u2013')
            .replace(/\.\.\./g, '\u2026')
            .replace(/(^|[\s([{\u2014])"([^"]*)"/g, '$1\u201c$2\u201d')
            .replace(/(^|[\s([{\u2014])'([^']*)'/g, '$1\u2018$2\u2019');

        return this.applyLocaleTypography(result);
    }

    applyLocaleTypography(text) {
        const locale = this.getTypographyLocale();
        if (locale.startsWith('de')) {
            return this.normalizeDialogueQuotes(text);
        }

        return text;
    }

    getTypographyLocale() {
        const gameConfig = this.getModule('game-config') || window.GameConfig;
        const locale = gameConfig?.getLocale?.()
            || gameConfig?.getConfig?.()?.metadata?.language
            || 'en_US';

        return String(locale).trim().toLowerCase().replace('_', '-');
    }

    normalizeDialogueQuotes(text) {
        return String(text || '')
            .replace(/&(ldquo|bdquo|laquo|raquo);([^&\n]+?)&(rdquo|ldquo|laquo|raquo);/gi, '»$2«')
            .replace(/["\u201c\u201e\u201d\u00ab\u00bb]([^"\u201c\u201e\u201d\u00ab\u00bb\n]+?)["\u201c\u201d\u201e\u00ab\u00bb]/g, '»$1«');
    }

    escapeHtml(text) {
        return String(text)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }

    normalizeParagraph(text) {
        return String(text).replace(/\s*\n\s*/g, ' ').trim();
    }

    countWords(text) {
        const words = String(text).trim().match(/\S+/g);
        return words ? words.length : 0;
    }

    resolveAssetUrl(kind, filename) {
        const root = this.assetRoots[kind];
        const safeName = String(filename || '').replace(/\\/g, '/').replace(/^\/+/, '');

        if (!root || !safeName || safeName.includes('..') || /^[a-z]+:/i.test(safeName)) {
            return '';
        }

        return root + safeName.split('/').map(encodeURIComponent).join('/');
    }
}

const MarkupParser = new MarkupParserModule();

export { MarkupParser };

if (window.moduleRegistry) {
    window.moduleRegistry.register(MarkupParser);
}

window.MarkupParser = MarkupParser;
