///////////////////////////////
///
///        This demo uses a Lerp() function to
///        simulate a fake perspective using 
///        a single focal point. There is no
///        FOV but the Z position of the focal
///        point to adjust the perspective
///        effect.
///
///////////////////////////////

const canvas = document.getElementById("pageCanvas");
const fps = 60;

let ctx = canvas.getContext("2d", { alpha: false });
let mouse = { 'x': 0, 'y': 0 };
let center = { 'x': canvas.width / 2, 'y': canvas.height / 2 };

let vertices2D = [];            // List of the 2D points that will have to be drawn
let object3DList = [];            // List of 3D objects to translate and draw
let missilesList = [];
let starList = [];
let stars2D = [];                // List of every 2D-translated star to draw


const missileSpeed = 2.5;
const starAmount = 100;
let shipSpeed = 0.3;
let pointSize = 3;            // Size of a single star
let scaleFactor = 100;        // Used to make every model bigger
let depth = 6 * scaleFactor;     // depth is the maximum positive value Z can have. Increase this value decreases the fake-fov
let drawStyle = "#ecf0f1";        // Pencil color 
let lineWidth = 0.2;            // Thickness of the plane wires
let pitch = 15;                    // Amount of "wind" pitching when the user is not moving the plane
let turbulenceReduction = 800;    // Increasing this value reduces the "wind pitching" speed

const shootCooldown = 8 / fps;    // Increasing that value decreases the firerate
let shootTimeout = null;

let isMouseInside = false;

///////////////////////////////
///
///        MODELS 
///
///////////////////////////////

// Default polygon to use as a fallback in case
// no models is loaded
let shipObject3D = {
        "v": [    // vertices
            { x: 0, y: 1, z: 0 },    //A
            { x: 1, y: -1, z: 0 },    //B
            { x: -1, y: -1, z: 0 },    //C
            { x: 0, y: -1, z: 1 },    //D
        ],
    
        "f": [    // faces
            [0, 1, 2],
            [0, 3, 1],
            [0, 2, 1],
            [1, 2, 3]
        ],
    
        "position": [
            { x: 0, y: 0, z: 0 }
        ]
    }

// Unless loaded otherwise, the ship missile is
// nothing but a line
let shipMissile3D = {
        "v": [
            { x: 0, y: 0, z: -0.3 },    //A
            { x: 0, y: 0, z: 0 },    //B
        ],
    
        "f": [
            [0, 1],
        ],
    
        "position": [
            { x: 0, y: 0, z: 0 }
        ]
    }


///////////////////////////////
///
///        GAME OBJECTS
///
///////////////////////////////

const missile = class {
        constructor(x, y, z) {
        
                let missileInstance = clone(shipMissile3D);
                this.v = missileInstance.v;
                this.f = missileInstance.f
                this.position = missileInstance.position;
        
                this.position.x = x;
                this.position.y = y;
                this.position.z = z;
        
            }
    };

const star = class {
        constructor(x, y, z) {
                this.x = x;
                this.y = y;
                this.z = z;
            }
    };


///////////////////////////////
///
///        HIGH LEVEL LOOP
///        This function will be called 
///        every 1/fps second
///
///////////////////////////////

function tick() {
        clear();        // Clears the canvas 
        readjust();        // Resizes the canvas based on window size 
        refreshInput();
        calculate();    // Updates 3D objects position and projects 3D objects to 2D layer 
        draw();            // Draws the 2D layer
    }


///////////////////////////////
///
///        LOOP ELEMENTS
///
///////////////////////////////

// Resets every drawing list
function clear() {
        stars2D = [];
        vertices2D = [];
        object3DList = [];
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

function refreshInput() {
        if (!isMouseInside) {
                resetMouse(4 / fps);
            }
    }

// Moves the ship based on the input + turbulence effect
function calculate() {
    
        ///////////
        ///    SHIP POSITION & ROTATION
        ///////////
    
        // Fake "wind turbulence" effect
        let angleDeg = Math.atan2(center.y - mouse.y, center.x - mouse.x) * 180 / Math.PI;
        const wannaPitch = angleDeg + 90;
        const turbulence = Math.cos(performance.now() / turbulenceReduction) * 10;
        pitch = fastLerp(pitch, (wannaPitch + turbulence), 3 / fps);
    
        // Rotating the ship is made by copying it entirely and rotating every of its points in the process
        let pitchedShip = {
                'v': [],
                'f': [],
                'position': []
            };
    
        // Rotating every point
        for (let i = 0; i < shipObject3D["v"].length; i++) {
                const point = shipObject3D["v"][i];
                const rotated = rotatePoint(0, 0, point.x, point.y, pitch);
                pitchedShip["v"][i] = { x: rotated.x, y: rotated.y, z: point.z };
            }
    
        // Copying every face
        for (let i = 0; i < shipObject3D["f"].length; i++) {
                const faceSet = shipObject3D["f"][i];
                pitchedShip["f"][i] = faceSet;
            }
    
        pitchedShip.position.x = mouse.x;
        pitchedShip.position.y = mouse.y;
        pitchedShip.position.z = 0;    // The ship is always in the front (depth of 0)
    
        // Adding the ship to the queue of 3D objects to be drawn
        object3DList.push(pitchedShip);
    
        ///////////
        /// MISSILES
        ///////////    
        let newList = [];
        for (let i = 0; i < missilesList.length; i++) {
                let thisMissile = missilesList[i];
        
                // We make each missile advance by a bit
                thisMissile.position.z += missileSpeed / fps;
        
                // Unless they're too far, we add them to draw
                if (thisMissile.position.z < 1) {
                        newList.push(thisMissile);
                        object3DList.push(thisMissile);
                    }
        
            }
        missilesList = newList;
    
        ///////////
        ///    STARS
        ///////////
        for (let i = 0; i < starList.length; i++) {
                starList[i].z -= shipSpeed / fps;
        
                if (starList[i].z < 0) {
                        let nova = { x: 0, y: 0 };
                        // Either they're rushing for the top/bottom, or for the left/right
                        if (Math.random() > 0.5) {
                                nova.x = (Math.floor(Math.random() * 2) * canvas.width);
                                nova.y = Math.random() * canvas.height;
                            }
                        else {
                                nova.x = Math.random() * canvas.width;
                                nova.y = (Math.floor(Math.random() * 2) * canvas.height);
                            }
                        starList[i].x = nova.x;
                        starList[i].y = nova.y;
                        starList[i].z = 1;
                    }
        
            }
    
        ///////////
        ///    PROJECTION
        /// This should be the LAST STEP
        /// Once every 3D object updated,
        /// we can project their vertices
        /// on a 2D plane.
        ///////////
    
        for (let h = 0; h < object3DList.length; h++) {
                const object3D = object3DList[h];
                let object2D = [];
        
                // Each vertex (.v) gets projected in a 2D space
                for (let i = 0; i < object3D.v.length; i++) {
                        const thisVertix = object3D.v[i];
                        object2D.push(
                            {    // The higher the Z value is, the nearer we bring the X and Y positions from the center of the screen using a Lerp
                                    x: fastLerp(object3D.position.x + thisVertix.x * scaleFactor, center.x, (thisVertix.z * scaleFactor) / depth + object3D.position.z),
                                    y: fastLerp(object3D.position.y - thisVertix.y * scaleFactor, center.y, (thisVertix.z * scaleFactor) / depth + object3D.position.z)
                                    // This is a perfectly working way to simulate perspective [as long as we don't ever have to move the camera]
                                }
                        );
                    }
        
                // The projected object gets added to the queue of objects to draw 
                vertices2D.push(object2D);
            }
        for (let i = 0; i < starList.length; i++) {
                thisStar = starList[i];
                // Same projection calculation goes on for the stars, but a bit simpler (no need to worry for scale)
                stars2D.push(
                    {
                            x: fastLerp(thisStar.x, center.x, (thisStar.z)),
                            y: fastLerp(thisStar.y, center.y, (thisStar.z)),
                            size: Math.sqrt(thisStar.z)
                        }
                )
            }
    }

function draw() {
    
        // Drawing stars
        for (let i = 0; i < stars2D.length; i++) {
                const thisPoint = stars2D[i];
                thisPointSize = (1 - thisPoint.size) * pointSize;
        
                ctx.beginPath();
                ctx.arc(
                    thisPoint.x,
                    thisPoint.y,
                    thisPointSize, 0, 2 * Math.PI, false);
                ctx.fillStyle = drawStyle;
                ctx.fill();
            }
    
        // Drawing every 3D object
        for (let h = 0; h < object3DList.length; h++) {
                const object3D = object3DList[h];
        
                for (let i = 0; i < object3D.f.length; i++) {
                        const thisFace = object3D.f[i];
            
                        // As we kept the index of the points intact during the projection, we can still
                        // connect the vertices of every face as they would have been connected in a 3D
                        // space
                        for (let j = 0; j < thisFace.length; j++) {
                
                                ctx.beginPath();
                                ctx.strokeStyle = drawStyle;
                
                                // First point ever doesn't have to be connected to the previous one
                                const firstPoint = vertices2D[h][thisFace[j]];
                                const secondPoint =
                                    (j == thisFace.length - 1 ?
                                        vertices2D[h][thisFace[0]]
                                        : vertices2D[h][thisFace[j + 1]]
                                    );
                
                                ctx.moveTo(firstPoint.x, firstPoint.y);
                                ctx.lineTo(secondPoint.x, secondPoint.y);
                
                                ctx.lineWidth = lineWidth;
                                ctx.stroke();
                            }
                    }
            }
    }



///////////////////////////////
///
///        GAME FUNCTIONS
///
///////////////////////////////

// Loads a model for the ship
function setObject(json_string) {
        shipObject3D = JSON.parse(json_string);
        shipObject3D["position"] = { x: 0, y: 0, z: 0 };
    }

// Loads a model for the missiles
function setMissile(json_string) {
        shipMissile3D = JSON.parse(json_string);
        shipMissile3D["position"] = { x: 0, y: 0, z: 0 };
    }

// Resizes the canvas depending on the screen / page size
function readjust() {
        let ow = [canvas.width, canvas.height];
        canvas.width = canvas.parentElement.clientWidth;
        canvas.height = canvas.parentElement.clientHeight;
        center = { 'x': canvas.width / 2, 'y': canvas.height / 2 };
        if (ow[0] != canvas.width || ow[1] != canvas.height) {
                starList = [];
                for (let i = 0; i < starAmount; i++) {
                        let nova = { x: 0, y: 0 };
                        if (Math.random() > 0.5) {
                                nova.x = (Math.round(Math.random()) * canvas.width);
                                nova.y = Math.random() * canvas.height;
                            }
                        else {
                                nova.x = Math.random() * canvas.width;
                                nova.y = (Math.round(Math.random()) * canvas.height);
                            }
                        starList.push(new star(
                            nova.x,
                            nova.y,
                            Math.random()));
                    }
            }
    }

// Creates 4 missiles and adds them to the missile list, then triggers shoot timeout
function shoot() {
    
        let rotated = rotatePoint(mouse.x, mouse.y, mouse.x + 20, mouse.y, -pitch);
        missilesList.push(new missile(rotated.x, rotated.y, 0));
    
        rotated = rotatePoint(mouse.x, mouse.y, mouse.x + 13, mouse.y, -pitch);
        missilesList.push(new missile(rotated.x, rotated.y, 0));
    
        rotated = rotatePoint(mouse.x, mouse.y, mouse.x - 13, mouse.y, -pitch);
        missilesList.push(new missile(rotated.x, rotated.y, 0));
    
        rotated = rotatePoint(mouse.x, mouse.y, mouse.x - 20, mouse.y, -pitch);
        missilesList.push(new missile(rotated.x, rotated.y, 0));
    
        shootTimeout = setTimeout(shoot, shootCooldown * 1000);
    }

// The player stops shooting
function clearShoot() {
        clearTimeout(shootTimeout);
    }

// Changes pencil color 
function setDrawStyle(str) {
        drawStyle = str;
    }

// Calculate mouse position with scrolling
function updateMousePos(canvas, evt) {
        const doc = document.documentElement;
        const left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
        // const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
        const top = 0;
        const posX = evt.clientX;
        const posY = evt.clientY;
        let x = posX - canvas.offsetLeft + left;
        let y = posY - canvas.offsetTop + top;
    
        mouse = { "x": x, "y": y };
        isMouseInside = true;
    }

function resetMouse(delta) {
        const targetX = canvas.width / 2 + Math.sin(new Date().getTime() * 0.0001) * (canvas.width / 5);
        const targetY = canvas.height * 2 / 3;
        mouse.x = fastLerp(mouse.x, targetX, delta);
        mouse.y = fastLerp(mouse.y, targetY, delta);
    }

function mouseLeft() {
        isMouseInside = false;
        clearShoot();
    }


///////////////////////////////
///
///        UTILITY FUNCTIONS
///
///////////////////////////////

function rotatePoint(cx, cy, x, y, angle) {
        let radians = (Math.PI / 180) * angle,
            cos = Math.cos(radians),
            sin = Math.sin(radians),
            nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
            ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
        return { x: nx, y: ny };
    }

// Basic lerp implementation
function fastLerp(value1, value2, position) {
        return (value1 * (1 - position) + value2 * (position));
    }

// Utility function to clone objects
function clone(object) {
        return JSON.parse(JSON.stringify(object));
    }


///////////////////////////////
///
///        GAME START
///
///////////////////////////////

resetMouse(1);
const game = setInterval(tick, (1 / fps) * 1000);


///////////////////////////////
///                         ///
/// Made by Louve Hurlante  ///
/// [GPL3 License]          ///
///                         ///
///                   / V\  ///
///                  / `  / ///
///                 <<   |  ///
///                 /    |  ///
///               /      |  ///
///             /        |  ///
///           /    \  \ /   ///
///          (      ) | |   ///
///  ________|   _/_  | |   ///
///<__________\______)\__)  ///
///                         ///
///////////////////////////////