import {ESPLoader,Transport} from "https://cdn.jsdelivr.net/npm/esptool-js@0.2.1/bundle.js";
/**
 * write all messages to the console
 */
class ConsoleOutputHandler{
    clean() {
    }
    writeLine(data) {
        console.log("ESPInstaller:",data);
    }
    write(data) {
        console.log(data);
    }
}

/**
 * write messages to an instance of xterm 
 * to use this, include in your html
    <script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.min.js"></script>
	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
 * and create a div element
    <div id="terminal"/>   
 * provide the id of this div to the constructor   
 */
class XtermOutputHandler {
    constructor(termId) {
        let termElement = document.getElementById(termId);
        if (termElement) {
            this.term = new Terminal({ cols: 120, rows: 40 , convertEol: true });
            this.term.open(termElement);
        }
        this.clean=this.clean.bind(this);
        this.writeLine=this.writeLine.bind(this);
        this.write=this.write.bind(this);
    }
    clean() {
        if (!this.term) return;
        this.term.clear();
    }
    writeLine(data) {
        if (!this.term) {
            console.log("TERM:", data);
            return;
        };
        this.term.writeln(data);
    }
    write(data) {
        if (!this.term) {
            console.log("TERM:", data);
            return;
        };
        this.term.write(data)
    }
};
class ESPInstaller{
    constructor(outputHandler){
        this.espLoaderTerminal=outputHandler|| new ConsoleOutputHandler();
        this.transport=undefined;
        this.esploader=undefined;
        this.chipFamily=undefined;
        this.base=import.meta.url.replace(/[^/]*$/,"install.php");
        this.consoleDevice=undefined;
        this.consoleReader=undefined;
        this.imageChipId=undefined;
    }
    /**
     * get an URL query parameter
     * @param  key 
     * @returns 
     */
    static getParam(key){
        let value=RegExp(""+key+"[^&]+").exec(window.location.search);
        // Return the unescaped value minus everything starting from the equals sign or an empty string
        return decodeURIComponent(!!value ? value.toString().replace(/^[^=]+./,"") : "");
    };
    static checkAvailable(){
        if (! navigator.serial || ! navigator.serial.requestPort) return false;
        return true;
    }
    /**
     * execute a reset on the connected device
     */
    async resetTransport() {
        if (!this.transport) {
            throw new Error("not connected");
        }
        this.espLoaderTerminal.writeLine("Resetting...");
        await this.transport.device.setSignals({
            dataTerminalReady: false,
            requestToSend: true,
        });
        await this.transport.device.setSignals({
            dataTerminalReady: false,
            requestToSend: false,
        });
    };
    
    async disconnect(){
        if (this.consoleDevice){
            try{
                if (this.consoleReader){
                    await this.consoleReader.cancel();
                    this.consoleReader=undefined;
                }
                await this.consoleDevice.close();
            }catch(e){
                console.log(`error cancel serial read ${e}`);
            }
            this.consoleDevice=undefined;
        }
        if (this.transport){
            try{
                await this.transport.disconnect();
                await this.transport.waitForUnlock(1500);
            }catch (e){}
            this.transport=undefined;
        }
        this.esploader=undefined;
    }
    async connect() {
        this.espLoaderTerminal.clean();
        await this.disconnect();
        let device = await navigator.serial.requestPort({});
        if (!device) {
            return;
        }
        try {
            this.transport = new Transport(device);
            this.esploader = new ESPLoader(this.transport, 115200, this.espLoaderTerminal);
            let foundChip = await this.esploader.main_fn();
            if (!foundChip) {
                throw new Error("unable to read chip id");
            }
            this.espLoaderTerminal.writeLine(`chip: ${foundChip}`);
            await this.esploader.flash_id();
            this.chipFamily = this.esploader.chip.CHIP_NAME;
            this.imageChipId = this.esploader.chip.IMAGE_CHIP_ID;
            this.espLoaderTerminal.writeLine(`chipFamily: ${this.chipFamily}`);
        } catch (e) {
            this.disconnect();
            throw e;
        }
    }

    async startConsole(baud) {
        await this.disconnect();
        try {
            let device = await navigator.serial.requestPort({});
            if (!device) {
                return;
            }
            this.consoleDevice=device;
            let br=baud || 115200;
            await device.open({
                baudRate: br
            });
            this.consoleReader=device.readable.getReader();
            this.espLoaderTerminal.clean();
            this.espLoaderTerminal.writeLine(`Console at ${br}:`);
            while (this.consoleReader) {
                let {value:val,done:done} = await this.consoleReader.read();
                if (typeof val !== 'undefined') {
                    this.espLoaderTerminal.write(val);
                }
                if (done){
                    console.log("Console reader stopped");
                    break;
                }
            }
        } catch (e) { this.espLoaderTerminal.writeLine(`Error: ${e}`) }
        this.espLoaderTerminal.writeLine("Console reader stopped");
    }
    async stopConsole(){
        await this.disconnect();
    }
    
    isConnected(){
        return this.transport !== undefined;
    }
    checkConnected(){
        if (! this.isConnected){
            throw new Error("not connected");
        }
    }

    getChipFamily(){
        this.checkConnected();
        return this.chipFamily;
    }
    getChipId(){
        this.checkConnected();
        return this.imageChipId;
    }
    /**
     * flass the device
     * @param {*} fileList : an array of entries {data:blob,address:number}
     */
    async writeFlash(fileList){
        this.checkConnected();
        this.espLoaderTerminal.writeLine(`Flashing....`);
        await this.esploader.write_flash(
            fileList,
            "keep",
            "keep",
            "keep",
            false
        )
        await this.resetTransport();
        this.espLoaderTerminal.writeLine(`Done.`);
    }
    /**
     * fetch a release asset from github
     * @param {*} user 
     * @param {*} repo 
     * @param {*} version 
     * @param {*} name 
     * @returns 
     */
    async getReleaseAsset(user,repo,version,name){
        const url=this.base+"?dlName="+encodeURIComponent(name)+
            "&dlVersion="+encodeURIComponent(version)+
            "&user="+encodeURIComponent(user)+
            "&repo="+encodeURIComponent(repo);
        this.espLoaderTerminal.writeLine(`downloading image from ${url}`);    
        const resp=await fetch(url);
        if (! resp.ok){
            throw new Error(`unable to download image from ${url}: ${resp.status}`);
        }
        const reader=new FileReader();
        const blob= await resp.blob();
        let data=await new Promise((resolve)=>{
            reader.addEventListener("load",() => resolve(reader.result));
            reader.readAsBinaryString(blob);
        });
        this.espLoaderTerminal.writeLine(`successfully loaded ${data.length} bytes`);
        return data;
    }
    /**
     * handle the click of an install button
     * @param {*} isFull 
     * @param {*} user 
     * @param {*} repo 
     * @param {*} version 
     * @param {*} assetName
     * @param {*} checkChip will be called with the found chipId and the data and the isFull flag
     *            must return an info with the flashStart being set
     * @returns 
     */
    async installClicked(isFull, user, repo, version, assetName,checkChip) {
        try {
            await this.connect();
            let imageData = await this.getReleaseAsset(user, repo, version, assetName);
            if (!imageData || imageData.length == 0) {
                throw new Error(`no image data fetched`);
            }
            let info=await checkChip(this.getChipId(),imageData,isFull);
            let fileList = [
                { data: imageData, address: info.flashStart }
            ];
            let txt = isFull ? "baseImage (all data will be erased)" : "update";
            if (!confirm(`ready to install ${version}\n${txt}`)) {
                this.espLoaderTerminal.writeLine("aborted by user...");
                await this.disconnect();
                return;
            }
            await this.writeFlash(fileList);
            await this.disconnect();
        } catch (e) {
            this.espLoaderTerminal.writeLine(`Error: ${e}`);
            alert(`Error: ${e}`);
        }
    }
    /**
     * directly run the flash
     * @param {*} isFull 
     * @param {*} imageData the data to be flashed
     * @param {*} version the info shown in the dialog
     * @param {*} checkChip will be called with the found chipId and the data
     *            must return an info with flashStart
     * @returns 
     */
    async runFlash(isFull,imageData,version,checkChip){
        try {
            await this.connect();
            let info= await checkChip(this.getChipId(),imageData,isFull); //just check
            let fileList = [
                { data: imageData, address: info.flashStart }
            ];
            let txt = isFull ? "baseImage (all data will be erased)" : "update";
            if (!confirm(`ready to install ${version}\n${txt}`)) {
                this.espLoaderTerminal.writeLine("aborted by user...");
                await this.disconnect();
                return;
            }
            await this.writeFlash(fileList);
            await this.disconnect();
        } catch (e) {
            this.espLoaderTerminal.writeLine(`Error: ${e}`);
            alert(`Error: ${e}`);
        }    
    }
    /**
     * fetch the release info from the github API
     * @param {*} user 
     * @param {*} repo 
     * @returns 
     */
    async getReleaseInfo(user,repo){
        let url=this.base+"?api=1&user="+encodeURIComponent(user)+"&repo="+encodeURIComponent(repo)
        let resp=await fetch(url);
        if (! resp.ok){
            throw new Error(`unable to query release info from ${url}: ${resp.status}`);
        }
        return await resp.json();
    }
    /**
     * get the release info in a parsed form
     * @param {*} user 
     * @param {*} repo 
     * @returns an object: {version:nnn, assets:[name1,name2,...]}
     */
    async getParsedReleaseInfo(user,repo){
        let raw=await this.getReleaseInfo(user,repo);
        let rt={
            version:raw.name,
            assets:[]
        };
        if (! raw.assets) return rt;
        raw.assets.forEach((asset)=>{
            rt.assets.push(asset.name);
        })
        return rt;
    }
};
export {ConsoleOutputHandler, XtermOutputHandler};
export default ESPInstaller;