import Disk2 from 'js/cards/disk2';
import SmartPort from 'js/cards/smartport';
import Debugger from 'js/debugger';
import {
    BlockFormat,
    BLOCK_FORMATS,
    DISK_FORMATS,
    DriveNumber,
    FloppyFormat,
    FLOPPY_FORMATS,
    JSONDisk,
    MassStorage,
} from 'js/formats/types';
import { includes, word } from 'js/types';
import { initGamepad } from 'js/ui/gamepad';

type ProgressCallback = (current: number, total: number) => void;

/**
 * Routine to split a legacy hash into parts for disk loading
 *
 * @returns an padded array for 1 based indexing
 */
export const getHashParts = (hash: string) => {
    const parts = hash.match(/^#([^|]*)\|?(.*)$/) || ['', '', ''];
    return ['', parts[1], parts[2]];
};

/**
 * Update the location hash to reflect the current disk state, if possible
 *
 * @param parts a padded array with values starting at index 1
 */
export const setHashParts = (parts: string[]) => {
    window.location.hash = `#${parts[1]}` + (parts[2] ? `|${parts[2]}` : '');
};

export const getNameAndExtension = (url: string) => {
    const urlParts = url.split('/');
    const file = urlParts.pop() || url;
    const fileParts = file.split('.');
    const ext = fileParts.pop()?.toLowerCase() || '[none]';
    const name = decodeURIComponent(fileParts.join('.'));

    return { name, ext };
};

export const loadLocalFile = (
    storage: MassStorage<FloppyFormat|BlockFormat>,
    formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
    driveNo: DriveNumber,
    file: File,
) => {
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = function () {
            const result = this.result as ArrayBuffer;
            const { name, ext } = getNameAndExtension(file.name);
            if (includes(formats, ext)) {
                initGamepad();
                if (storage.setBinary(driveNo, name, ext, result)) {
                    resolve(true);
                } else {
                    reject(`Unable to load ${name}`);
                }
            } else {
                reject(`Extension "${ext}" not recognized.`);
            }
        };
        fileReader.readAsArrayBuffer(file);
    });
};

/**
 * Local file loading routine. Allows a File object from a file
 * selection form element to be loaded.
 *
 * @param smartPort SmartPort object
 * @param driveNo Drive number
 * @param file Browser File object to load
 * @returns true if successful
 */
export const loadLocalBlockFile = (smartPort: SmartPort, driveNo: DriveNumber, file: File) => {
    return loadLocalFile(smartPort, BLOCK_FORMATS, driveNo, file);
};

/**
 * Local file loading routine. Allows a File object from a file
 * selection form element to be loaded.
 *
 * @param disk2 Disk2 object
 * @param driveNo Drive number
 * @param file Browser File object to load
 * @returns true if successful
 */
export const loadLocalNibbleFile = (disk2: Disk2, driveNo: DriveNumber, file: File) => {
    return loadLocalFile(disk2, FLOPPY_FORMATS, driveNo, file);
};

/**
 * JSON loading routine, loads a JSON file at the given URL. Requires
 * proper cross domain loading headers if the URL is not on the same server
 * as the emulator.
 *
 * @param disk2 Disk2 object
 * @param driveNo Drive number
 * @param url URL, relative or absolute to JSON file
 * @returns true if successful
 */
export const loadJSON = async (
    disk2: Disk2,
    driveNo: DriveNumber,
    url: string,
) => {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error(`Error loading: ${response.statusText}`);
    }
    const data = await response.json() as JSONDisk;
    if (!includes(FLOPPY_FORMATS, data.type)) {
        throw new Error(`Type "${data.type}" not recognized.`);
    }
    disk2.setDisk(driveNo, data);
    initGamepad(data.gamepad);
};

export const loadHttpFile = async (
    url: string,
    signal?: AbortSignal,
    onProgress?: ProgressCallback,
): Promise<ArrayBuffer> => {
    const response = await fetch(url, signal ? { signal } : {});
    if (!response.ok) {
        throw new Error(`Error loading: ${response.statusText}`);
    }
    if (!response.body) {
        throw new Error('Error loading: no body');
    }
    const reader = response.body.getReader();
    const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
    let received = 0;
    const chunks: Uint8Array[] = [];

    let result = await reader.read();
    onProgress?.(1, contentLength);
    while (!result.done) {
        chunks.push(result.value);
        received += result.value.length;
        onProgress?.(received, contentLength);
        result = await reader.read();
    }

    const data = new Uint8Array(received);
    let offset = 0;
    for (const chunk of chunks) {
        data.set(chunk, offset);
        offset += chunk.length;
    }

    return data.buffer;
};

/**
 * HTTP loading routine, loads a file at the given URL. Requires
 * proper cross domain loading headers if the URL is not on the same server
 * as the emulator.
 *
 * @param smartPort SmartPort object
 * @param driveNo Drive number
 * @param url URL, relative or absolute to JSON file
 * @returns true if successful
 */
export const loadHttpBlockFile = async (
    smartPort: SmartPort,
    driveNo: DriveNumber,
    url: string,
    signal?: AbortSignal,
    onProgress?: ProgressCallback
): Promise<boolean> => {
    const { name, ext } = getNameAndExtension(url);
    if (!includes(BLOCK_FORMATS, ext)) {
        throw new Error(`Extension "${ext}" not recognized.`);
    }
    const data = await loadHttpFile(url, signal, onProgress);
    smartPort.setBinary(driveNo, name, ext, data);
    initGamepad();

    return true;
};

/**
 * HTTP loading routine, loads a file at the given URL. Requires
 * proper cross domain loading headers if the URL is not on the same server
 * as the emulator.
 *
 * @param disk2 Disk2 object
 * @param driveNo Drive number
 * @param url URL, relative or absolute to JSON file
 * @returns true if successful
 */
export const loadHttpNibbleFile = async (
    disk2: Disk2,
    driveNo: DriveNumber,
    url: string,
    signal?: AbortSignal,
    onProgress?: ProgressCallback
) => {
    if (url.endsWith('.json')) {
        return loadJSON(disk2, driveNo, url);
    }
    const { name, ext } = getNameAndExtension(url);
    if (!includes(FLOPPY_FORMATS, ext)) {
        throw new Error(`Extension "${ext}" not recognized.`);
    }
    const data = await loadHttpFile(url, signal, onProgress);
    disk2.setBinary(driveNo, name, ext, data);
    initGamepad();
    return loadHttpFile(url, signal, onProgress);
};

export const loadHttpUnknownFile = async (
    smartStorageBroker: SmartStorageBroker,
    driveNo: DriveNumber,
    url: string,
    signal?: AbortSignal,
    onProgress?: ProgressCallback,
) => {
    const data = await loadHttpFile(url, signal, onProgress);
    const { name, ext } = getNameAndExtension(url);
    smartStorageBroker.setBinary(driveNo, name, ext, data);
};

export class SmartStorageBroker implements MassStorage<unknown> {
    constructor(private disk2: Disk2, private smartPort: SmartPort) {}

    setBinary(driveNo: DriveNumber, name: string, ext: string, data: ArrayBuffer): boolean {
        if (includes(DISK_FORMATS, ext)) {
            if (data.byteLength >= 800 * 1024) {
                if (includes(BLOCK_FORMATS, ext)) {
                    this.smartPort.setBinary(driveNo, name, ext, data);
                } else {
                    throw new Error(`Unable to load "${name}"`);
                }
            } else if (includes(FLOPPY_FORMATS, ext)) {
                this.disk2.setBinary(driveNo, name, ext, data);
            } else {
                throw new Error(`Unable to load "${name}"`);
            }
        } else {
            throw new Error(`Extension "${ext}" not recognized.`);
        }
        return true;
    }

    getBinary(_drive: number) {
        return null;
    }
}

/**
 * Load binary file into memory.
 *
 * @param file File object to read into memory
 * @param address Address at which to start load
 * @param debug Debugger object
 * @returns resolves to true if successful
 */
export const loadLocalBinaryFile = (file: File, address: word, debug: Debugger) => {
    return new Promise((resolve, _reject) => {
        const fileReader = new FileReader();
        fileReader.onload = function () {
            const result = this.result as ArrayBuffer;
            const bytes = new Uint8Array(result);
            debug.setMemory(address, bytes);
            resolve(true);
        };
        fileReader.readAsArrayBuffer(file);
    });
};
