import { commands, DocumentSymbol, Range, Selection, Uri, window } from "vscode";
import { jsonAvailable } from "../features/linters/jsonAvailable";
import {
    GoToDefinitionCommandContext,
    GoToDefinitionFile,
    GoToDefinitionType,
} from "./contexts/GoToDefinitionCommandContext";
import { Telemetry } from "../lib/Telemetry";
import { NinjaALRange } from "../lib/types/NinjaALRange";
import { CodeCommand } from "./commands";

export function goToDefinition(context: GoToDefinitionCommandContext<NinjaALRange>) {
    Telemetry.instance.log("goto-def", context.goto.app.hash, { file: context.goto.file, type: context.goto.type });

    switch (context.goto.file) {
        case GoToDefinitionFile.Manifest:
            goToManifest(context);
            break;

        case GoToDefinitionFile.Configuration:
            goToConfiguration(context);
            break;
    }
}

async function getIdRanges(uri: Uri): Promise<DocumentSymbol | undefined> {
    await jsonAvailable;

    const symbols = await (commands.executeCommand(CodeCommand.ExecuteDocumentSymbolProvider, uri) as Promise<
        DocumentSymbol[] | undefined
    >);
    if (!symbols) {
        return;
    }
    return symbols.find(symbol => symbol.name === "idRanges");
}

async function getObjectRanges(uri: Uri): Promise<DocumentSymbol | undefined> {
    await jsonAvailable;

    const symbols = await (commands.executeCommand(CodeCommand.ExecuteDocumentSymbolProvider, uri) as Promise<
        DocumentSymbol[] | undefined
    >);
    if (!symbols) {
        return;
    }
    return symbols.find(symbol => symbol.name === "objectRanges");
}

async function getNamedLogicalRanges(uri: Uri, name: string): Promise<DocumentSymbol[] | undefined> {
    const idRanges = await getIdRanges(uri);
    if (!idRanges) {
        return;
    }
    const result: DocumentSymbol[] = [];
    for (let range of idRanges!.children) {
        if (
            range.children.find(c => c.name === "description" && c.detail === name) ||
            (!name && !range.children.find(c => c.name === "description"))
        ) {
            result.push(range);
        }
    }
    return result;
}

async function getObjectTypeRanges(uri: Uri, objectType: string): Promise<DocumentSymbol | undefined> {
    const objectRanges = await getObjectRanges(uri);
    return objectRanges?.children.find(r => r.name === objectType!);
}

async function goToManifest({ goto }: GoToDefinitionCommandContext<NinjaALRange>) {
    const { uri } = goto.app.manifest;
    const idRanges = await getIdRanges(uri);
    if (!idRanges) {
        return;
    }

    const editor = await window.showTextDocument(uri);

    const from = `${goto.range?.from}`;
    const to = `${goto.range?.to}`;

    switch (goto.type) {
        case GoToDefinitionType.IdRanges:
            editor.selection = new Selection(idRanges!.range.start, idRanges!.range.end);
            break;

        case GoToDefinitionType.Range:
            const range = idRanges.children.find(
                c =>
                    c.children.find(c => c.name === "from" && c.detail === from) &&
                    c.children.find(c => c.name === "to" && c.detail === to)
            );
            if (range) {
                editor.selection = new Selection(range.range.start, range.range.end);
            }
            break;
    }
}

async function goToConfiguration({ goto }: GoToDefinitionCommandContext<NinjaALRange>) {
    const { uri } = goto.app.config;
    const from = `${goto.range?.from}`;
    const to = `${goto.range?.to}`;

    let selection: Selection | undefined;
    let selections: Selection[] = [];
    let idRanges: DocumentSymbol | undefined;
    let objectRanges: DocumentSymbol | undefined;
    let objectTypeRanges: DocumentSymbol | undefined;
    let logicalRanges: DocumentSymbol[] | undefined;

    switch (goto.type) {
        case GoToDefinitionType.IdRanges:
            idRanges = await getIdRanges(uri);
            selection = new Selection(idRanges!.range.start, idRanges!.range.end);
            break;

        case GoToDefinitionType.ObjectRanges:
            objectRanges = await getObjectRanges(uri);
            selection = new Selection(objectRanges!.range.start, objectRanges!.range.end);
            break;

        case GoToDefinitionType.LogicalName:
            logicalRanges = await getNamedLogicalRanges(uri, goto.logicalName!);
            if (!logicalRanges || logicalRanges.length === 0) {
                return;
            }
            for (let range of logicalRanges!) {
                selections.push(new Selection(range.range.start, range.range.end));
            }
            break;

        case GoToDefinitionType.Range:
            logicalRanges = await getNamedLogicalRanges(uri, goto.range!.description);
            if (!logicalRanges || logicalRanges.length === 0) {
                return;
            }
            const range = logicalRanges.find(
                r =>
                    r.children.find(c => c.name === "from" && c.detail === from) &&
                    r.children.find(c => c.name === "to" && c.detail === to)
            );
            if (range) {
                selection = new Selection(range.range.start, range.range.end);
            }
            break;

        case GoToDefinitionType.ObjectType:
            objectTypeRanges = await getObjectTypeRanges(uri, goto.objectType!);
            if (objectTypeRanges) {
                selection = new Selection(objectTypeRanges.range.start, objectTypeRanges.range.end);
            }
            break;

        case GoToDefinitionType.ObjectTypeRanges:
            objectTypeRanges = await getObjectTypeRanges(uri, goto.objectType!);
            const logicalObjectTypeRanges = objectTypeRanges?.children.filter(c =>
                c.children.find(c => c.name === "description" && c.detail === goto.logicalName!)
            );
            if (!logicalObjectTypeRanges) {
                return;
            }
            for (let range of logicalObjectTypeRanges) {
                selections.push(new Selection(range.range.start, range.range.end));
            }
            break;

        case GoToDefinitionType.ObjectTypeRange:
            objectTypeRanges = await getObjectTypeRanges(uri, goto.objectType!);
            const logicalObjectTypeRange = objectTypeRanges?.children.find(
                c =>
                    c.children.find(c => c.name === "description" && c.detail === goto.range!.description) &&
                    c.children.find(c => c.name === "from" && c.detail === from) &&
                    c.children.find(c => c.name === "to" && c.detail === to)
            );
            if (logicalObjectTypeRange) {
                selection = new Selection(logicalObjectTypeRange.range.start, logicalObjectTypeRange.range.end);
            }
    }

    if (!selection && selections.length === 0) {
        return;
    }

    const editor = await window.showTextDocument(uri);
    if (selection) {
        editor.selection = selection;
        editor.revealRange(new Range(selection.start, selection.end));
    } else {
        editor.selections = selections;
        const firstSelection = selections.reduce(
            (previous, current) => (current.start.line < previous.start.line ? current : previous),
            selections[0]
        );
        const lastSelection = selections.reduce(
            (previous, current) => (current.end.line > previous.end.line ? current : previous),
            selections[0]
        );
        editor.revealRange(new Range(firstSelection.start, lastSelection.end));
    }
}