/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */

"use strict";

// dep: geo.base
// dep: data.local
// dep: kiri.consts
// dep: kiri.api
// dep: kiri.main
gapp.register("kiri.export", [], (root, exports) => {

const { base, data, kiri } = root;
const { api, consts } = kiri;
const { local } = data;
const { util } = base;
const { stats, ui } = api;
const { MODES } = consts;

kiri.export = exportFile;

let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1;

function localGet(key) {
    return api.local.get(key);
}

function localSet(key, val) {
    return api.local.set(key, val);
}

function exportFile(options) {
    let mode = api.mode.get();
    let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined)
        .filter(v => v)
        .map(v => v.replace(/ /g,'-'))
        .map(v => v.substring(0,20))
        .map(v => v.split('.')[0]);
    api.event.emit('export', mode);
    switch (mode) {
        case 'WEDM':
        case 'WJET':
        case 'DRAG':
        case 'LASER': return callExportLaser(options, names);
        case 'FDM': return callExport(options, mode, names);
        case 'CAM': return callExport(options, mode, names);
        case 'SLA': return callExportSLA(options, names);
    }
}

function callExport(callback, mode, names) {
    let alert = api.feature.work_alerts ? api.show.alert("Exporting") : null;
    let gcode = [];
    let section = [];
    let sections = { };
    kiri.client.export(api.conf.get(), (line) => {
        if (typeof line !== 'string') {
            if (line.section) {
                sections[line.section] = section = [];
            }
            return;
        }
        gcode.push(line);
        section.push(line);
    }, (output, error) => {
        api.hide.alert(alert);
        if (error) {
            api.show.alert(error, 5);
        } else if (callback) {
            callback(gcode.join('\r\n'), output);
        } else {
            exportGCodeDialog(gcode, mode === 'CAM' ? sections : undefined, output, names);
        }
    });
}

function callExportLaser(options, names) {
    kiri.client.export(api.conf.get(), (line) => {
        console.log({unexpected_line: line});
    }, (output, error) => {
        if (error) {
            api.show.alert(error, 5);
        } else {
            exportLaserDialog(output, names);
        }
    });
}

function callExportSLA(options, names) {
    kiri.client.export(api.conf.get(), (line) => {
        api.show.progress(line.progress, "exporting");
    }, (output, error) => {
        api.show.progress(0);
        if (error) {
            api.show.alert(error, 5);
        } else {
            kiri.driver.SLA.printDownload(output, api, names);
        }
    });
}

function exportLaserDialog(data, names) {
    localSet('kiri-print-seq', printSeq++);

    const fileroot = names[0] || "laser";
    const filename = `${fileroot}-${(printSeq.toString().padStart(3,"0"))}`;
    const settings = api.conf.get();
    const driver = kiri.driver.LASER;

    function download_svg() {
        api.util.download(
            driver.exportSVG(settings, data),
            $('print-filename').value + ".svg"
        );
    }

    function download_dxf() {
        api.util.download(
            driver.exportDXF(settings, data),
            $('print-filename').value + ".dxf"
        );
    }

    function download_gcode() {
        api.util.download(
            driver.exportGCode(settings, data),
            $('print-filename').value + ".gcode"
        );
    }

    api.ajax("/kiri/output-laser.html", function(html) {
        let segments = 0;
        data.forEach(layer => { segments += layer.length });
        ui.print.innerHTML = html;
        $('print-filename').value = filename;
        $('print-lines').value = util.comma(segments);
        $('print-svg').onclick = download_svg;
        $('print-dxf').onclick = download_dxf;
        $('print-lg').onclick = download_gcode;
        api.modal.show('print');
    });
}

function bindField(field, varname) {
    $(field).onblur = function() {
        console.log('save', field, 'to', varname);
        localSet(varname, $(field).value.trim());
    };
}

function exportGCodeDialog(gcode, sections, info, names) {
    localSet('kiri-print-seq', printSeq++);

    let settings = api.conf.get(),
        MODE = api.mode.get_id(),
        name = names[0] || (MODE === MODES.CAM ? "cnc" : "print"),
        pre = `${name}-${(printSeq.toString().padStart(3,"0"))}`,
        filename = pre,// + (new Date().getTime().toString(36)),
        fileext = settings.device.gcodeFExt || "gcode",
        codeproc = settings.device.gcodeProc,
        octo_host,
        octo_apik,
        grid_host,
        grid_apik,
        grid_target,
        grid_targets = {},
        grid_local,
        grid_uuid;

    // join gcode array into a string
    gcode = gcode.join('\r\n') + '\r\n';

    // run gcode post processor function (when supplied and valid)
    if (codeproc && self[codeproc]) {
        gcode = self[codeproc](gcode);
    }

    function getBlob() {
        return new Blob(
            [gcode],
            {type:"application/octet-stream"});
    }

    function sendto_octoprint() {
        if (!(octo_host && octo_apik)) return;

        let form = new FormData(),
            ajax = new XMLHttpRequest(),
            host = octo_host.value.toLowerCase(),
            apik = octo_apik.value;

        if (host.indexOf("http") !== 0) {
            api.show.alert("host missing protocol (http:// or https://)");
            return;
        }
        if (api.const.SECURE && !api.util.isSecure(host)) {
            api.show.alert("host must begin with 'https' on a secure site");
            return;
        }

        localSet('octo-host', host.trim());
        localSet('octo-apik', apik.trim());

        filename = $('print-filename').value;
        form.append("file", getBlob(), filename+"."+fileext);
        ajax.onreadystatechange = function() {
            if (ajax.readyState === 4) {
                let status = ajax.status;
                stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`);
                if (status >= 200 && status < 300) {
                    api.modal.hide();
                } else {
                    api.show.alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText);
                }
                api.show.progress(0);
            }
            api.show.progress(0);
        };
        ajax.upload.addEventListener('progress', function(evt) {
            api.show.progress(evt.loaded/evt.total, "sending");
        });
        ajax.open("POST", host+"/api/files/local");
        if (apik) {
            ajax.setRequestHeader("X-Api-Key", apik);
        }
        ajax.send(form);
    }

    function gridhost_tracker(host,key) {
        ajax(host+"/api/check?key="+key, function(data) {
            data = js2o(data);
            console.log(data);
            if (!(data.done || data.error)) {
                setTimeout(function() { gridhost_tracker(host,key) }, 1000);
            }
        });
    }

    function gridlocal_probe(ev, devs) {
        if (ev && ev.code !== 'Enter') return;

        if (!devs && api.probe.local(gridlocal_probe)) return;

        grid_local = devs;

        let gdev = localGet('grid-local');
        let gloc = $('grid-local');
        let html = [];
        for (let uuid in devs) {
            gdev = gdev || uuid;
            let dev = devs[uuid];
            let sel = uuid === gdev ? ' selected' : '';
            html.push(`<option id="gl-${uuid}" value="${uuid}" ${sel}>${dev.stat.device.name}</option>`);
        }

        if (html.length) {
            gloc.innerHTML = html.join('\n');
            gloc.onchange = (ev) => {
                localSet('grid-local', gloc.options[gloc.selectedIndex].value);
            };
            $('send-to-gridhead').style.display = 'flex';
            $('send-to-gridspool').style.display = 'flex';
        } else {
            $('send-to-gridhead').style.display = '';
            $('send-to-gridspool').style.display = '';
        }
    }

    function sendto_gridlocal() {
        let uuid = $('grid-local').value;
        let dev = grid_local[uuid];
        if (dev) {
            let file = $('print-filename').value;
            fetch(
                `${api.probe.live}/api/grid_send?uuid=${uuid}&file=${encodeURIComponent(file + "." + fileext)}`,
                {method: "POST", body: gcode}
            )
            .then(t => t.text())
            .then(t => {
                stats.add(`ua_${api.mode.get_lower()}_print_local_ok`);
                console.log({grid_spool_said: t});
            })
            .catch(e => {
                stats.add(`ua_${api.mode.get_lower()}_print_local_err`);
                console.log({grid_local_spool_error: e});
            })
            .finally(() => {
                api.modal.hide();
            });
        }
    }

    function admin_gridlocal() {
        let dev = grid_local[$('grid-local').value];
        if (dev && dev.stat && dev.stat.device) {
            let dsd = dev.stat.device;
            window.open(`http://${dsd.addr[0]}:${dsd.port || 4080}`, "_grid_admin");
        }
    }

    function gridhost_probe(ev, set_host) {
        if (ev && ev.code !== 'Enter') return;
        if (!(grid_host && grid_apik)) return;

        if (set_host) grid_host.value = set_host;

        let xhtr = new XMLHttpRequest(),
            host = grid_host.value,
            apik = grid_apik.value,
            target = grid_target.value;

        if (!apik) $('gpapik').style.display = 'none';

        if (!host && api.probe.grid(gridhost_probe)) return;

        if (!host) return;

        xhtr.onreadystatechange = function() {
            if (xhtr.readyState === 4) {
                if (xhtr.status >= 200 && xhtr.status < 300) {
                    localSet('grid-host', host);
                    localSet('grid-apik', apik);
                    let res = JSON.parse(xhtr.responseText);
                    let sel = false;
                    let match = false;
                    let first = null;
                    let html = [];
                    grid_targets = {};
                    for (let key in res) {
                        first = first || key;
                        if (!localGet('grid-target')) {
                            localSet('grid-target', key);
                            sel = true;
                        } else {
                            sel = localGet('grid-target') === key;
                        }
                        match = match || sel;
                        grid_targets[html.length] = key;
                        html.push(
                            "<option id='gpo-'" + key + " value='" +key + "'" +
                            (sel ? " selected" : "") +
                            ">" +
                            (res[key].comment || key) +
                            "</option>"
                        );
                    }
                    if (!match) {
                        localSet('grid-target', first);
                    }
                    grid_target.innerHTML = html.join('\n');
                } else if (xhtr.status === 401) {
                    $('gpapik').style.display = '';
                } else {
                    local.removeItem('grid-host');
                    local.removeItem('grid-apik');
                    console.log("invalid grid:host url");
                }
            }
        };

        xhtr.open("GET", host + "/api/active?key=" + apik);
        xhtr.send();
    }

    function sendto_gridhost() {
        if (!(grid_host && grid_apik)) return;

        let xhtr = new XMLHttpRequest(),
            host = grid_host.value,
            apik = grid_apik.value,
            target = localGet('grid-target') || '';

        if (target === '') {
            api.show.alert('invalid or missing target');
            return;
        }
        if (host.indexOf("http") !== 0) {
            api.show.alert("host missing protocol (http:// or https://)");
            return;
        }
        if (host.indexOf("://") < 0) {
            api.show.alert("host:port malformed");
            return;
        }
        if (api.const.SECURE && !api.util.isSecure(host)) {
            api.show.alert("host must begin with 'https' on a secure site");
            return;
        }

        localSet('grid-host', host.trim());
        localSet('grid-apik', apik.trim());

        xhtr.onreadystatechange = function() {
            if (xhtr.readyState === 4) {
                let status = xhtr.status;
                stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`);
                if (status >= 200 && status < 300) {
                    let json = js2o(xhtr.responseText);
                    gridhost_tracker(host,json.key);
                    api.ajax(host+"/api/wait?key="+json.key, function(data) {
                        data = js2o(data);
                        console.log(data);
                        api.show.alert("print to "+target+": "+data.status, 600);
                    });
                } else {
                    api.show.alert("grid:host error\nstatus: "+status+"\nmessage: "+xhtr.responseText, 10000);
                }
                api.show.progress(0);
            }
        };
        xhtr.upload.addEventListener('progress', function(evt) {
            api.show.progress(evt.loaded/evt.total, "sending");
        });
        filename = $('print-filename').value;
        xhtr.open("POST",
            host + "/api/print?" +
            "filename=" + filename +
            "&target=" + target +
            "&key=" + apik +
            "&time=" + Math.round(info.time) +
            "&length=" + Math.round(info.distance) +
            "&image=" + filename
        );
        xhtr.setRequestHeader("Content-Type", "text/plain");
        let snapshot = api.view.snapshot;
        xhtr.send(snapshot ? [gcode,snapshot].join("\0") : gcode);
        api.modal.hide();
    }

    function download() {
        filename = $('print-filename').value;
        api.util.download(gcode, filename + "." + fileext);
        // saveAs(getBlob(), filename + "." + fileext);
    }

    function pad(v) {
        v = v.toString();
        return v.length < 2 ? '0' + v : v;
    }

    function calcWeight() {
        try {
        let density = $('print-density');
        $('print-weight').value = (
            (Math.PI * util.sqr(
                info.settings.device.extruders[0].extFilament / 2
            )) *
            info.distance *
            (parseFloat(density.value) || 1.25) /
            1000
        ).toFixed(2);
        density.onkeyup = (ev) => {
            if (ev.key === 'Enter') calcWeight();
        };
        } catch (e) { }
    }

    function calcTime() {
        let floor = Math.floor,
            time = floor(info.time),
            hours = floor(time / 3600),
            newtime = time - hours * 3600,
            mins = floor(newtime / 60),
            secs = newtime - mins * 60;

        $('output-time').value = [pad(hours),pad(mins),pad(secs)].join(':');
    }

    fetch("/kiri/output-gcode.html").then(r => r.text()).then(html => {
        ui.print.innerHTML = html;
        let set = api.conf.get();
        let fdm = MODE === MODES.FDM;
        let octo = set.controller.exportOcto && MODE !== MODES.CAM;
        let ghost = set.controller.exportGhost;
        let local = set.controller.exportLocal;
        let preview = set.controller.exportPreview;
        $('code-preview-head').style.display = preview ? '' : 'none';
        $('code-preview').style.display = preview ? '' : 'none';
        $('print-download').onclick = download;
        $('print-filament-head').style.display = fdm ? '' : 'none';
        $('print-filament-info').style.display = fdm ? '' : 'none';
        $('print-filename').value = filename;
        $('print-filesize').value = util.comma(info.bytes);
        $('print-filament').value = Math.round(info.distance);
        calcTime();
        if (fdm) {
            calcWeight();
        }

        // persist fields when changed
        bindField('octo-host', 'octo-host');
        bindField('octo-apik', 'octo-apik');
        bindField('grid-host', 'grid-host');
        bindField('grid-apik', 'grid-apik');

        // in cam mode, show zip file option
        let downloadZip = $('print-zip');
        downloadZip.style.display = sections ? 'flex' : 'none';
        downloadZip.onclick = function() {
            let files = [];
            for (let [ name, data ] of Object.entries(sections)) {
                if (name.indexOf('op-') === 0) {
                    let head = sections.header || [];
                    let foot = sections.footer || [];
                    files.push({
                        name: `${name}.${fileext}`,
                        data: [ ...head, ...data, ...foot ].join('\r\n')
                    })
                }
            }
            kiri.client.zip(files, progress => {
                api.show.progress(progress.percent/100, "generating zip files");
            }, output => {
                api.show.progress(0);
                api.util.download(output, `${$('print-filename').value}.zip`);
            })
        };

        // in palette mode, show download button
        let downloadPalette = $('print-palette');
        downloadPalette.style.display = info.segments ? 'flex' : 'none';
        // generate MAFX downloadble file
        if (info.segments) {
            // todo: reduce segments to eliminate 0 lenghts and transitions before 150mm
            let { settings, segments } = info;
            let { device, bounds } = settings;
            let { min, max } = bounds;
            let extras = device.extras || {};
            let pinfo = extras.palette || {};
            // filter pings to those occuring after all tubes combined
            let pings = info.purges || [];
            let driveInfo = {};
            let volume = {};
            let length = {};
            // clean up and round pings
            pings.forEach(p => {
                p.length = p.length.round(3);
                // p.length = (p.length - pinfo.offset).round(2);
                // p.length = (p.length + pinfo.offset).round(2);
                p.extrusion = p.extrusion.round(3);
            });
            // add length of push filament to the last segment
            segments.peek().emitted += pinfo.push;
            let lastEmit = 0;
            let ratioVolume = (0.4 * 0.4) / (1.75 * 1.75);
            for (let seg of segments) {
                let seginfo = driveInfo[seg.tool] = driveInfo[seg.tool] || { length: 0, volume: 0 };
                seg.emitted += pinfo.offset;
                seginfo.length += seg.emitted - lastEmit;
                seginfo.volume = seginfo.length * ratioVolume;
                volume[seg.tool+1] = seginfo.volume.round(2);
                length[seg.tool+1] = seginfo.length.round(2);
                lastEmit = seg.emitted;
            }
            let totalLength = Object.values(length).reduce((a,v) => a+v).round(2);
            let totalVolume = Object.values(volume).reduce((a,v) => a+v).round(2);
            // console.log({info, device, pinfo, segments, volume, length, totalLength});
            let meta = {
                version: "3.2",
                printerProfile: {
                    id: pinfo.printer,
                    name: device.deviceName || "My Printer"
                },
                preheatTemperature: { nozzle: [0], bed: 0 },
                paletteNozzle: 0,
                time: info.time.round(1),
                volume,
                length,
                totalLength,
                totalVolume,
                inputsUsed: Object.keys(driveInfo).length,
                splices: segments.length,
                pings: pings.length,
                boundingBox: {
                    min: [ min.x, min.y, min.z ],
                    max: [ max.x, max.y, max.z ]
                },
                filaments: Object.keys(driveInfo).map(v => { return {
                    name: `Color${v}`,
                    type: "Filament",
                    color: `#${v}0${v}0${v}0`,
                    materialId: parseInt(v) + 1,
                    filamentId: parseInt(v) + 1
                }}),
            };
            let lastDrive;
            let algokeys = {};
            let algorithms = [];
            let defaultSplice = {
                compression: pinfo.press,
                cooling: pinfo.cool,
                heat: pinfo.heat
            };
            for (let key of Object.keys(driveInfo)) {
                key = parseInt(key) + 1;
                algorithms.push({
                    ingoingId: key,
                    outgoingId: key,
                    ...defaultSplice
                });
            }
            let palette = {
                version: "3.0",
                drives: [0, 0, 0, 0, 0, 0, 0, 0].map((v,i) => {
                    return driveInfo[i] ? i+1 : 0
                }),
                splices: segments.filter(r => {
                    return r.emitted >= 150;
                }).map(r => {
                    if (lastDrive >= 0 && lastDrive !== r.tool) {
                        let key = `${lastDrive}+${r.tool}`;
                        if (!algokeys[key]) {
                            let rec = algokeys[key] = {
                                ingoingId: lastDrive + 1,
                                outgoingId: r.tool + 1,
                                ...defaultSplice
                            };
                            algorithms.push(rec);
                        }
                    }
                    lastDrive = r.tool;
                    return { id: r.tool + 1, length: r.emitted.round(2) }
                }),
                pings,
                algorithms
            };
            let png;
            kiri.client.png({}, data => {
                png = data.png;
            });
            console.log({meta,palette});
            downloadPalette.onclick = function() {
                kiri.client.zip([
                    {name:"meta.json", data:JSON.stringify(meta,undefined,4)},
                    {name:"palette.json", data:JSON.stringify(palette,undefined,4)},
                    {name:"thumbnail.png", data:png.buffer}
                ], progress => {
                    api.show.progress(progress.percent/100, "generating palette files");
                }, output => {
                    api.show.progress(0);
                    api.util.download(output, `${$('print-filename').value}.mafx`);
                })
            };
        }

        // octoprint setup
        $('send-to-octohead').style.display = octo ? '' : 'none';
        $('send-to-octoprint').style.display = octo ? '' : 'none';
        if (octo) try {
            $('print-octoprint').onclick = sendto_octoprint;
            octo_host = $('octo-host');
            octo_apik = $('octo-apik');
            if (MODE === MODES.CAM) {
                $('send-to-octohead').style.display = 'none';
                $('send-to-octoprint').style.display = 'none';
            } else {
                $('send-to-octohead').style.display = '';
                $('send-to-octoprint').style.display = '';
            }
            // hide octoprint when hard-coded in the url
            if (api.const.OCTO) {
                $('ophost').style.display = 'none';
                $('opapik').style.display = 'none';
                $('oph1nt').style.display = 'none';
                $('send-to-gridhost').style.display = 'none';
            }
            octo_host.value = localGet('octo-host') || '';
            octo_apik.value = localGet('octo-apik') || '';
        } catch (e) { console.log(e) }

        // grid:host setup
        $('send-to-hosthead').style.display = ghost ? '' : 'none';
        $('send-to-gridhost').style.display = ghost ? '' : 'none';
        if (ghost) try {
            $('print-gridhost').onclick = sendto_gridhost;
            $('grid-host').onkeyup = gridhost_probe;
            $('grid-apik').onkeyup = gridhost_probe;
            grid_host = $('grid-host');
            grid_apik = $('grid-apik');
            grid_target = $('grid-target');
            grid_target.onchange = function(ev) {
                localSet('grid-target', grid_targets[grid_target.selectedIndex]);
            };
            grid_host.value = localGet('grid-host') || '';
            grid_apik.value = localGet('grid-apik') || '';
            gridhost_probe();
        } catch (e) { console.log(e) }

        // grid:local setup
        if (local) try {
            gridlocal_probe();
            $('print-gridlocal').onclick = sendto_gridlocal;
            $('admin-gridlocal').onclick = admin_gridlocal;
        } catch (e) { console.log(e) }

        // preview of the generated GCODE (first 64k max)
        if (preview && gcode) $('code-preview-textarea').value = gcode.substring(0,65535);

        // show dialog
        api.modal.show('print');
    });
}

});
