import * as vscode from 'vscode';
import * as ra from './rust-analyzer-api';

import { ColorTheme, TextMateRuleSettings } from './color_theme';

import { Ctx } from './ctx';
import { sendRequestWithRetry, isRustDocument } from './util';

export function activateHighlighting(ctx: Ctx) {
    const highlighter = new Highlighter(ctx);

    ctx.client.onNotification(ra.publishDecorations, params => {
        if (!ctx.config.highlightingOn) return;

        const targetEditor = vscode.window.visibleTextEditors.find(
            editor => {
                const unescapedUri = unescape(
                    editor.document.uri.toString(),
                );
                // Unescaped URI looks like:
                // file:///c:/Workspace/ra-test/src/main.rs
                return unescapedUri === params.uri;
            },
        );
        if (!targetEditor) return;

        highlighter.setHighlights(targetEditor, params.decorations);
    });


    vscode.workspace.onDidChangeConfiguration(
        _ => highlighter.removeHighlights(),
        null,
        ctx.subscriptions,
    );

    vscode.window.onDidChangeActiveTextEditor(
        async (editor: vscode.TextEditor | undefined) => {
            if (!editor || !isRustDocument(editor.document)) return;
            if (!ctx.config.highlightingOn) return;
            const client = ctx.client;
            if (!client) return;

            const decorations = await sendRequestWithRetry(
                client,
                ra.decorationsRequest,
                { uri: editor.document.uri.toString() },
            );
            highlighter.setHighlights(editor, decorations);
        },
        null,
        ctx.subscriptions,
    );
}

// Based on this HSL-based color generator: https://gist.github.com/bendc/76c48ce53299e6078a76
function fancify(seed: string, shade: 'light' | 'dark') {
    const random = randomU32Numbers(hashString(seed));
    const randomInt = (min: number, max: number) => {
        return Math.abs(random()) % (max - min + 1) + min;
    };

    const h = randomInt(0, 360);
    const s = randomInt(42, 98);
    const l = shade === 'light' ? randomInt(15, 40) : randomInt(40, 90);
    return `hsl(${h},${s}%,${l}%)`;
}

class Highlighter {
    private ctx: Ctx;
    private decorations: Map<
        string,
        vscode.TextEditorDecorationType
    > | null = null;

    constructor(ctx: Ctx) {
        this.ctx = ctx;
    }

    public removeHighlights() {
        if (this.decorations == null) {
            return;
        }

        // Decorations are removed when the object is disposed
        for (const decoration of this.decorations.values()) {
            decoration.dispose();
        }

        this.decorations = null;
    }

    public setHighlights(editor: vscode.TextEditor, highlights: ra.Decoration[]) {
        const client = this.ctx.client;
        if (!client) return;
        // Initialize decorations if necessary
        //
        // Note: decoration objects need to be kept around so we can dispose them
        // if the user disables syntax highlighting
        if (this.decorations == null) {
            this.decorations = initDecorations();
        }

        const byTag: Map<string, vscode.Range[]> = new Map();
        const colorfulIdents: Map<
            string,
            [vscode.Range[], boolean]
        > = new Map();
        const rainbowTime = this.ctx.config.rainbowHighlightingOn;

        for (const tag of this.decorations.keys()) {
            byTag.set(tag, []);
        }

        for (const d of highlights) {
            if (!byTag.get(d.tag)) {
                continue;
            }

            if (rainbowTime && d.bindingHash) {
                if (!colorfulIdents.has(d.bindingHash)) {
                    const mut = d.tag.endsWith('.mut');
                    colorfulIdents.set(d.bindingHash, [[], mut]);
                }
                colorfulIdents
                    .get(d.bindingHash)![0]
                    .push(
                        client.protocol2CodeConverter.asRange(d.range),
                    );
            } else {
                byTag
                    .get(d.tag)!
                    .push(
                        client.protocol2CodeConverter.asRange(d.range),
                    );
            }
        }

        for (const tag of byTag.keys()) {
            const dec = this.decorations.get(
                tag,
            ) as vscode.TextEditorDecorationType;
            const ranges = byTag.get(tag)!;
            editor.setDecorations(dec, ranges);
        }

        for (const [hash, [ranges, mut]] of colorfulIdents.entries()) {
            const textDecoration = mut ? 'underline' : undefined;
            const dec = vscode.window.createTextEditorDecorationType({
                light: { color: fancify(hash, 'light'), textDecoration },
                dark: { color: fancify(hash, 'dark'), textDecoration },
            });
            editor.setDecorations(dec, ranges);
        }
    }
}

function initDecorations(): Map<string, vscode.TextEditorDecorationType> {
    const theme = ColorTheme.load();
    const res = new Map();
    TAG_TO_SCOPES.forEach((scopes, tag) => {
        // We are going to axe this soon, so don't try to detect unknown tags.
        // Users should switch to the new semantic tokens implementation.
        if (!scopes) return;
        const rule = theme.lookup(scopes);
        const decor = createDecorationFromTextmate(rule);
        res.set(tag, decor);
    });
    return res;
}

function createDecorationFromTextmate(
    themeStyle: TextMateRuleSettings,
): vscode.TextEditorDecorationType {
    const decorationOptions: vscode.DecorationRenderOptions = {};
    decorationOptions.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen;

    if (themeStyle.foreground) {
        decorationOptions.color = themeStyle.foreground;
    }

    if (themeStyle.background) {
        decorationOptions.backgroundColor = themeStyle.background;
    }

    if (themeStyle.fontStyle) {
        const parts: string[] = themeStyle.fontStyle.split(' ');
        parts.forEach(part => {
            switch (part) {
                case 'italic':
                    decorationOptions.fontStyle = 'italic';
                    break;
                case 'bold':
                    decorationOptions.fontWeight = 'bold';
                    break;
                case 'underline':
                    decorationOptions.textDecoration = 'underline';
                    break;
                default:
                    break;
            }
        });
    }
    return vscode.window.createTextEditorDecorationType(decorationOptions);
}

// sync with tags from `syntax_highlighting.rs`.
const TAG_TO_SCOPES = new Map<string, string[]>([
    ["field", ["entity.name.field"]],
    ["function", ["entity.name.function"]],
    ["module", ["entity.name.module"]],
    ["constant", ["entity.name.constant"]],
    ["macro", ["entity.name.macro"]],

    ["variable", ["variable"]],
    ["variable.mutable", ["variable", "meta.mutable"]],

    ["type", ["entity.name.type"]],
    ["type.builtin", ["entity.name.type", "support.type.primitive"]],
    ["type.self", ["entity.name.type.parameter.self"]],
    ["type.param", ["entity.name.type.parameter", "entity.name.type.param.rust"]],
    ["type.lifetime", ["entity.name.type.lifetime", "entity.name.lifetime.rust"]],

    ["literal.byte", ["constant.character.byte"]],
    ["literal.char", ["constant.character.rust"]],
    ["numeric_literal", ["constant.numeric"]],

    ["comment", ["comment"]],
    ["string_literal", ["string.quoted"]],
    ["attribute", ["meta.attribute.rust"]],

    ["keyword", ["keyword"]],
    ["keyword.unsafe", ["keyword.other.unsafe"]],
    ["keyword.control", ["keyword.control"]],
]);

function randomU32Numbers(seed: number) {
    let random = seed | 0;
    return () => {
        random ^= random << 13;
        random ^= random >> 17;
        random ^= random << 5;
        random |= 0;
        return random;
    };
}

function hashString(str: string): number {
    let res = 0;
    for (let i = 0; i < str.length; ++i) {
        const c = str.codePointAt(i)!;
        res = (res * 31 + c) & ~0;
    }
    return res;
}