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

import { THREE } from '../ext/three.js';
import { api } from './api.js';
import { broker as motoBroker } from '../moto/broker.js';
import { client as motoClient } from '../moto/client.js';
import { space as motoSpace } from '../moto/space.js';
import { util as meshUtil } from './util.js';

const { Matrix4, Vector3, Box3, Box3Helper, Quaternion } = THREE;
const worker = motoClient.fn;
const lookUp = new Vector3(0,0,-1);

// broker updates generated by objects
const publish = {
    meta: motoBroker.bind("object_meta"),
    destroy: motoBroker.bind("object_destroy"),
    visible: motoBroker.bind("object_visible"),
};

class MeshObject {
    constructor(id) {
        this.id = id || meshUtil.uuid();
        this.meta = { pos: [0,0,0] };
        worker.object_create({ id: this.id, type: this.type });
        worker.object_meta({ id: this.id, meta: this.meta });
        this.log('NEW');
    }

    log() {
        api.log.emit(this.id, this.type, ...arguments);
    }

    get type() {
        throw "type() requires implementation";
    }

    // @returns {THREE.Object3D}
    get object() {
        throw "object() requires implementation";
    }

    get bounds() {
        throw "bounds() requires implementation";
    }

    // manage lifecycle with worker, mesh app caches, etc
    destroy() {
        // update worker state
        worker.object_destroy({id: this.id});
        // update object store
        api.db.space.remove(this.id);
        // main app cache workspace updates
        publish.destroy(this.id);
    }

    // used during state restoration
    applyMeta(meta = {}) {
        this.log('apply-meta', meta);
        if (meta.pos) this.position(...meta.pos);
        this.visible(meta.visible ?? true);
        this.metaChanged(meta);
        return this;
    }

    // used during state restoration
    applyMatrix(elements) {
        if (elements) {
            this.log('apply-matrix');
            this.object.applyMatrix4(new Matrix4().fromArray(elements));
        }
        return this;
    }

    // used during model splitting
    applyMatrix4(matrix) {
        return this.applyMatrix(matrix.elements);
    }

    select() {
        throw "select() requires implementation";
    }

    remove() {
        throw "remove() requires implementation";
    }

    focus() {
        api.focus(this);
        return this;
    }

    visible(opt) {
        if (opt === undefined) {
            return this.object.visible;
        }
        if (opt.toggle) {
            this.visible(!this.visible());
        } else {
            this.object.visible = opt;
        }
        publish.visible({ id: this.id, visible: this.object.visible });
        return this;
    }

    floor() {
        return this.move(0, 0, -this.bounds.min.z);
    }

    center() {
        throw "center() requires implementation";
    }

    centerXY() {
        throw "centerXY() requires implementation";
    }

    move() {
        throw "move() requires implementation";
    }

    scale() {
        throw "scale() requires implementation";
    }

    qrotate(quaternion) {
        throw "qrotate() requires implementation";
    }

    rotate(x = 0, y = 0, z = 0) {
        this.log('object-rotate', ...arguments);
        let m = new Matrix4();
        if (x) m.multiply(new Matrix4().makeRotationX(x));
        if (y) m.multiply(new Matrix4().makeRotationY(y));
        if (z) m.multiply(new Matrix4().makeRotationZ(z));
        this.qrotate(new Quaternion().setFromRotationMatrix(m));
        return this;
    }

    rotation() {
        throw "rotation() requires implementation";
    }

    // rotate object in the directon of normal (place face on Z plane)
    rotateTowardZ(normal) {
        let q = new Quaternion().setFromUnitVectors(normal, lookUp);
        this.qrotate(q);
        this.floor();
    }

    position() {
        throw "position() requires implementation";
    }

    metaChanged(values = {}) {
        this.object.updateMatrix();
        motoSpace.update();
        Object.assign(this.meta, values, {
            // matrix: this.object.matrix.elements
        });
        worker.object_meta({ id: this.id, meta: this.meta });
        publish.meta({ id: this.id, meta: this.meta });
        return this;
    }
}

export { MeshObject as object };
