import {promisified} from 'tauri/api/tauri';
import {listen} from 'tauri/api/event';
import {Component, Vue} from 'vue-property-decorator';
import type {GenericDevices} from 'n-link-core/components/devices';


export type DevId = { address: number; busNumber: number };

function devToString(dev: DevId) {
    return `${dev.busNumber}:${dev.address}`;
}

function stringToDev(dev: string): DevId {
    const parts = dev.split(':');
    // eslint-disable-next-line
    return {busNumber: Number.parseInt(parts[0]), address: Number.parseInt(parts[1])};
}

export type Version = { major: number; minor: number; patch: number; build: number };
export type Lcd = { width: number; height: number; bpp: number; sample_mode: number };
export type HardwareType =
    | "Cas"
    | "NonCas"
    | "CasCx"
    | "NonCasCx"
    | { Unknown: number };
// The current state of the calculator.
export type RunLevel =
    | "Recovery"
    | "Os"
    | { Unknown: number };
export type Battery =
    | "Powered"
    | "Low"
    | "Ok"
    | { Unknown: number };
export type Info = { free_storage: number; total_storage: number; free_ram: number; total_ram: number; version: Version; boot1_version: Version; boot2_version: Version; hw_type: HardwareType; clock_speed: number; lcd: Lcd; os_extension: string; file_extension: string; name: string; id: string; run_level: RunLevel; battery: Battery; is_charging: boolean };

export type FileInfo = { path: string; isDir: boolean; date: number; size: number };

export type Progress = { remaining: number; total: number };

export type PartialCmd = { action: 'download'; path: [string, number]; dest: string }
    | { action: 'upload'; path: string; src: string }
    | { action: 'uploadOs'; src: string }
    | { action: 'deleteFile'; path: string }
    | { action: 'deleteDir'; path: string }
    | { action: 'createDir'; path: string }
    | { action: 'move'; src: string; dest: string }
    | { action: 'copy'; src: string; dest: string };

export type Cmd = { id: number } & PartialCmd;

export type Device = { name: string; isCxIi: boolean; needsDrivers: boolean; info?: Info; progress?: Progress; queue?: Cmd[]; running?: boolean };

async function downloadFile(dev: DevId | string, path: [string, number], dest: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'downloadFile', path, dest});
}

async function uploadFile(dev: DevId | string, path: string, src: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'uploadFile', path, src});
}

async function uploadOs(dev: DevId | string, src: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'uploadOs', src});
}

async function deleteFile(dev: DevId | string, path: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'deleteFile', path});
}

async function deleteDir(dev: DevId | string, path: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'deleteDir', path});
}

async function createDir(dev: DevId | string, path: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'createNspireDir', path});
}

async function move(dev: DevId | string, src: string, dest: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'move', src, dest});
}

async function copy(dev: DevId | string, src: string, dest: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    await promisified({...dev, cmd: 'copy', src, dest});
}

async function listDir(dev: DevId | string, path: string) {
    if (typeof dev === 'string') dev = stringToDev(dev);
    return await promisified({...dev, cmd: 'listDir', path}) as FileInfo[];
}

async function listAll(dev: DevId | string, path: FileInfo): Promise<FileInfo[]> {
    if (!path.isDir) return [path];
    try {
        const contents = await listDir(dev, path.path);
        const parts: FileInfo[] = [];
        for (const file of contents) {
            parts.push(...(await listAll(dev, {...file, path: `${path.path}/${file.path}`})));
        }
        parts.push(path);
        return parts;
    } catch (e) {
        console.error(path, e);
        return [];
    }
}

let queueId = 0;

@Component
class Devices extends Vue implements GenericDevices {
    devices: Record<string, Device> = {};
    enumerating = false;
    hasEnumerated = false;

    created() {
        this.enumerate().then(() => this.hasEnumerated = true, console.error);
        listen('addDevice', dev => {
            const payload = dev.payload as Device & DevId;
            const str = devToString(payload);
            const existing = this.devices[str] || {};
            this.$set(this.devices, str, {...existing, ...payload});
        });
        listen('removeDevice', dev => {
            this.$delete(this.devices, devToString(dev.payload as DevId));
        });
        listen('progress', dev => {
            const payload = dev.payload as Progress & DevId;
            const str = devToString(payload);
            this.$set(this.devices[str], 'progress', payload);
        });
    }

    async runQueue(dev: DevId | string) {
        if (typeof dev !== 'string') dev = devToString(dev);
        const device = this.devices[dev];
        if (!device?.queue || device.running) return;
        this.$set(device, 'running', true);
        // eslint-disable-next-line no-constant-condition
        while (true) {
            // The device has been removed
            if (!this.devices[dev]) return;

            const cmd = device.queue[0];
            if (!cmd) {
                device.running = false;
                return;
            }
            try {
                if (cmd.action === 'download') {
                    await downloadFile(dev, cmd.path, cmd.dest);
                } else if (cmd.action === 'upload') {
                    await uploadFile(dev, cmd.path, cmd.src);
                } else if (cmd.action === 'uploadOs') {
                    await uploadOs(dev, cmd.src);
                } else if (cmd.action === 'deleteFile') {
                    await deleteFile(dev, cmd.path);
                } else if (cmd.action === 'deleteDir') {
                    await deleteDir(dev, cmd.path);
                } else if (cmd.action === 'createDir') {
                    await createDir(dev, cmd.path);
                } else if (cmd.action === 'move') {
                    await move(dev, cmd.src, cmd.dest);
                } else if (cmd.action === 'copy') {
                    await copy(dev, cmd.src, cmd.dest);
                }
            } catch (e) {
                console.error(e);
            }
            if ('progress' in device) this.$delete(device, 'progress');
            device.queue.shift();
            await this.update(dev);
        }
    }

    private addToQueue(dev: string, ...cmds: PartialCmd[]) {
        const device = this.devices[dev];
        if (!device) return;
        if (!device.queue) {
            this.$set(device, 'queue', []);
        }
        device.queue?.push(...cmds.map(cmd => ({...cmd, id: queueId++} as Cmd)));
        this.runQueue(dev);
    }

    async enumerate() {
        this.enumerating = true;
        try {
            for (const dev of await promisified({cmd: 'enumerate'}) as (Device & DevId)[]) {
                this.$set(this.devices, devToString(dev as DevId), dev);
            }
        } finally {
            this.enumerating = false;
        }
    }

    async open(dev: DevId | string) {
        if (typeof dev === 'string') dev = stringToDev(dev);
        const info = await promisified({...dev, cmd: 'openDevice'});
        this.$set(this.devices[devToString(dev)], 'info', info);
    }

    async close(dev: DevId | string) {
        if (typeof dev === 'string') dev = stringToDev(dev);
        await promisified({...dev, cmd: 'closeDevice'});
        this.$delete(this.devices[devToString(dev)], 'info');
    }

    async update(dev: DevId | string) {
        if (typeof dev === 'string') dev = stringToDev(dev);
        const info = await promisified({...dev, cmd: 'updateDevice'});
        this.$set(this.devices[devToString(dev)], 'info', info);
    }

    async listDir(dev: DevId | string, path: string) {
        return await listDir(dev, path);
    }

    async promptUploadFiles(dev: DevId | string, path: string) {
        if (typeof dev !== 'string') dev = devToString(dev);
        const files = await promisified({cmd: 'selectFiles', filter: ['tns']}) as string[];
        for (const src of files) {
            this.addToQueue(dev, {action: 'upload', path, src});
        }
    }

    async uploadOs(dev: DevId | string, filter: string) {
        if (typeof dev !== 'string') dev = devToString(dev);
        const src = await promisified({cmd: 'selectFile', filter: [filter]}) as string | null;
        if (!src) return;
        this.addToQueue(dev, {action: 'uploadOs', src});
    }

    async downloadFiles(dev: DevId | string, files: [string, number][]) {
        if (typeof dev !== 'string') dev = devToString(dev);
        const dest = await promisified({cmd: 'selectFolder'}) as string | null;
        if (!dest) return;
        for (const path of files) {
            this.addToQueue(dev, {action: 'download', path, dest});
        }
    }

    async delete(dev: DevId | string, files: FileInfo[]) {
        if (typeof dev !== 'string') dev = devToString(dev);
        const toDelete: FileInfo[] = [];
        for (const file of files) {
            toDelete.push(...await listAll(dev, file));
        }
        for (const file of toDelete) {
            this.addToQueue(dev, {action: file.isDir ? 'deleteDir' : 'deleteFile', path: file.path});
        }
    }

    async createDir(dev: DevId | string, path: string) {
        if (typeof dev !== 'string') dev = devToString(dev);
        this.addToQueue(dev, {action: 'createDir', path});
    }

    async copy(dev: DevId | string, src: string, dest: string) {
        if (typeof dev !== 'string') dev = devToString(dev);
        this.addToQueue(dev, {action: 'copy', src, dest});
    }

    async move(dev: DevId | string, src: string, dest: string) {
        if (typeof dev !== 'string') dev = devToString(dev);
        this.addToQueue(dev, {action: 'move', src, dest});
    }

    uploadFiles(): Promise<void> {
        return Promise.resolve(undefined);
    }

    uploadOsFile(): Promise<void> {
        return Promise.resolve(undefined);
    }
}


const devices = new Devices();
export default devices;
Vue.prototype.$devices = devices;
