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

"use strict";

// dep: moto.license
// dep: moto.webui
// dep: moto.client
// dep: moto.broker
// dep: moto.space
// dep: data.index
// dep: mesh.api
// dep: mesh.split
// dep: mesh.edges
// dep: mesh.model
// dep: mesh.build
// dep: load.file
// use: geo.polygons
gapp.main("main.mesh", [], (root) => {

const { Quaternion } = THREE;
const { mesh, moto } = root;
const { broker } = gapp;
const { space } = moto;

const version = mesh.version = '1.5.5';
const call = broker.send;
const dbindex = [ "admin", "space" ];

function log() {
    return mesh.api.log.emit(...arguments);
}

// set below. called once the DOM readyState = complete
// this is the main() entrypoint called after all dependents load
function init() {
    let stores = data.open('mesh', { stores: dbindex, version: 4 }).init(),
        dark = false,
        ortho = false,
        zoomrev = true,
        zoomspd = 1,
        platform = space.platform,
        db = mesh.db = {
            admin: stores.promise('admin'),
            space: stores.promise('space')
        };

    // mark init time and use count
    db.admin.put("init", Date.now());
    db.admin.get("uses").then(v => db.admin.put("uses", (v||0) + 1));

    // setup default workspace
    space.setAntiAlias(true);
    space.useDefaultKeys(false);
    space.init($('container'), delta => { }, ortho);
    space.sky.set({
        grid: false,
        color: dark ? 0 : 0xffffff
    });
    platform.set({
        volume: false,
        round: false,
        zOffset: 0,
        opacity: 0,
        color: 0xdddddd,
        zoom: { reverse: true, speed: 1 },
        size: { width: 2000, depth: 2000, height: 0.05, maxz: 2000 },
        grid: { major: 25, minor: 5, zOffset: 0,
            colorMajor: 0xcccccc, colorMinor: 0xeeeeee,
            colorX: 0xff7777, colorY: 0x7777ff },
    });
    platform.onMove(() => {
        // save last location and focus
        db.admin.put('camera', {
            place: space.view.save(),
            focus: space.view.getFocus()
        });
    }, 100);
    space.view.setZoom(zoomrev, zoomspd);

    // reload stored space when worker is ready
    moto.client.on('ready', restore_space);

    // start worker
    moto.client.start(`/code/mesh_work?${gapp.version}`);

    // trigger space event binding
    call.space_init({ space, platform });

    // trigger ui building
    call.ui_build();
}

// restore space layout and view from previous session
async function restore_space() {
    const { api } = mesh;
    const space = moto.space;
    const db_admin = mesh.db.admin;
    const db_space = mesh.db.space;
    // let mcache = {};
    await db_admin.get("camera")
        .then(saved => {
            if (saved) {
                space.view.load(saved.place);
                space.view.setFocus(saved.focus);
            }
        });
    const mcache = await db_admin.get("meta") || {};
    let count = 0;
    await db_space.iterate({ map: true }).then(cached => {
        const keys = [];
        const claimed = [];
        for (let [id, data] of Object.entries(cached)) {
            // console.log({ id, data });
            keys.push(id);
            if (count++ === 0) {
                log(`restoring workspace`);
            }
            // restore object based on type
            // group arrays load models they contain
            // sketches are loaded by type since they're not grouped
            if (Array.isArray(data)) {
                claimed.push(id);
                let models = data
                    .map(id => {
                        claimed.push(id);
                        return { id, md: cached[id] }
                    })
                    .filter(r => r.md) // filter cache misses
                    .map(r => new mesh.model(r.md, r.id).applyMeta(mcache[r.id]))
                if (models.length) {
                    log(`restored ${models.length} model(s)`);
                    mesh.api.group.new(models, id).applyMeta(mcache[id])
                } else {
                    log(`removed empty group ${id}`);
                    db_space.remove(id);
                }
            } else if (data.type === 'sketch') {
                claimed.push(id);
                mesh.api.add.sketch({ id, ...data });
            }
        }
        for (let id of claimed) {
            keys.remove(id);
        }
        if (keys.length) {
            log(`removing ${keys.length} unclaimed meshes`);
        }
        // clear out meshes left in the space db along with their meta-data
        for (let id of keys) {
            db_space.remove(id);
            delete mcache[id];
        }
        // restore global cache only after objects are restored
        // otherwise their setup will corrupt the cache for other restores
        metaCache = mcache;
        store_meta();
    }).then(() => {
        // restore preferences after models are restored
        return api.prefs.load().then(() => {
            let { map } = api.prefs;
            let { space, mode } = map;
            api.grid(space.grid);
            // restore selected state
            let selist = space.select || [];
            let smodel = api.model.list().filter(m => selist.contains(m.id));
            let sgroup = api.group.list().filter(m => selist.contains(m.id));
            let tolist = space.tools || [];
            let tmodel = api.model.list().filter(m => tolist.contains(m.id));
            let tgroup = api.group.list().filter(m => tolist.contains(m.id));
            let sklist = api.sketch.list().filter(s => selist.contains(s.id));
            api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]);
            // restore edit mode
            api.mode[mode]();
            // restore dark mode
            set_darkmode(map.space.dark);
        });
    }).finally(() => {
        // hide loading curtain
        $d('curtain','none');
        if (api.prefs.map.info.welcome !== false) {
            api.welcome(version);
        }
    });
}

// add space event bindings
function space_init(data) {
    let platcolor = 0x00ff00;
    let { space, platform } = data;
    let { api } = mesh;
    let { selection } = api;

    // add file drop handler
    space.event.addHandlers(self, [
        'drop', (evt) => {
            estop(evt);
            platform.set({ opacity: 0, color: platcolor });
            call.load_files([...evt.dataTransfer.files]);
        },
        'dragover', evt => {
            estop(evt);
            evt.dataTransfer.dropEffect = 'copy';
            let color = platform.setColor(0x00ff00);
            if (color !== 0x00ff00) platcolor = color;
            platform.set({ opacity: 0.1 });
        },
        'dragleave', evt => {
            platform.set({ opacity: 0, color: platcolor });
        },
        'keypress', evt => {
            if (api.modal.showing) {
                return;
            }
            if (evt.key === '?') {
                return api.welcome(version);
            }
            let { shiftKey, metaKey, ctrlKey, code } = evt;
            switch (code) {
                case 'Digit1':
                    return api.mode.sketch();
                case 'Digit2':
                    return api.mode.object();
                case 'Digit3':
                    return api.mode.tool();
                case 'Digit4':
                    return api.mode.surface();
                case 'Digit5':
                    return api.mode.face();
                case 'Digit6':
                    return api.mode.edge();
                case 'KeyQ':
                    return api.settings();
                case 'KeyI':
                    return shiftKey ? api.tool.invert() : api.file.import();
                case 'KeyX':
                    return api.file.export();
                case 'KeyD':
                    return shiftKey && api.tool.duplicate();
                case 'KeyC':
                    return selection.centerXY().focus();
                case 'KeyF':
                    return shiftKey ? selection.focus() : selection.floor().focus();
                case 'KeyM':
                    return shiftKey ? api.tool.merge() : api.tool.mirror();
                case 'KeyU':
                    return shiftKey && api.tool.union();
                case 'KeyA':
                    if (api.mode.is([ api.modes.edge ])) return mesh.edges.add();
                    return shiftKey && api.tool.analyze();
                case 'KeyE':
                    if (api.mode.is([ api.modes.sketch ])) {
                        estop(evt);
                        return api.sketch.extrude();
                    }
                    return;
                case 'KeyV':
                    return shiftKey ? selection.show() : selection.focus();
                case 'KeyN':
                    return shiftKey ? estop(evt, api.tool.rename()) : api.normals();
                case 'KeyW':
                    return api.wireframe();
                case 'KeyG':
                    return shiftKey ?
                        (api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
                        api.grid();
                case 'KeyL':
                    return api.log.toggle({ spinner: false });
                case 'KeyS':
                    if (!api.mode.is([ api.modes.object ])) return;
                    return shiftKey ? selection.visible({toggle:true}) : mesh.split.start();
                case 'KeyB':
                    return selection.boundsBox({toggle:true});
                case 'KeyH':
                    return shiftKey ? selection.hide() : space.view.home();
                case 'KeyT':
                    return shiftKey ? api.tool.triangulate() : space.view.top();
                case 'KeyZ':
                    return space.view.reset();
            }
        },
        'keydown', evt => {
            let { shiftKey, metaKey, ctrlKey, code } = evt;
            let rv = (Math.PI / 12);
            if (api.modal.showing) {
                if (code === 'Escape') {
                    api.modal.cancel();
                }
                return;
            }
            let rot, floor = api.prefs.map.space.floor !== false;
            switch (code) {
                case 'KeyA':
                    if (metaKey || ctrlKey) {
                        if (!selection.sketch()?.selection.all()) {
                            api.mode.object();
                            selection.set(api.group.list());
                        }
                        estop(evt);
                    }
                    break;
                case 'Escape':
                    if (selection.clear()) {
                        mesh.edges.clear();
                        mesh.split.end();
                    }
                    estop(evt);
                    break;
                case 'Backspace':
                case 'Delete':
                    let mode = api.mode.get();
                    if ([api.modes.object, api.modes.tool, api.modes.sketch].contains(mode)) {
                        selection.delete();
                    } else {
                        for (let m of api.model.list()) {
                            m.deleteSelections();
                            space.refresh()
                        }
                    }
                    estop(evt);
                    break;
                case 'ArrowUp':
                    rot = selection.rotate(-rv,0,0);
                    break;
                case 'ArrowDown':
                    rot = selection.rotate(rv,0,0);
                    break;
                case 'ArrowLeft':
                    if (shiftKey) {
                        rot = selection.rotate(0,-rv,0);
                    } else {
                        floor = false;
                        rot = selection.rotate(0,0,rv);
                    }
                    break;
                case 'ArrowRight':
                    if (shiftKey) {
                        rot = selection.rotate(0,rv,0);
                    } else {
                        floor = false;
                        rot = selection.rotate(0,0,-rv);
                    }
                    break;
            }
            if (rot && floor) {
                rot.floor();
            }
        }
    ]);

    // mouse hover/click handlers. required to enable model drag in space.js
    // called two ways:
    // without args to return a list of selectable targets
    // with args when a selection is made
    space.mouse.downSelect((int, event, ints) => {
        let obj = int?.object;
        if (obj) api.selection.drag({ start: int.object });
        if (obj) obj.sketch_item?.sketch.drag({ start: int.object });
        if (event?.shiftKey) {
            return api.objects();
        } else {
            return undefined;
        }
    });

    // called two ways:
    // without args to return a list of selectable targets
    // with args when a selection is made
    space.mouse.upSelect((int, event, ints) => {
        if (ints?.length > 1) {
            // ensure sketch items are returned before sketch plane / handles
            let els = ints.filter(i => i.object.sketch_item);
            int = els.length ? els[0] : int;
        }
        if (event?.target?.nodeName === "CANVAS") {
            // a selection was made
            const { model, sketch, sketch_item } = int?.object || {};
            const { altKey, ctrlKey, metaKey, shiftKey } = event;
            if (mesh.split.active()) {
                return mesh.split.select(model);
            }
            if (model) {
                const group = model.group;
                if (metaKey) {
                    // set focus on intersected face
                    const { x, y, z } = int.point;
                    const q = new Quaternion().setFromRotationMatrix(group.object.matrix);
                    // rotate normal using group's matrix
                    const normal = shiftKey ? int.face.normal.applyQuaternion(q) : undefined;
                    // y,z swap due to world rotation for orbit controls
                    api.focus({center: { x, y:-z, z:y }, normal});
                    let one = api.sketch.selected.one;
                    if (one && confirm('attach sketch to face?')) {
                        one.center = {
                            x, y: -z, z: y
                        };
                        one.normal = {
                            x: normal.x,
                            y: normal.y,
                            z: normal.z
                        };
                        one.render();
                    }
                } else if (ctrlKey) {
                    // rotate selected face towawrd z "floor"
                    group.rotateTowardZ(int.face.normal);
                    selection.update();
                } else {
                    const { modes } = api;
                    const { surface } = api.prefs.map;
                    const opt = { radians: 0, radius: surface.radius };
                    const mode = api.mode.get();
                    switch(mode) {
                        case modes.sketch:
                            api.mode.object();
                        case modes.object:
                        case modes.tool:
                            selection.toggle(shiftKey ? model : model.group, mode === modes.tool);
                            break;
                        case modes.surface:
                            opt.radians = surface.radians;
                        case modes.face:
                            // find faces adjacent to point/line clicked
                            model.find(int,
                                altKey ? { toggle: true } :
                                shiftKey ? { clear: true } : { select: true },
                                opt);
                            break;
                        case modes.edge:
                            mesh.edges.select();
                            break;
                    }
                }
            } else if (sketch) {
                if (metaKey) {
                    // set focus on intersected face
                    const { x, y, z } = int.point;
                    // rotate normal using group's matrix
                    const normal = shiftKey ? int.face.normal.clone(): undefined;
                    // ransform the normal to align with sketch group
                    normal?.transformDirection(int.object.sketch.group.matrix);
                    // y,z swap due to world rotation for orbit controls
                    api.focus({ center: { x, y:-z, z:y }, normal });
                } else {
                    selection.toggle(sketch);
                }
            } else if (sketch_item) {
                sketch_item.toggle();
            }
        } else {
            // return objects upSelect can choose from
            let visible = api.objects().filter(o => o.visible);
            let sketches = api.selection.sketches();
            if (sketches.length) {
                // when sketch selected, append sketch items, too
                visible.appendAll(sketches[0].object.children);
            };
            return visible;
        }
    });

    space.mouse.onDrag((delta, offset, up = false) => {
        let { mode, modes } = api;
        if (delta) {
            if (delta.event?.shiftKey && selection.count()) {
                selection.drag({ delta, offset });
            }
        } else if (up) {
            if (selection.count()) {
                selection.drag({ end: true });
            }
        } else if (mode.is([ modes.object, modes.tool, modes.sketch ])) {
            // return true if there are draggable elements
            return api.objects().length > 0 || api.selection.sketches().length;
        }
    });
}

function load_files(files) {
    log(`loading file...`);
    let api = mesh.api;
    let has_image = false;
    let has_svg = false;
    let has_gbr = false;
    let sketch = api.sketch.selected.one;
    for (let file of files) {
        has_image = has_image || file.type === 'image/png';
        has_svg = has_svg || file.name.toLowerCase().endsWith(".svg") > 0;
        has_gbr = has_gbr || file.name.toLowerCase().endsWith(".gbr") > 0;
    }
    if (sketch && has_gbr) {
        load.File.load([...files], { flat: true }).then(layers => {
            for (let layer of layers.flat()) {
                let { circs, closed, open, rects } = layer;
                open = open.map(poly => {
                    const diam = poly.tool?.shape?.diameter;
                    return diam ? poly.offset_open(diam / 2, 'round') : null;
                }).filter(p => p).flat();
                for (let set of [ closed, open, circs, rects ]) {
                    let group = mesh.util.uuid();
                    for (let poly of set) {
                        sketch.add.polygon({ poly, group });
                    }
                }
            }
        });
    } else
    if (sketch && has_svg) {
        load.File.load([...files], { flat: true })
            .then(polys => polys.forEach(set => {
                let group = mesh.util.uuid();
                set.forEach(poly => sketch.add.polygon({ poly, group }))
            }))
            .catch(error => dbug.error(error))
            .finally(() => mesh.api.log.hide());
    } else
    if (has_svg) {
        api.modal.dialog({
            title: `svg import`,
            body: [ h.div({ class: "image-import" }, [
                h.div([
                    h.label("extrude"),
                    h.input({ id: "extrude_height", value: 5, size: 4 })
                ]),
                h.div([
                    h.label("repair"),
                    h.input({ id: "svg_repair", type: "checkbox", checked: true })
                ]),
                h.div([
                    h.button({ _: "import", onclick() {
                        let { svg_repair, extrude_height } = api.modal.bound;
                        load_files_opt(files, {
                            soup: svg_repair.checked,
                            depth: extrude_height
                        });
                        api.modal.hide();
                    } }),
                ])
            ]) ]
        });
    } else
    if (has_image) {
        api.modal.dialog({
            title: `image import`,
            body: [ h.div({ class: "image-import" }, [
                h.div([
                    h.label("invert pixels"),
                    h.input({ id: "inv_image", type: "checkbox" })
                ]),
                h.div([
                    h.label("invert alpha"),
                    h.input({ id: "inv_alpha", type: "checkbox" })
                ]),
                h.div([
                    h.label("border size"),
                    h.input({ id: "img_border", value: 0, size: 4 })
                ]),
                h.div([
                    h.label("blur pixels"),
                    h.input({ id: "img_blur", value: 0, size: 4 })
                ]),
                h.div([
                    h.label("base pixels"),
                    h.input({ id: "img_base", value: 0, size: 4 })
                ]),
                h.div([
                    h.button({ _: "import", onclick() {
                        let { inv_image, inv_alpha, img_border, img_blur, img_base } = api.modal.bound;
                        load_files_opt(files, {
                            inv_image: inv_image.checked,
                            inv_alpha: inv_alpha.checked,
                            border: parseInt(img_border.value || 0),
                            blur: parseInt(img_blur.value || 0),
                            base: parseInt(img_base.value || 0),
                        });
                        api.modal.hide();
                    } }),
                ])
            ]) ]
        });
    } else {
        load_files_opt(files);
    }
}

function load_files_opt(files, opt) {
    return load.File.load([...files], opt)
        .then(data => call.space_load(data))
        .catch(error => log(error).pin({}) && dbug.error(error))
        // .finally(() => mesh.api.log.hide());
}

// add object loader
function space_load(data) {
    if (data && data.length && (data = data.flat()).length)
    mesh.api.group.new(data.map(el => new mesh.model(el)))
        .promote()
        .focus();
}

let metaCache = {};

function store_meta() {
    mesh.db.admin.put("meta", metaCache);
}

function update_meta(id, data) {
    let meta = metaCache[id] = metaCache[id] || {};
    Object.assign(meta, data);
    store_meta();
}

// cache model visibility for page restores
function object_visible(data) {
    let { id, visible } = data;
    update_meta(id, { visible });
}

// cache model matrices for page restores
function object_meta(data) {
    let { id, meta } = data;
    update_meta(id, meta);
}

function object_destroy(id) {
    delete metaCache[id];
    store_meta();
}

// listen for changes like dark mode toggle
function set_darkmode(dark) {
    let { prefs, model } = mesh.api;
    let { sky, platform } = moto.space;
    prefs.map.space.dark = dark;
    if (dark) {
        mesh.material.wireframe.color.set(0xaaaaaa);
        mesh.material.wireline.color.set(0xaaaaaa);
        $('app').classList.add('dark');
    } else {
        mesh.material.wireframe.color.set(0,0,0);
        mesh.material.wireline.color.set(0,0,0);
        $('app').classList.remove('dark');
    }
    sky.set({
        color: dark ? 0 : 0xffffff,
        ambient: { intensity: dark ? 0.55 : 1.1 }
    });
    platform.set({
        light: dark ? 0.08 : 0.08,
        grid: dark ? {
            colorMajor: 0x666666,
            colorMinor: 0x333333,
        } : {
            colorMajor: 0xcccccc,
            colorMinor: 0xeeeeee,
        },
    });
    mesh.api.updateFog();
    platform.setSize();
    for (let m of model.list()) {
        m.normals({ refresh: true });
    }
    prefs.save();
}

function set_normals_length(length) {
    let { prefs } = mesh.api;
    prefs.map.normals.length = length || 1;
    prefs.save();
}

function set_normals_color(color) {
    let { prefs } = mesh.api;
    let { map } = prefs;
    if (map.space.dark) {
        map.normals.color_dark = color || 0;
    } else {
        map.normals.color_lite = color || 0;
    }
    prefs.save();
}

function set_surface_radians(radians) {
    let { prefs } = mesh.api;
    prefs.map.surface.radians = parseFloat(radians || 0.1);
    prefs.save();
}

function set_surface_radius(radius) {
    let { prefs } = mesh.api;
    prefs.map.surface.radius = parseFloat(radius || 0.2);
    prefs.save();
}

function set_wireframe_opacity(opacity) {
    let { prefs } = mesh.api;
    prefs.map.wireframe.opacity = parseFloat(opacity || 0.15);
    prefs.save();
}

function set_wireframe_fog(fogx) {
    let { prefs } = mesh.api;
    prefs.map.wireframe.fog = parseFloat(fogx || 3);
    prefs.save();
}

function set_snap_value(snap) {
    let { prefs } = mesh.api;
    prefs.map.space.snap = parseFloat(snap || 1);
    prefs.save();
}

// bind functions to topics
broker.listeners({
    load_files,
    object_meta,
    object_destroy,
    object_visible,
    space_init,
    space_load,
    set_darkmode,
    set_normals_color,
    set_normals_length,
    set_surface_radians,
    set_surface_radius,
    set_wireframe_opacity,
    set_wireframe_fog,
    set_snap_value
});

// remove version cache bust from url
window.history.replaceState({},'','/mesh/');

// setup init() trigger when dom + scripts complete
document.onreadystatechange = function() {
    if (document.readyState === 'complete') {
        init();
    }
}

});
