"use strict";

const orbit = require("./orbit");
const cookie = require("./cookie")("bbctrl-");
const font = require("./helvetiker_regular.typeface.json");

module.exports = {
    template: "#path-viewer-template",
    props: [ "toolpath" ],

    data: function() {
        return {
            enabled: false,
            loading: false,
            dirty: true,
            snapView: cookie.get("snap-view", "isometric"),
            small: cookie.get_bool("small-path-view", true),
            surfaceMode: "cut",
            showPath: cookie.get_bool("show-path", true),
            showTool: cookie.get_bool("show-tool", true),
            showBBox: cookie.get_bool("show-bbox", true),
            showAxes: cookie.get_bool("show-axes", true),
            showIntensity: cookie.get_bool("show-intensity", false)
        };
    },

    computed: {
        target: function() {
            return this.$el.querySelector(".path-viewer-content");
        },

        webglAvailable: function() {
            // Create canvas element. The canvas is not added to the
            // document itself, so it is never displayed in the
            // browser window.
            const canvas = document.createElement("canvas");

            // Get WebGLRenderingContext from canvas element.
            const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");

            // Report the result.
            return gl && gl instanceof WebGLRenderingContext;
        }
    },

    watch: {
        toolpath: function() {
            Vue.nextTick(this.update);
        },

        surfaceMode: function(mode) {
            this.update_surface_mode(mode);
        },

        small: function(enable) {
            cookie.set_bool("small-path-view", enable);
            Vue.nextTick(this.update_view);
        },

        showPath: function(enable) {
            cookie.set_bool("show-path", enable);
            this.set_visible(this.pathView, enable);
        },

        showTool: function(enable) {
            cookie.set_bool("show-tool", enable);
            this.set_visible(this.toolView, enable);
        },

        showAxes: function(enable) {
            cookie.set_bool("show-axes", enable);
            this.set_visible(this.axesView, enable);
        },

        showIntensity: function(enable) {
            cookie.set_bool("show-intensity", enable);
            Vue.nextTick(this.update);
        },

        showBBox: function(enable) {
            cookie.set_bool("show-bbox", enable);
            this.set_visible(this.bboxView, enable);
            this.set_visible(this.envelopeView, enable);
        },

        x: function() {
            this.axis_changed();
        },

        y: function() {
            this.axis_changed();
        },

        z: function() {
            this.axis_changed();
        }
    },

    ready: function() {
        this.graphics();
        Vue.nextTick(this.update);
    },

    methods: {
        update: async function() {
            if (!this.webglAvailable) {
                return;
            }

            if (!this.state.selected) {
                this.dirty = true;
                this.scene = new THREE.Scene();

            } else if (!this.toolpath.filename && !this.loading) {
                this.loading = true;
                this.dirty = true;
                this.draw_loading();
            }

            if (!this.enabled || !this.toolpath.filename) {
                return;
            }

            async function get(url) {
                const response = await fetch(`${url}`, { cache: "no-cache" });
                const arrayBuffer = await response.arrayBuffer();

                return new Float32Array(arrayBuffer);
            }

            const [ positions, speeds ] = await Promise.all([
                get(`/api/path/${this.toolpath.filename}/positions`),
                get(`/api/path/${this.toolpath.filename}/speeds`)
            ]);

            this.positions = positions;
            this.speeds = speeds;
            this.loading = false;

            // Update scene
            this.scene = new THREE.Scene();
            this.draw(this.scene);
            this.snap(this.snapView);

            this.update_view();
        },

        update_surface_mode: function(mode) {
            if (!this.enabled) {
                return;
            }

            if (typeof this.surfaceMaterial != "undefined") {
                this.surfaceMaterial.wireframe = mode == "wire";
                this.surfaceMaterial.needsUpdate = true;
            }

            this.set_visible(this.surfaceMesh, mode == "cut" || mode == "wire");
            this.set_visible(this.workpieceMesh, mode == "solid");
        },

        load_surface: function(surface) {
            if (typeof surface == "undefined") {
                this.vertices = undefined;
                this.normals = undefined;
                return;
            }

            this.vertices = surface.vertices;

            // Expand normals
            this.normals = [];
            for (let i = 0; i < surface.normals.length / 3; i++) {
                for (let j = 0; j < 3; j++) {
                    for (let k = 0; k < 3; k++) {
                        this.normals.push(surface.normals[i * 3 + k]);
                    }
                }
            }
        },

        set_visible: function(target, visible) {
            if (typeof target != "undefined") {
                target.visible = visible;
            }
            this.dirty = true;
        },

        get_dims: function() {
            const computedStyle = window.getComputedStyle(this.target);

            return {
                width: parseInt(computedStyle.width),
                height: parseInt(computedStyle.height)
            };
        },

        update_view: function() {
            if (!this.enabled) {
                return;
            }

            const dims = this.get_dims();

            this.camera.aspect = dims.width / dims.height;
            this.camera.updateProjectionMatrix();
            this.renderer.setSize(dims.width, dims.height);

            if (this.loading) {
                this.controls.reset();
                this.camera.position.copy(new THREE.Vector3(0, 0, 600));
                this.camera.lookAt(new THREE.Vector3(0, 0, 0));
            }

            this.dirty = true;
        },

        update_tool: function(tool) {
            if (!this.enabled) {
                return;
            }

            if (typeof tool == "undefined") {
                tool = this.toolView;
            }

            if (typeof tool == "undefined") {
                return;
            }

            tool.position.x = this.x.pos;
            tool.position.y = this.y.pos;
            tool.position.z = this.z.pos;
        },

        update_envelope: function(envelope) {
            if (!this.enabled || !this.axes.homed) {
                return;
            }

            if (typeof envelope == "undefined") {
                envelope = this.envelopeView;
            }

            if (typeof envelope == "undefined") {
                return;
            }

            const min = new THREE.Vector3();
            const max = new THREE.Vector3();

            for (const axis of "xyz") {
                min[axis] = this[axis].min - this[axis].off;
                max[axis] = this[axis].max - this[axis].off;
            }

            const bounds = new THREE.Box3(min, max);
            if (bounds.isEmpty()) {
                envelope.geometry = this.create_empty_geom();
            } else {
                envelope.geometry = this.create_bbox_geom(bounds);
            }
        },

        axis_changed: function() {
            this.update_tool();
            this.update_envelope();
            this.dirty = true;
        },

        graphics: function() {
            if (!this.webglAvailable) {
                return;
            }

            try {
                // Renderer
                this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
                this.renderer.setPixelRatio(window.devicePixelRatio);
                this.renderer.setClearColor(0, 0);
                this.target.appendChild(this.renderer.domElement);

            } catch (e) {
                console.log("WebGL not supported: ", e);
                return;
            }

            this.enabled = true;

            // Camera
            this.camera = new THREE.PerspectiveCamera(45, 4 / 3, 1, 10000);

            // Lighting
            this.ambient = new THREE.AmbientLight(0xffffff, 0.5);

            const keyLight = new THREE.DirectionalLight(new THREE.Color("hsl(30, 100%, 75%)"), 0.75);
            keyLight.position.set(-100, 0, 100);

            const fillLight = new THREE.DirectionalLight(new THREE.Color("hsl(240, 100%, 75%)"), 0.25);
            fillLight.position.set(100, 0, 100);

            const backLight = new THREE.DirectionalLight(0xffffff, 0.5);
            backLight.position.set(100, 0, -100).normalize();

            this.lights = new THREE.Group();
            this.lights.add(keyLight);
            this.lights.add(fillLight);
            this.lights.add(backLight);

            // Surface material
            this.surfaceMaterial = this.create_surface_material();

            // Controls
            this.controls = new orbit(this.camera, this.renderer.domElement);
            this.controls.enableDamping = true;
            this.controls.dampingFactor = 0.2;
            this.controls.rotateSpeed = 0.25;
            this.controls.enableZoom = true;

            // Move lights with scene
            this.controls.addEventListener("change", function(scope) {
                return function() {
                    keyLight.position.copy(scope.camera.position);
                    fillLight.position.copy(scope.camera.position);
                    backLight.position.copy(scope.camera.position);
                    keyLight.lookAt(scope.controls.target);
                    fillLight.lookAt(scope.controls.target);
                    backLight.lookAt(scope.controls.target);
                    scope.dirty = true;
                };
            }(this));

            // Events
            window.addEventListener("resize", this.update_view, false);

            // Start it
            this.render();
        },

        create_surface_material: function() {
            return new THREE.MeshPhongMaterial({
                specular: 0x111111,
                shininess: 10,
                side: THREE.FrontSide,
                color: 0x0c2d53
            });
        },

        draw_loading: function() {
            this.scene = new THREE.Scene();

            const geometry = new THREE.TextGeometry("Loading 3D View...", {
                font: new THREE.Font(font),
                size: 40,
                height: 5,
                curveSegments: 12,
                bevelEnabled: true,
                bevelThickness: 10,
                bevelSize: 8,
                bevelSegments: 5
            });
            geometry.computeBoundingBox();

            const mesh = new THREE.Mesh(geometry, this.surfaceMaterial);

            this.scene.add(mesh);
            this.scene.add(this.ambient);
            this.scene.add(this.lights);
            this.update_view();
        },

        draw_workpiece: function(scene, material) {
            if (typeof this.workpiece == "undefined") {
                return;
            }

            let min = this.workpiece.min;
            let max = this.workpiece.max;

            min = new THREE.Vector3(min[0], min[1], min[2]);
            max = new THREE.Vector3(max[0], max[1], max[2]);
            const dims = max.clone().sub(min);

            const geometry = new THREE.BoxGeometry(dims.x, dims.y, dims.z);
            const mesh = new THREE.Mesh(geometry, material);

            const offset = dims.clone();
            offset.divideScalar(2);
            offset.add(min);

            mesh.position.add(offset);

            geometry.computeBoundingBox();

            scene.add(mesh);

            return mesh;
        },

        draw_surface: function(scene, material) {
            if (typeof this.vertices == "undefined") {
                return;
            }

            const geometry = new THREE.BufferGeometry();

            geometry.addAttribute("position", new THREE.Float32BufferAttribute(this.vertices, 3));
            geometry.addAttribute("normal", new THREE.Float32BufferAttribute(this.normals, 3));

            geometry.computeBoundingSphere();
            geometry.computeBoundingBox();

            return new THREE.Mesh(geometry, material);
        },

        draw_tool: function(scene, bbox) {
            // Tool size is relative to bounds
            const size = bbox.getSize(new THREE.Vector3());
            let length = (size.x + size.y + size.z) / 24;

            if (length < 1) {
                length = 1;
            }

            const material = new THREE.MeshPhongMaterial({
                transparent: true,
                opacity: 0.75,
                specular: 0x161616,
                shininess: 10,
                color: 0xffa500 // Orange
            });

            const geometry = new THREE.CylinderGeometry(length / 2, 0, length, 128);
            geometry.translate(0, length / 2, 0);
            geometry.rotateX(0.5 * Math.PI);

            const mesh = new THREE.Mesh(geometry, material);
            this.update_tool(mesh);
            mesh.visible = this.showTool;
            scene.add(mesh);
            return mesh;
        },

        draw_axis: function(axis, up, length, radius) {
            let color;

            if (axis == 0) {
                color = 0xff0000;
            } else if (axis == 1) {
                color = 0x00ff00;
            } else if (axis == 2) {
                color = 0x0000ff;
            }

            const group = new THREE.Group();
            const material = new THREE.MeshPhongMaterial({
                specular: 0x161616, shininess: 10, color: color
            });
            let geometry = new THREE.CylinderGeometry(radius, radius, length, 128);
            geometry.translate(0, -length / 2, 0);
            group.add(new THREE.Mesh(geometry, material));

            geometry = new THREE.CylinderGeometry(1.5 * radius, 0, 2 * radius, 128);
            geometry.translate(0, -length - radius, 0);
            group.add(new THREE.Mesh(geometry, material));

            if (axis == 0) {
                group.rotateZ((up ? 0.5 : 1.5) * Math.PI);
            } else if (axis == 1) {
                group.rotateX((up ? 0 : 1 ) * Math.PI);
            } else if (axis == 2) {
                group.rotateX((up ? 1.5 : 0.5) * Math.PI);
            }

            return group;
        },

        draw_axes: function(scene, bbox) {
            const size = bbox.getSize(new THREE.Vector3());
            let length = (size.x + size.y + size.z) / 3;
            length /= 10;

            if (length < 1) {
                length = 1;
            }

            const radius = length / 20;

            const group = new THREE.Group();

            for (let axis = 0; axis < 3; axis++) {
                for (let up = 0; up < 2; up++) {
                    group.add(this.draw_axis(axis, up, length, radius));
                }
            }

            group.visible = this.showAxes;
            scene.add(group);

            return group;
        },

        get_color: function(speed) {
            if (isNaN(speed)) {
                return [ 255, 0, 0 ];
            } // Rapid

            let intensity = speed / this.toolpath.maxSpeed;
            if (typeof speed == "undefined" || !this.showIntensity) {
                intensity = 1;
            }

            return [ 0, 255 * intensity, 127 * (1 - intensity) ];
        },

        draw_path: function(scene) {
            const geometry = new THREE.BufferGeometry();
            const material = new THREE.LineBasicMaterial({
                vertexColors: THREE.VertexColors,
                linewidth: 1.5
            });

            const positions = new THREE.Float32BufferAttribute(this.positions, 3);
            geometry.addAttribute("position", positions);

            let colors = [];
            for (let i = 0; i < this.speeds.length; i++) {
                const color = this.get_color(this.speeds[i]);
                Array.prototype.push.apply(colors, color);
            }

            colors = new THREE.Uint8BufferAttribute(colors, 3, true);
            geometry.addAttribute("color", colors);

            geometry.computeBoundingSphere();
            geometry.computeBoundingBox();

            const line = new THREE.Line(geometry, material);

            line.visible = this.showPath;
            scene.add(line);

            return line;
        },

        create_empty_geom: function() {
            const geometry = new THREE.BufferGeometry();
            geometry.addAttribute("position",
                new THREE.Float32BufferAttribute([], 3));
            return geometry;
        },

        create_bbox_geom: function(bbox) {
            const vertices = [];

            if (!bbox.isEmpty()) {
                // Top
                vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
                vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);

                // Bottom
                vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
                vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);

                // Sides
                vertices.push(bbox.min.x, bbox.min.y, bbox.min.z);
                vertices.push(bbox.min.x, bbox.max.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.min.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.max.y, bbox.min.z);
                vertices.push(bbox.max.x, bbox.min.y, bbox.max.z);
                vertices.push(bbox.max.x, bbox.max.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.min.y, bbox.max.z);
                vertices.push(bbox.min.x, bbox.max.y, bbox.max.z);
            }

            const geometry = new THREE.BufferGeometry();

            geometry.addAttribute("position",
                new THREE.Float32BufferAttribute(vertices, 3));

            return geometry;
        },

        draw_bbox: function(scene, bbox) {
            const geometry = this.create_bbox_geom(bbox);
            const material = new THREE.LineBasicMaterial({ color: 0xffffff });
            const line = new THREE.LineSegments(geometry, material);

            line.visible = this.showBBox;

            scene.add(line);

            return line;
        },

        draw_envelope: function(scene) {
            const geometry = this.create_empty_geom();
            const material = new THREE.LineBasicMaterial({ color: 0x00f7ff });
            const line = new THREE.LineSegments(geometry, material);

            line.visible = this.showBBox;

            scene.add(line);
            this.update_envelope(line);

            return line;
        },

        draw: function(scene) {
            // Lights
            scene.add(this.ambient);
            scene.add(this.lights);

            // Model
            this.pathView = this.draw_path(scene);
            this.surfaceMesh = this.draw_surface(scene, this.surfaceMaterial);
            this.workpieceMesh = this.draw_workpiece(scene, this.surfaceMaterial);
            this.update_surface_mode(this.surfaceMode);

            // Compute bounding box
            const bbox = this.get_model_bounds();

            // Tool, axes & bounds
            this.toolView = this.draw_tool(scene, bbox);
            this.axesView = this.draw_axes(scene, bbox);
            this.bboxView = this.draw_bbox(scene, bbox);
            this.envelopeView = this.draw_envelope(scene);
        },

        render: function() {
            window.requestAnimationFrame(this.render);

            if (typeof this.scene == "undefined") {
                return;
            }

            if (this.controls.update() || this.dirty) {
                this.dirty = false;
                this.renderer.render(this.scene, this.camera);
            }
        },

        get_model_bounds: function() {
            const bbox = new THREE.Box3(new THREE.Vector3(0, 0, 0),
                new THREE.Vector3(0.00001, 0.00001, 0.00001));

            function add(o) {
                if (typeof o != "undefined") {
                    const oBBox = new THREE.Box3();
                    oBBox.setFromObject(o);
                    bbox.union(oBBox);
                }
            }

            add(this.pathView);
            add(this.surfaceMesh);
            add(this.workpieceMesh);

            return bbox;
        },

        snap: function(view) {
            if (this.loading) {
                return;
            }

            if (view != this.snapView) {
                this.snapView = view;
                cookie.set("snap-view", view);
            }

            const bbox = this.get_model_bounds();
            this.controls.reset();
            bbox.getCenter(this.controls.target);
            this.update_view();

            // Compute new camera position
            const center = bbox.getCenter(new THREE.Vector3());
            const offset = new THREE.Vector3();

            switch (view) {
                case "isometric": offset.y -= 1; offset.z += 1; break;
                case "front": offset.y -= 1; break;
                case "back": offset.y += 1; break;
                case "left": offset.x -= 1; break;
                case "right": offset.x += 1; break;
                case "top": offset.z += 1; break;
                case "bottom": offset.z -= 1; break;
            }

            offset.normalize();

            // Initial camera position
            const position = new THREE.Vector3().copy(center).add(offset);
            this.camera.position.copy(position);
            this.camera.lookAt(center); // Get correct camera orientation

            const theta = this.camera.fov / 180 * Math.PI; // View angle
            const cameraLine = new THREE.Line3(center, position);
            const cameraUp = new THREE.Vector3()
                .copy(this.camera.up)
                .applyQuaternion(this.camera.quaternion);
            const cameraLeft = new THREE.Vector3()
                .copy(offset)
                .cross(cameraUp)
                .normalize();

            const corners = [
                new THREE.Vector3(bbox.min.x, bbox.min.y, bbox.min.z),
                new THREE.Vector3(bbox.min.x, bbox.min.y, bbox.max.z),
                new THREE.Vector3(bbox.min.x, bbox.max.y, bbox.min.z),
                new THREE.Vector3(bbox.min.x, bbox.max.y, bbox.max.z),
                new THREE.Vector3(bbox.max.x, bbox.min.y, bbox.min.z),
                new THREE.Vector3(bbox.max.x, bbox.min.y, bbox.max.z),
                new THREE.Vector3(bbox.max.x, bbox.max.y, bbox.min.z),
                new THREE.Vector3(bbox.max.x, bbox.max.y, bbox.max.z),
            ];

            let dist = this.camera.near; // Min camera dist

            for (let i = 0; i < corners.length; i++) {
                // Project on to camera line
                const p1 = cameraLine.closestPointToPoint(corners[i], false, new THREE.Vector3());

                // Compute distance from projection to center
                let d = p1.distanceTo(center);
                if (cameraLine.closestPointToPointParameter(p1, false) < 0) {
                    d = -d;
                }

                // Compute up line
                const up = new THREE.Line3(p1, new THREE.Vector3().copy(p1).add(cameraUp));

                // Project on to up line
                const p2 = up.closestPointToPoint(corners[i], false, new THREE.Vector3());

                // Compute length
                let l = p1.distanceTo(p2);

                // Update min camera distance
                dist = Math.max(dist, d + l / Math.tan(theta / 2));

                // Compute left line
                const left = new THREE.Line3(p1, new THREE.Vector3().copy(p1).add(cameraLeft));

                // Project on to left line
                const p3 = left.closestPointToPoint(corners[i], false, new THREE.Vector3());

                // Compute length
                l = p1.distanceTo(p3);

                // Update min camera distance
                dist = Math.max(dist, d + l / Math.tan(theta / 2) / this.camera.aspect);
            }

            this.camera.position.copy(offset.multiplyScalar(dist * 1.2).add(center));
        }
    },

    mixins: [ require("./axis-vars") ]
};
