import {XtermOutputHandler} from "./installUtil.js";
import ESPInstaller from "./installUtil.js";
import { readFile, addEl, getParam, setValue, setVisible } from "./helper.js";
import * as zip from "https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.7.29/+esm";
(function(){

    const UPDATE_START=65536;

    //taken from index.js
    const HDROFFSET = 288;
    const VERSIONOFFSET = 16;
    const NAMEOFFSET = 48;
    const MINSIZE = HDROFFSET + NAMEOFFSET + 32;
    const CHIPIDOFFSET=12; //2 bytes chip id here
    const imageMagic=0xe9; //at byte 0
    const imageCheckBytes = {
        288: 0x32, //app header magic
        289: 0x54,
        290: 0xcd,
        291: 0xab
    };
    /**
     * map of our known chip ids to flash starts for full images
     * see https://github.com/espressif/esptool-js/tree/main/src/targets
     * IMAGE_CHIP_ID, BOOTLOADER_FLASH_OFFSET
     * 0 - esp32 - starts at 0x1000
     * 9 - esp32s3 - starts at 0
     */
    const FLASHSTART={
        0:0x1000, //ESP32
        9:0,      //ESP32S3
        2:0x1000, //ESP32S2
        5:0       //ESP32C3
    };
    const decodeFromBuffer=(buffer, start, length)=>{
        while (length > 0 && buffer.charCodeAt(start + length - 1) == 0) {
            length--;
        }
        if (length <= 0) return "";
        return buffer.substr(start,length);
    }
    const getChipId=(buffer)=>{
        if (buffer.length < CHIPIDOFFSET+2) return -1;
        return buffer.charCodeAt(CHIPIDOFFSET)+256*buffer.charCodeAt(CHIPIDOFFSET+1);
    }
    /**
     * 
     * @param {string} content the content to be checked
     */
    const checkImage = (content,isFull) => {
        let prfx=isFull?"full":"update";
        let startOffset=0;
        let flashStart=UPDATE_START;
        let chipId=getChipId(content);
        if (isFull){
            if (chipId < 0) throw new Error(prfx+"image: no valid chip id found");
            flashStart=FLASHSTART[chipId];
            if (flashStart === undefined) throw new Error(prfx+"image: unknown chip id "+chipId);
            startOffset=UPDATE_START-flashStart;
        }
        if (content.length < (MINSIZE+startOffset)) {
            throw new Error(prfx+"image to small, only " + content.length + " expected " + (MINSIZE+startOffset));
        }
        if (content.charCodeAt(0) != imageMagic){
            throw new Error("no image magic "+imageMagic+" at start of "+prfx+"image");
        }
        for (let idx in imageCheckBytes) {
            let cb=content.charCodeAt(parseInt(idx)+startOffset);
            if (cb != imageCheckBytes[idx]) {
                throw new Error(prfx+"image: missing magic byte at position " + idx + ", expected " +
                    imageCheckBytes[idx] + ", got " + cb);
            }
        }
        let version = decodeFromBuffer(content, startOffset+ HDROFFSET + VERSIONOFFSET, 32);
        let fwtype = decodeFromBuffer(content, startOffset+ HDROFFSET + NAMEOFFSET, 32);
        let rt = {
            fwtype: fwtype,
            version: version,
            chipId:chipId,
            flashStart: flashStart
        };
        return rt;
    }
    
    const checkImageFile=(file,isFull)=>{
        let minSize=MINSIZE+(isFull?(UPDATE_START-FULL_START):0);
        return new Promise(function (resolve, reject) {
            if (!file) reject("no file");
            if (file.size < minSize) reject("file is too small");
            let slice = file.slice(0, minSize);
            let reader = new FileReader();
            reader.addEventListener('load', function (e) {
                let content = e.target.result;
                try{
                    let rt=checkImage(content,isFull);
                    resolve(rt);
                }
                catch (e){
                    reject(e);    
                }
            });
            reader.readAsBinaryString(slice);
        });
    }
    let espLoaderTerminal;
    let espInstaller;
    let releaseData={};
    let showConsole;
    let hideConsole;
    const enableConsole=(enable,disableBoth)=>{
        if (showConsole) showConsole.disabled=!enable || disableBoth;
        if (hideConsole) hideConsole.disabled=enable || disableBoth;
    }
    const showError=(txt)=>{
        let hFrame=document.querySelector('.heading');
        if (hFrame){
            hFrame.textContent=txt;
            hFrame.classList.add("error");
        }
        else{
            alert(txt);
        }
    }
    const buildHeading=(info,element)=>{
        let hFrame=document.querySelector(element||'.heading');
        if (! hFrame) return;
        hFrame.textContent='';
        let h=addEl('h2',undefined,hFrame,`ESP32 Install ${info}`)
    }
    const checkChip= async (chipId,data,isFull)=>{
        let info=checkImage(data,isFull);
        if (info.chipId != chipId){
            let res=confirm("different chip signatures - image("+chipId+"), chip ("+info.chipId+")\nUse this image any way?");
            if (! res) throw new Error("user abort");
        }
        return info;
    }
    const baudRates=[1200,
        2400,
        4800,
        9600,
        14400,
        19200,
        28800,
        38400,
        57600,
        115200,
        230400,
        460800];
    const buildConsoleButtons=(element)=>{
        let bFrame=document.querySelector(element||'.console');
        if (! bFrame) return;
        bFrame.textContent='';
        let cLine=addEl('div','buttons',bFrame);
        let bSelect=addEl('select','consoleBaud',cLine);
        baudRates.forEach((baud)=>{
            let v=addEl('option',undefined,bSelect,baud+'');
            v.setAttribute('value',baud);
        });
        bSelect.value=115200;
        showConsole=addEl('button','showConsole',cLine,'ShowConsole');
        showConsole.addEventListener('click',async()=>{
            enableConsole(false);
            await espInstaller.startConsole(bSelect.value);
        })
        hideConsole=addEl('button','hideConsole',cLine,'HideConsole');
        hideConsole.addEventListener('click',async()=>{
            await espInstaller.stopConsole();
            enableConsole(true);
        })
    }
    const handleLocalFile= async (file)=>{
        setValue('content','');
        showLoading("loading "+file.name);
        try {
            if (file.name.match(/.zip$/)) {
                //zip
                await handleZip(file);
            }
            else {
                if (! file.name.match(/\.bin$/)){
                    throw new Error("only .zip or .bin");
                }
                let data=await readFile(file);
                let isFull=false;
                let info;
                try{
                    info=checkImage(data,true);
                    isFull=true;
                }catch (e){
                    try{
                        info=checkImage(data);
                    }
                    catch(x){
                        throw new Error(file.name+" is no image: "+x);
                    }
                }
                if (isFull){
                    buildCustomButtons("dummy",undefined,data,file.name,"Local");
                }
                else{
                    buildCustomButtons("dummy",data,undefined,file.name,"Local");
                }
            }
        } catch (e) {
            alert(e);
        }
        showLoading();
    }
    const buildUploadButtons= (element)=>{
        let bFrame=document.querySelector(element||'.upload');
        if (! bFrame) return;
        bFrame.textContent='';
        let it=addEl('div','item',bFrame);
        addEl('div','version',it,`Local File`);
        let cLine=addEl('div','buttons',it);
        let fi=addEl('input','uploadFile',cLine);
        fi.setAttribute('type','file');
        fi.addEventListener('change',async (ev)=>{
            let files=ev.target.files;
            if (files.length < 1) return;
            await handleLocalFile(files[0]);
        });
        let bt=addEl('button','uploadButton',cLine,'upload');
        bt.addEventListener('click',()=>{
            fi.click();
        });
    }
    const buildButtons=(user,repo,element)=>{
        let bFrame=document.querySelector(element||'.content');
        if (! bFrame) return;
        bFrame.textContent='';
        if (!releaseData.assets) return;
        let version=releaseData.name;
        if (! version){
            alert("no version found in release data");
            return;
        }
        addEl('div','version',bFrame,`Prebuild: ${version}`);
        let items={};
        releaseData.assets.forEach((asset)=>{
            let name=asset.name;
            let base=name.replace(/-all\.bin/,'').replace(/-update\.bin/,'');
            if (items[base] === undefined){
                items[base]={};
            }
            let item=items[base];
            item.label=base.replace(/-[0-9][0-9]*/,'');
            if (name.match(/-update\./)){
                item.update=name;
            }
            else{
                item.basic=name;
            }
        });
        for (let k in items){
            let item=items[k];
            let line=addEl('div','item',bFrame);
            addEl('div','itemTitle',line,item.label);
            let btLine=addEl('div','buttons',line);
            let tb=addEl('button','installButton',btLine,'Initial');
            tb.addEventListener('click',async ()=>{
                enableConsole(false,true);
                await espInstaller.installClicked(
                    true,
                    user,
                    repo,
                    version,
                    item.basic,
                    checkChip
                )
                enableConsole(true);
            });
            tb=addEl('button','installButton',btLine,'Update');
            tb.addEventListener('click',async ()=>{
                enableConsole(false,true);
                await espInstaller.installClicked(
                    false,
                    user,
                    repo,
                    version,
                    item.update,
                    checkChip
                )
                enableConsole(true);
            });
        }

    }
    const buildCustomButtons = (name, updateData, fullData,info,title,element) => {
        let bFrame = document.querySelector(element || '.content');
        if (!bFrame) return;
        if (fullData === undefined && updateData === undefined) return;
        let version;
        let vinfo;
        let uinfo;
        if (fullData !== undefined){
            vinfo=checkImage(fullData,true);
            version=vinfo.version;
        }
        if (updateData !== undefined){
            uinfo=checkImage(updateData);
            if (version !== undefined){
                if (uinfo.version != version){
                    throw new Error("different versions in full("+version+") and update("+uinfo.version+") image");
                }
            }
            else{
                version=uinfo.version;
            }
        }
        bFrame.textContent = '';
        let item=addEl('div','item',bFrame);
        addEl('div', 'version', item, title+" "+version);
        if (info){
            addEl('div','version',item,info);
        }
        let btLine = addEl('div', 'buttons', item);
        let tb;
        if (fullData !== undefined) {
            tb = addEl('button', 'installButton', btLine, 'Initial');
            tb.addEventListener('click', async () => {
                enableConsole(false, true);
                await espInstaller.runFlash(
                    true,
                    fullData,
                    version,
                    checkChip
                )
                enableConsole(true);
            });
        }
        if (updateData !== undefined) {
            tb = addEl('button', 'installButton', btLine, 'Update');
            tb.addEventListener('click', async () => {
                enableConsole(false, true);
                await espInstaller.runFlash(
                    false,
                    updateData,
                    version,
                    checkChip
                )
                enableConsole(true);
            });
        }
    }
    const showLoading=(title)=>{
        setVisible('loadingFrame',title !== undefined);
        if (title){
            setValue('loadingText',title);
        }
    };
    class BinaryStringWriter extends zip.Writer {

        constructor() {
          super();
          this.binaryString = "";
        }
      
        writeUint8Array(array) {
          for (let indexCharacter = 0; indexCharacter < array.length; indexCharacter++) {
            this.binaryString += String.fromCharCode(array[indexCharacter]);
          }
        }
      
        getData() {
          return this.binaryString;
        }
      }
    const handleZip = async (zipFile) => {
        showLoading("loading zip");
        let reader;
        let title="Custom";
        if (typeof(zipFile) === 'string'){
            setValue('loadingText', 'downloading custom build')
            reader= new zip.HttpReader(zipFile);
        }
        else{
            setValue('loadingText', 'loading zip file');
            reader = new zip.BlobReader(zipFile);
            title="Local";
        }
        let zipReader = new zip.ZipReader(reader);
        const entries = (await zipReader.getEntries());
        let fullData;
        let updateData;
        let base = "";
        let environment;
        let buildflags;
        for (let i = 0; i < entries.length; i++) {
            if (entries[i].filename.match(/-all.bin$/)) {
                fullData = await (entries[i].getData(new BinaryStringWriter()));
                base = entries[i].filename.replace("-all.bin", "");
            }
            if (entries[i].filename.match(/-update.bin$/)) {
                updateData = await (entries[i].getData(new BinaryStringWriter()));
                base = entries[i].filename.replace("-update.bin", "");
            }
            if (entries[i].filename === 'buildconfig.txt') {
                let txt = await (entries[i].getData(new zip.TextWriter()));
                environment = txt.replace(/.*pio run *.e */, '').replace(/ .*/, '');
                buildflags = txt.replace(/.*PLATFORMIO_BUILD_FLAGS="/, '').replace(/".*/, '');
            }
        }
        let info;
        if (environment !== undefined && buildflags !== undefined) {
            info = `env=${environment}, flags=${buildflags}`;
        }
        if (updateData === undefined && fullData === undefined){
            throw new Error("no firmware files found in zip");
        }
        buildCustomButtons("dummy", updateData, fullData, info,title);
        showLoading();
    }  
    window.onload = async () => {
        if (! ESPInstaller.checkAvailable()){
            showError("your browser does not support the ESP flashing (no serial)");
            return;
        }
        let custom=getParam('custom');
        let user;
        let repo;
        let errorText=`unable to query release info for user ${user}, repo ${repo}: `;
        if (! custom){
            user = window.gitHubUser||getParam('user','wellenvogel');
            repo = window.gitHubRepo || getParam('repo','esp32-nmea2000');
            if (!user || !repo) {
                alert("missing parameter user or repo");
            }
        }
        try {
            espLoaderTerminal = new XtermOutputHandler('terminal');
            espInstaller = new ESPInstaller(espLoaderTerminal);
            buildConsoleButtons();
            buildUploadButtons();
            if (! custom){
                buildHeading(`${user}:${repo}`);
                releaseData = await espInstaller.getReleaseInfo(user, repo);
                buildButtons(user, repo);
                showLoading();
            }
            else{
                errorText="unable to download custom build";
                await handleZip(custom);
            }
        } catch(error){
            showLoading();
            alert(errorText+error)
        };
    }
})();