import fs from "fs";
import { dirname, join, resolve as _resolve } from "path";
import { randomBytes } from "crypto";

import GIFEncoder from "gif-encoder-2";
import { PNG } from "pngjs";

import { generateSpinFrames } from "./quat.js";
import { run } from "./run.js";

import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));

import type { XYZ } from "./quat.js";


export function resolve(...path: Array<string>): string {
    return _resolve(__dirname, "..", ...path);
}

export interface RenderXYZ {
    x?: number,
    y?: number,
    z?: number
}

export interface RenderSize {
    height?: number,
    width?: number
}

export interface FlyAroundOptions {
    perspective?: boolean;
    zoom?: number,

    rotate?: RenderXYZ,

    size?: RenderSize
}

export interface PlotOptions {
    //format?: "pdf" | "svg"; // "ps"
    //
}

export interface RenderOptions {
    perspective?: boolean,
    zoom?: number,

//    pan?: RenderXYZ,
    rotate?: RenderXYZ,
//    pivot?: RenderXYZ,

    size?: RenderSize
}

function flatXYZ(xyz?: RenderXYZ): string {
    if (xyz == null) { return "0,0,0"; }

    return "'" + [
        (xyz.x || 0).toFixed(2),
        (xyz.y || 0).toFixed(2),
        (xyz.z || 0).toFixed(2),
    ].join(",") + "'";
}

function normalizeSize(size?: RenderSize): { width: number, height: number } {
    if (size == null) { return { width: 512, height: 512 }; }
    let { width, height } = size;
    if (width != null) {
        if (height == null) { height = width; }
    } else if (height == null) {
        if (width == null) { width = height; }
    }
    if (width == null) { width = 512; }
    if (height == null) { height = 512; }

    return { width, height };
}

function searchDir(folder: string, ext: string): Array<string> {
    ext = "." + ext;

    const result: Array<string> = [ ];
    for (const filename of fs.readdirSync(folder)) {
        if (filename[0] !== "_" && filename[0] !== "." &&
          filename.endsWith(ext)) { result.push(filename); }
    }
    return result;
}

export class Project {
    readonly cmd: string;
    readonly projectFolder: string;

    constructor(cmd: string, projectFolder: string) {
        this.cmd = resolve(cmd);
        this.projectFolder = projectFolder;
    }

    _searchDir(ext: string): Array<string> {
        return searchDir(this.projectFolder, ext);
    }

    get projectName(): string {
        const filename = this.projectFilename;
        return filename.substring(0, filename.length - ".kicad_pro".length);
    }

    get projectFilename(): string {
        const files = this._searchDir("kicad_pro");

        if (files.length === 0 ) {
            throw new Error(`No project file found in ${ JSON.stringify(this.projectFolder)}`);
        }

        if (files.length > 1) {
            throw new Error(`Too many project files found in ${ JSON.stringify(this.projectFolder)}`);
        }

        return join(this.projectFolder, files[0]);;
    }

    get pcbFilename(): string {
        const files = this._searchDir("kicad_pcb");

        if (files.length === 0 ) {
            throw new Error(`No PCB file found in ${ JSON.stringify(this.projectFolder)}`);
        }

        if (files.length > 1) {
            throw new Error(`Too many PCB files found in ${ JSON.stringify(this.projectFolder)}`);
        }

        return join(this.projectFolder, files[0]);;

    }

    get schFilename(): string {
        const filename = `${ this.projectName }.kicad_sch`;

        if (this._searchDir("kicad_sch").indexOf(filename) >= 0) {
            return filename;
        }

        throw new Error("could not find root schematic?");
    }

    mv(src: string, dst: string): void {
        src = join(this.projectFolder, src);
        dst = join(this.projectFolder, dst);
        fs.renameSync(src, dst);
    }


    plot(folder: string, options?: PlotOptions): void {
        if (options == null) { options = { }; }

        console.log("GENERATING:", folder);

        const args: Array<string> = [
            "sch", "export", "svg", "--output", ".ducki-tmp"
        ];

        args.push(this.schFilename);

        fs.mkdirSync(folder, { recursive: true });

        const result = run(this.cmd, args);

        for (const line of result.stdout.split("\n")) {
            const prefix = "Plotted to '";
            const suffix = "'.";
            if (!line.startsWith(prefix)) { continue; }

            const filename = line.substring(prefix.length, line.length - suffix.length);

            // Get the real filename (generally more concise and unique)
            const data = fs.readFileSync(filename).toString();
            const match = data.match(/<desc>File: (.*?).kicad_sch<\/desc>/i);
            if (!match) { throw new Error(`missing desc`); }
            let name = match[1];
            name = (name === this.projectName) ? "sch.svg": (`sch-${ name }.svg`);

            console.log("GENERATING:", join(folder, name));

            fs.renameSync(filename, join(this.projectFolder, folder, name));
        }
    }

    _render(filename: string, options: RenderOptions): void {
        filename = _resolve(this.projectFolder, filename);
        if (!filename.endsWith(".png") && !filename.endsWith(".jpg")) {
            throw new Error("Invalid image filename ${ JSON.stringify(filename) }");
        }

        const args: Array<string> = [
            "pcb", "render", "--output", filename
        ];

        if (options.perspective) { args.push("--perspective"); }
        if (options.zoom) { args.push("--zoom", String(options.zoom)); }
//        if (options.pivot) { args.push("--pivot", flatXYZ(options.pivot)); }
        if (options.rotate) { args.push("--rotate", flatXYZ(options.rotate)); }
//        if (options.pan) { args.push("--pan", flatXYZ(options.pan)); }

        // @TODO: Why does Kicad always render images a bit pixels shy?

        const size = normalizeSize(options.size);
        args.push("--width", String(size.width));
        args.push("--height", String(size.height));

        args.push(this.pcbFilename);

        fs.mkdirSync(dirname(filename), { recursive: true });

        const result = run(this.cmd, args);

        if (result.stdout.indexOf("Successfully created 3D render image") === -1) {
            console.log({
                args,
                status: result.status,
                stdout: result.stdout,
                stderr: result.stderr
            });
            throw new Error("Failed to render PCB");
        }
    }

    render(filename: string, options?: RenderOptions): void {
        console.log("GENERATING:", filename);
        return this._render(filename, options ?? { })
    }

    flyaround(filename: string, options?: FlyAroundOptions): void {
        if (options == null) { options = { }; }
        console.log("GENERATING:", filename);
        const perspective = options.perspective ?? false;
        const zoom = options.zoom ?? 0.8;

        filename = _resolve(this.projectFolder, filename);

        const dr = 5;

        const size = normalizeSize(options.size);

        const tmp = `.ducki-tmp/temp-${ randomBytes(16).toString("hex") }.png`;

        const getImage = (rotate: XYZ) => {
            this._render(tmp, { perspective, rotate, size, zoom });

            const data = fs.readFileSync(tmp);
            const img = PNG.sync.read(data);
            const bg = [ 0xb1, 0xb1, 0xc4 ]; // @TODO: expose in CLI
            for (let i = 0; i < img.data.length; i += 4) {
                const a = img.data[i + 3] / 255;
                if (a == 0) {
                    img.data[i + 0] = bg[0];
                    img.data[i + 1] = bg[1];
                    img.data[i + 2] = bg[2];
                } else if (a < 1) {
                    img.data[i + 0] = Math.trunc((a * img.data[i + 0]) + ((1 - a) * bg[0]));
                    img.data[i + 1] = Math.trunc((a * img.data[i + 1]) + ((1 - a) * bg[1]));
                    img.data[i + 2] = Math.trunc((a * img.data[i + 2]) + ((1 - a) * bg[2]));
                }
            }
            return img;
        };

        const rot = Object.assign({ x: 0, y: 0, z: 0 }, options.rotate);
        const rots = generateSpinFrames(rot, 360, 360 / dr);

        const img = getImage(rots[0]);

        const encoder = new GIFEncoder(img.width, img.height, 'neuquant', true);
        encoder.start();
        encoder.setRepeat(0);
        encoder.setDelay(35);
        encoder.setQuality(10);
        encoder.addFrame(img.data);

        for (let i = 1; i < rots.length; i++) {
            encoder.addFrame(getImage(rots[i]).data);
        }

        encoder.finish();

        fs.writeFileSync(filename, encoder.out.getData());
    }
}
