import type { EditorRange, HeadingCache } from "obsidian";
import { stringifyYaml } from "obsidian";
import { instaTocCodeBlockId } from "./constants";
import type EditorService from "./editorService";
import type { InstaTocSettings } from "./settings/Settings";
import { resolveTocTitle } from "./settings/localTocSettings";
import { normalizeLocalTocSettings } from "./settings/localTocSettings";
import type { Validator } from "./validator";

export class ManageToc {
    private editorService: EditorService;
    private settings: InstaTocSettings;
    private validator: Validator;
    private headingLevelStack: number[];

    private constructor(editorService: EditorService, settings: InstaTocSettings, validator: Validator) {
        this.editorService = editorService;
        this.settings = settings;
        this.validator = validator;
        this.headingLevelStack = [];
    }

    public static async run(
        editorService: EditorService,
        settings: InstaTocSettings,
        validator: Validator
    ): Promise<void> {
        const instance = new ManageToc(editorService, settings, validator);
        await instance.updateAutoToc();
    }

    // Determine the correct indentation level
    private getIndentationLevel(headingLevel: number): number {
        // Pop from the stack until we find a heading level less than the current
        while (
            this.headingLevelStack.length > 0 // Avoid indentation for the first heading
            && headingLevel <= this.headingLevelStack[this.headingLevelStack.length - 1]
        ) {
            this.headingLevelStack.pop();
        }
        this.headingLevelStack.push(headingLevel);

        const currentIndentLevel = this.headingLevelStack.length - 1;

        return currentIndentLevel;
    }

    private createTocContent(tocHeadingRefs: string[]): string {
        const title = resolveTocTitle(this.validator.localTocSettings, this.settings);
        const localSettingsYaml = stringifyYaml(normalizeLocalTocSettings(this.validator.localTocSettings)).trimEnd();
        const localSettingsContent = `---\n${localSettingsYaml}\n---`;
        const titleContent = title ? `${"#".repeat(title.level)} ${title.text}` : "";
        const tocList = tocHeadingRefs.join("\n");

        return [ localSettingsContent, titleContent, tocList ].filter((section) => section.length > 0).join("\n\n");
    }

    private getMinimalDiff(
        currentTocBlock: string,
        nextTocBlock: string
    ): { startOffset: number; endOffset: number; insert: string; } | null {
        if (currentTocBlock === nextTocBlock) {
            return null;
        }

        let startOffset = 0;
        const maxPrefixLength = Math.min(currentTocBlock.length, nextTocBlock.length);

        while (startOffset < maxPrefixLength && currentTocBlock[startOffset] === nextTocBlock[startOffset]) {
            startOffset += 1;
        }

        let currentEndOffset = currentTocBlock.length;
        let nextEndOffset = nextTocBlock.length;

        while (
            currentEndOffset > startOffset
            && nextEndOffset > startOffset
            && currentTocBlock[currentEndOffset - 1] === nextTocBlock[nextEndOffset - 1]
        ) {
            currentEndOffset -= 1;
            nextEndOffset -= 1;
        }

        return { startOffset, endOffset: currentEndOffset, insert: nextTocBlock.slice(startOffset, nextEndOffset) };
    }

    private offsetToPosition(source: string, offset: number, start: EditorRange["from"]): EditorRange["from"] {
        const precedingText = source.slice(0, offset);
        const lines = precedingText.split("\n");
        const lineOffset = lines.length - 1;
        const lastLine = lines[lineOffset] ?? "";

        return { line: start.line + lineOffset, ch: lineOffset === 0 ? start.ch + lastLine.length : lastLine.length };
    }

    /** Generates a new insta-toc codeblock with normal dash-type bullets */
    private generateToc(): string {
        const tocHeadingRefs: string[] = [];
        const fileHeadings: HeadingCache[] = this.validator.fileHeadings;

        for (const headingCache of fileHeadings) {
            const headingLevel: number = headingCache.level;
            const headingText: string = headingCache.heading;

            if (headingText.length === 0) continue;

            const currentIndentLevel = this.getIndentationLevel(headingLevel);

            // Calculate the indentation based on the current indentation level
            const indent: string = " ".repeat(currentIndentLevel * 4);
            const tocHeadingRef = `${indent}- ${headingText}`;

            tocHeadingRefs.push(tocHeadingRef);
        }

        const tocContent: string = this.createTocContent(tocHeadingRefs);
        return `\`\`\`${instaTocCodeBlockId}\n${tocContent}\n\`\`\``;
    }

    private getTocUpdate(
        insertRange: EditorRange,
        newTocBlock: string
    ): { from: EditorRange["from"]; to: EditorRange["to"]; insert: string; } | null {
        const existingTocBlock: string | undefined = this.editorService.editor?.getRange(
            insertRange.from,
            insertRange.to
        );

        if (existingTocBlock === undefined) {
            return null;
        }

        const diff = this.getMinimalDiff(existingTocBlock, newTocBlock);

        if (!diff) {
            return null;
        }

        return {
            from: this.offsetToPosition(existingTocBlock, diff.startOffset, insertRange.from),
            to: this.offsetToPosition(existingTocBlock, diff.endOffset, insertRange.from),
            insert: diff.insert
        };
    }

    // Dynamically update the TOC
    private async updateAutoToc(): Promise<void> {
        const tocInsertRange: EditorRange = this.validator.tocInsertPos;
        const newTocBlock: string = this.generateToc();
        const tocUpdate = this.getTocUpdate(tocInsertRange, newTocBlock);

        if (tocUpdate) {
            await this.editorService.applyEditorChange(tocUpdate.from, tocUpdate.to, tocUpdate.insert);
        }
    }
}
