import { BrushPreview } from "rkgk/brush-preview.js";
import { Haku } from "rkgk/haku.js";
import { randomId } from "rkgk/random.js";
import { SaveData } from "rkgk/framework.js";
import { ContextMenu, globalContextMenuSpace } from "rkgk/context-menu.js";

export const builtInPresets = [
    {
        id: "builtin/default",
        name: "Default",
        code: `
-- Try playing around with the values
-- and see what happens!
color = #000
thickness = 8

withDotter \\d ->
  stroke thickness color (line (d From) (d To))
        `.trim(),
    },

    {
        id: "builtin/thick",
        name: "Thick",
        code: `
color = #000
thickness = 48

withDotter \\d ->
  stroke thickness color (line (d From) (d To))
        `.trim(),
    },

    {
        id: "builtin/pencil",
        name: "Pencil",
        code: `
color = #0003
thickness = 6

withDotter \\d ->
  stroke thickness color (line (d From) (d To))
        `.trim(),
    },

    {
        id: "builtin/woobly",
        name: "Woobly",
        code: `
color = #000
minThickness = 8
maxThickness = 20
wavelength = 1

withDotter \\d ->
  let pi = 3.14159265
  let a = (sin (d Num * wavelength / pi) + 1) / 2
  let range = maxThickness - minThickness
  let thickness = minThickness + a * range
  stroke thickness color (line (d From) (d To))
        `.trim(),
    },

    {
        id: "builtin/wavy",
        name: "Wavy",
        code: `
color = #000
thickness = 4
amplitude = 50
wavelength = 1

mag = \\v ->
  hypot (vecX v) (vecY v)

norm = \\u ->
  let l = mag u
  u / vec l l

perpClockwise = \\v ->
  vec (vecY v) (-(vecX v))

withDotter \\d ->
  let pi = 3.14159265
  let a = sin (d Num * wavelength / pi) * amplitude
  let direction = (d To) - (d From)
  let clockwise = norm (perpClockwise direction) * vec a a
  let from = d From + clockwise
  let to = d To + clockwise
  stroke thickness color (line from to)
        `.trim(),
    },

    {
        id: "builtin/rainbow",
        name: "Rainbow",
        code: `
wavelength = 0.1
thickness = 8

colorCurve = \\n ->
  abs (cos n)

withDotter \\d ->
  let pi = 3.14159265
  let l = wavelength
  let r = colorCurve (d Num * l)
  let g = colorCurve (d Num * l + pi/3)
  let b = colorCurve (d Num * l + 2*pi/3)
  let color = rgba r g b 1
  stroke thickness color (line (d From) (d To))
        `.trim(),
    },

    // ...feel free to add more presets here!
];

function presetButton(info) {
    let button = document.createElement("button");
    let preview = button.appendChild(new BrushPreview(56, 56));
    let label = button.appendChild(document.createElement("p"));
    label.classList.add("label");
    label.innerText = info.name;
    return { button, preview };
}

export class BrushBox extends HTMLElement {
    dirty = false;

    saveData = new SaveData("brushBox");

    connectedCallback() {
        this.saveData.attachToElement(this);

        this.classList.add("rkgk-panel");

        this.brushes = [];

        this.brushesContainer = this.appendChild(document.createElement("div"));
        this.brushesContainer.classList.add("brushes");

        this.userPresets = this.saveData.get("userPresets", []);

        this.updateBrushes();

        // NOTE: Restoring the currently selected brush from local storage must NOT end up in a
        // code editor update, so as not to reset the user's currently WIP brush.
        let savedCurrentBrush = this.saveData.get("currentBrush", "builtin/default");
        if (this.findBrushPreset(savedCurrentBrush)) {
            this.setCurrentBrush(savedCurrentBrush);
        } else {
            console.warn("brush preset does not exist", savedCurrentBrush);
            this.setCurrentBrush("builtin/default");
            this.#sendBrushChange(this.findBrushPreset(this.currentPresetId));
        }
    }

    initialize(wallLimits) {
        this.haku = new Haku(wallLimits);
        this.renderBrushes();
    }

    updateBrushes() {
        this.brushesContainer.replaceChildren();
        this.brushes = [];

        const addBrush = (preset, { mutable }) => {
            let pb = presetButton(preset);
            this.brushesContainer.appendChild(pb.button);
            this.brushes.push({
                preset,
                presetButton: pb,
            });

            pb.button.addEventListener("click", (event) => {
                if (this.dirty) {
                    let confirmation = new ContextMenu();

                    confirmation.addHtmlP(
                        "<strong>Your brush has unsaved changes.</strong>" +
                            "<br>Switching to another brush will discard them." +
                            "<br>Are you sure?",
                    );
                    confirmation.addSeparator();
                    confirmation
                        .addButton("Discard changes and switch brush", {
                            classList: ["destructive"],
                        })
                        .addEventListener("click", () => {
                            this.#sendBrushChange(preset);
                        });
                    confirmation.addHtmlP("Click anywhere else to cancel", {
                        classList: ["small"],
                    });

                    globalContextMenuSpace.openAtCursor(event, confirmation);
                } else {
                    this.#sendBrushChange(preset);
                }
            });

            pb.button.addEventListener("contextmenu", (event) => {
                event.preventDefault();

                let menu = new ContextMenu();

                menu.addButton(this.currentPresetId == preset.id ? "Save" : "Overwrite", {
                    disabled: !mutable,
                    tooltip: mutable ? "" : "This is a built-in brush and cannot be modified",
                }).addEventListener("click", () => {
                    this.dispatchEvent(
                        Object.assign(new Event(".overwritePreset"), {
                            preset,
                        }),
                    );

                    this.updateBrushes();
                    this.renderBrushes();
                    this.markClean();
                    this.saveUserPresets();
                });

                menu.addButton("Duplicate").addEventListener("click", () => {
                    let id = this.saveUserPreset(preset);
                    this.updateBrushes();
                    this.renderBrushes();

                    if (!this.dirty) {
                        this.currentPresetId = id;
                        let duped = this.findBrushPreset(this.currentPresetId);
                        this.#sendBrushChange(duped);
                    }
                });

                menu.addButton("Delete", {
                    classList: ["destructive"],
                    disabled: !mutable,
                    tooltip: mutable ? "" : "This is a built-in brush and cannot be deleted",
                }).addEventListener("click", () => {
                    // NOTE: This leaves the box in a weird state where there is no current brush.
                    // That's okay; code should not assume there's a current preset ID.
                    if (this.currentPresetId == preset.id) {
                        this.currentPresetId = null;
                        this.markDirty();
                    }

                    let index = this.userPresets.indexOf(preset);
                    this.userPresets.splice(index, 1);

                    this.updateBrushes();
                    this.renderBrushes();
                    this.saveUserPresets();
                    // NOTE: No brush change because we don't want to overwrite the editor.
                });

                globalContextMenuSpace.openAtCursor(event, menu);
            });
        };

        for (let preset of builtInPresets) addBrush(preset, { mutable: false });
        for (let preset of this.userPresets) addBrush(preset, { mutable: true });

        let saveButton = this.brushesContainer.appendChild(document.createElement("button"));
        saveButton.classList.add("save-button");
        let plusIcon = saveButton.appendChild(document.createElement("div"));
        plusIcon.classList.add("icon", "icon-plus");
        let saveLabel = saveButton.appendChild(document.createElement("p"));
        saveLabel.classList.add("label");
        saveLabel.innerText = "Save new";

        saveButton.addEventListener("click", () => {
            this.dispatchEvent(new Event(".clickNew"));
        });

        if (this.currentPresetId != null) this.setCurrentBrush(this.currentPresetId);
    }

    async renderBrushes() {
        for (let brush of this.brushes) {
            this.haku.setBrush(brush.preset.code);
            await brush.presetButton.preview.renderBrush(this.haku);
        }
    }

    setCurrentBrush(id) {
        this.currentPresetId = id;
        this.saveData.set("currentBrush", this.currentPresetId);

        for (let brush of this.brushes) {
            let button = brush.presetButton.button;
            if (brush.preset.id != id) button.classList.remove("current");
            else button.classList.add("current");
        }
    }

    indexOfBrushPreset(id) {
        for (let i in this.brushes) {
            if (this.brushes[i].preset.id == id) return i;
        }
        return null;
    }

    findBrushPreset(id) {
        let index = this.indexOfBrushPreset(id);
        if (index == null) return null;
        return this.brushes[index].preset;
    }

    markDirty() {
        this.dirty = true;
        this.classList.add("unsaved");
    }

    markClean() {
        this.dirty = false;
        this.classList.remove("unsaved");
    }

    saveUserPreset({ name, code }, id = null) {
        if (id == null) {
            // Save a new preset.
            id = `user/${randomId()}`;
            console.log("saving new brush", id);
            this.userPresets.push({
                id,
                name,
                code,
            });
        } else {
            // Overwrite an existing one.
            let preset = this.userPresets.find((p) => p.id == id);
            console.log("overwriting existing brush", preset);
            preset.code = code;
        }
        this.saveUserPresets();
        return id;
    }

    saveUserPresets() {
        this.saveData.set("userPresets", this.userPresets);
    }

    #sendBrushChange(preset) {
        this.dispatchEvent(
            Object.assign(new Event(".brushChange"), {
                preset,
            }),
        );
    }
}

customElements.define("rkgk-brush-box", BrushBox);
