import clsx from 'clsx';
import styles from './assets/highlight.module.css';

const parseTableRow = (line: string): string[] =>
    line.split('|').slice(1, -1).map(cell => cell.trim());

const isSeparatorRow = (line: string): boolean =>
    /^\|[\s|:\-]+\|$/.test(line.trim());

export const parseList = (block: string, ordered: boolean): string => {
    const marker = ordered ? /^\d+\. / : /^[-+] /;
    const items = block.trim().split('\n')
        .map(l => `<li>${l.trim().replace(marker, '')}</li>`)
        .join('');
    const tag = ordered ? 'ol' : 'ul';
    return `<${tag} class="${styles.list}">${items}</${tag}>`;
};

export const parseTable = (table: string): string => {
    const lines = table.trim().split('\n').map(l => l.trim()).filter(l => l.startsWith('|'));
    if (lines.length < 2) return table;

    const sepIndex = lines.findIndex(isSeparatorRow);
    if (sepIndex === -1) return table;

    const headerLines = lines.slice(0, sepIndex);
    const bodyLines = lines.slice(sepIndex + 1);

    const renderCells = (cells: string[], tag: 'th' | 'td') =>
        cells.map(cell => `<${tag}>${cell}</${tag}>`).join('');

    const headers = headerLines
        .map(l => `<tr>${renderCells(parseTableRow(l), 'th')}</tr>`)
        .join('');
    const rows = bodyLines
        .map(l => `<tr>${renderCells(parseTableRow(l), 'td')}</tr>`)
        .join('');

    return `<table class="${styles.table}"><thead>${headers}</thead><tbody>${rows}</tbody></table>`;
};

export const highlight = (message: string, keepMarkup = true): string => {
    let resultHTML = '';
    const tokenRegex = /(\*{1,3}|"|```|`|(?:^|\n)#{1,3} |(?:^|\n)> |\n)|---/g;
    const headerRegex = /#{1,3} $/;
    const blockquoteRegex = /> $/;
    const stack: string[] = [];
    let inCodeBlock = false;
    let inMonospaced = false;
    let inHeader = false;
    let inBlockquote = false;
    let lastIndex = 0;
    let match: RegExpExecArray | null;

    while ((match = tokenRegex.exec(message)) !== null) {
        resultHTML += message.slice(lastIndex, match.index);
        lastIndex = tokenRegex.lastIndex;

        const token = match[0];
        const isClose = stack.at(-1) === token;
        const keepToken = keepMarkup || token === '"';

        if (inCodeBlock) {
            if (token === '```' && isClose) {
                inCodeBlock = false;
                stack.pop();
                resultHTML += `${keepToken ? token : ''}</span>`;
            } else {
                resultHTML += token;
            }
            continue;
        }

        if (inMonospaced) {
            if (token === '`' && isClose) {
                inMonospaced = false;
                stack.pop();
                resultHTML += `${keepToken ? token : ''}</span>`;
            } else {
                resultHTML += token;
            }
            continue;
        }

        const headerMatch = token.match(headerRegex);
        if (headerMatch) {
            if (inHeader) resultHTML += '</span>';
            if (inBlockquote) { resultHTML += '</span>'; inBlockquote = false; }
            const markup = keepMarkup ? headerMatch[0] : '';
            const len = headerMatch[0].length;
            inHeader = true;
            resultHTML += `${token.slice(0, -len)}<span class="${clsx(styles.header, styles[`header${len - 1}`])}">${markup}`;
            continue;
        }

        const blockquoteMatch = token.match(blockquoteRegex);
        if (blockquoteMatch) {
            if (inHeader) { resultHTML += '</span>'; inHeader = false; }
            if (inBlockquote) resultHTML += '</span>';
            const markup = keepMarkup ? '> ' : '';
            inBlockquote = true;
            resultHTML += `${token.slice(0, -2)}<span class="${styles.blockquote}">${markup}`;
            continue;
        }

        if (token === '\n') {
            if (inHeader) {
                resultHTML += `${keepMarkup ? '\n' : ''}</span>`;
                inHeader = false;
            } else if (inBlockquote) {
                resultHTML += `${keepMarkup ? '\n' : ''}</span>`;
                inBlockquote = false;
            } else {
                resultHTML += '\n';
            }
            continue;
        }

        if (token === '***' || token === '---') {
            resultHTML += `<span class="${styles.hr}">${keepToken ? token : ''}</span>`;
        } else if (isClose) {
            stack.pop();
            resultHTML += `${keepToken ? token : ''}</span>`;
        } else if (token === '*') {
            stack.push(token);
            resultHTML += `<span class="${styles.italic}">${keepToken ? token : ''}`;
        } else if (token === '**') {
            stack.push(token);
            resultHTML += `<span class="${styles.bold}">${keepToken ? token : ''}`;
        } else if (token === '"') {
            stack.push(token);
            resultHTML += `<span class="${styles.quote}">"`;
        } else if (token === '```') {
            stack.push(token);
            inCodeBlock = true;
            resultHTML += `<span class="${styles.codeBlock}">${keepToken ? token : ''}`;
        } else if (token === '`') {
            stack.push(token);
            inMonospaced = true;
            resultHTML += `<span class="${styles.inlineCode}">${keepToken ? token : ''}`;
        }
    }

    resultHTML += message.slice(lastIndex);

    if (inHeader) resultHTML += '</span>';
    if (inBlockquote) resultHTML += '</span>';
    resultHTML += '</span>'.repeat(stack.length);

    if (!keepMarkup) {
        resultHTML = resultHTML.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) =>
            `<img src="${src}" alt="${alt}" class="${styles.image}"/>`
        );
        resultHTML = resultHTML.replace(/((?:(?:^|\n)\|.+)+)/g, match => parseTable(match));
        resultHTML = resultHTML.replace(/((?:(?:^|\n)[-+] .+)+)/g, match => parseList(match, false));
        resultHTML = resultHTML.replace(/((?:(?:^|\n)\d+\. .+)+)/g, match => parseList(match, true));
    }

    return resultHTML;
}
