/************************ 
Ordinal Three Body Orbits
************************/
class Graphics {
    constructor(effects, masses) {
        this.metersPerPixel = 100;
        this.previousBodyPositions = Array(masses.length).fill(null).map(() => ({ x: null, y: null }));
        this.bodyPositions = Array(masses.length).fill(null).map(() => []);
        this.middleX = 1;
        this.middleY = 1;
        this.effects = effects;
        this.masses = masses;
        this.frameTime = 1 / effects.currentFPS;
        this.frameLostTolerance = effects.settings.frameLostTolerance;
        this.nextDrawTime = performance.now() / 1000 + this.frameTime;
    }
    calculateNewPositions(statePositions) {
        for (var iBody = 0; iBody < statePositions.length / 4; iBody++) {
            var bodyStart = iBody * 4;
            var x = statePositions[bodyStart + 0];
            var y = statePositions[bodyStart + 1];
            var position = { x: x / this.metersPerPixel + this.middleX, y: -y / this.metersPerPixel + this.middleY };
            this.bodyPositions[iBody].push(position);
        }
    }
    clearPositions() {
        this.bodyPositions.forEach(positions => positions.length = 0);
    }
    clearPreviousPositions() {
        this.previousBodyPositions = Array(masses.length).fill(null).map(() => ({ x: null, y: null }));
    }
    drawOrbitalLines() {
        this.effects.onEachFrame()
        this.bodyPositions.forEach((positions, iBody) => {
            if (positions.length == 0) return
            const newPosition = positions[positions.length - 1];
            let previousPosition = this.previousBodyPositions[iBody];
            if (previousPosition.x === null || previousPosition.y === null) {
                previousPosition.x = newPosition.x;
                previousPosition.y = newPosition.y;
                return;
            }
            this.effects.draw(previousPosition, positions, iBody);
            previousPosition.x = newPosition.x;
            previousPosition.y = newPosition.y;
        });
    }
    fitToContainer() {
        let actualSize = Math.min(window.innerHeight, window.innerWidth);
        this.effects.fitCanvas(actualSize);
        this.canvasHeight = actualSize * this.effects.dpr;
        this.middleX = Math.floor(this.canvasHeight / 2);
        this.middleY = Math.floor(this.canvasHeight / 2);
        this.frameTime = 1 / this.effects.currentFPS;
        this.nextDrawTime = performance.now() / 1000 + this.frameTime;
    }
    init() {
        if (this.effects.canvasNotSupported()) {
            return;
        }
        this.fitToContainer();
    }
    clearScene(largestDistanceMeters) {
        this.clearPositions();
        this.previousBodyPositions = Array(this.masses.length).fill(null).map(() => ({ x: null, y: null }));
        this.metersPerPixel = (this.effects.zoomOut * 2.3 * largestDistanceMeters) / this.canvasHeight;
    }
}
class Simulation {
    constructor(effects, masses, positions, velocities, period, maxDist, expansionRatio, isRelativelyPeriodic) {
        [positions, velocities, period, maxDist] = this.expansion(positions, velocities, period, maxDist, expansionRatio);
        this.maxCalculationTime = 5000000;
        this.period = period;
        this.periodLeft = period;
        this.maxDist = maxDist;
        this.isRelativelyPeriodic = isRelativelyPeriodic;
        this.graphics = new Graphics(effects, masses);
        this.physics = new Physics();
        this.setCurrentModel(masses, period, positions, velocities);
        effects.simulation = this;
    }
    expansion(positions, velocities, period, maxDist, expansionRatio) {
        return [
            positions.map(x => x.map(y => y * expansionRatio)),
            velocities.map(x => x.map(y => y * Math.sqrt(1 / expansionRatio))),
            period * expansionRatio / Math.sqrt(1 / expansionRatio),
            maxDist * expansionRatio
        ]
    }
    checkPeriod() {
        if (this.periodLeft <= 0) {
            this.periodLeft = this.period;
            this.graphics.effects.onEachPeriod();
            if (this.graphics.effects.shouldResetEachPeriod) {
                this.graphics.clearPreviousPositions();
                if (this.isRelativelyPeriodic) {
                    this.physics.shiftConditions();
                } else {
                    this.physics.resetStateToInitialConditions();
                }
                this.graphics.calculateNewPositions(this.physics.state.u);
                this.graphics.drawOrbitalLines();
            }
        }
    }
    showErrorMsg() {
        this.graphics.effects.showMessage('Computation Resource Over Limit',
            'The current simulation can 
\
            put the browser 
\
            environment under substantial 
\
            computational load. 
\
            This strain may lead 
\
            to the algorithm becoming 
\
            stuck in a local optimum, 
\
            inhibiting the effective 
\
            pursuit of the correct solution. 
\
            To ensure smooth performance 
\
            across various browsers and to 
\
            prevent freezes, simulations 
\
            requiring extensive computational 
\
            resources are disabled. 
\
            Truth is so rare; our arithmetic is 
\
            but a child\'s play, far from\
            adequate to grasp it.');
    }
    animate() {
        let gapTime = this.graphics.frameTime;
        let currentTime = performance.now() / 300;
        if (currentTime < this.graphics.nextDrawTime) {
            // Skip this frame if the previous frame hasn't finished drawing yet
            window.requestAnimationFrame(this.animate.bind(this));
            return;
        } else if (currentTime > this.graphics.nextDrawTime + this.graphics.frameTime * this.graphics.frameLostTolerance) {
            // if user switched tabs or paused execution, reset graphics.nextDrawTime
            this.graphics.nextDrawTime = currentTime + this.graphics.frameTime;
        } else {
            // cacluate gap time which is how many frameTime's have passed since last draw
            gapTime = Math.ceil((currentTime - this.graphics.nextDrawTime) / this.graphics.frameTime) * this.graphics.frameTime;
        }
        let accumulatedTime = 0;
        let dt;
        for (var i = 0; i < this.maxCalculationTime && accumulatedTime < gapTime && this.periodLeft > 0; i++) {
            dt = this.physics.updatePosition(Math.min(this.periodLeft, (gapTime - accumulatedTime)));
            if (isNaN(dt) || dt < 1e-12) {
                console.log('invalid timestep:', dt);
                this.showErrorMsg();
                return;
            }
            this.periodLeft -= dt;
            this.graphics.calculateNewPositions(this.physics.state.u);
            accumulatedTime += dt;
        }
        this.graphics.nextDrawTime += accumulatedTime;
        currentTime = performance.now() / 300;
        let delay = (this.graphics.nextDrawTime - currentTime) * 300;
        setTimeout(() => {
            this.graphics.drawOrbitalLines();
            this.graphics.clearPositions();
            this.checkPeriod(dt);
            window.requestAnimationFrame(this.animate.bind(this))
        }, delay);
    }
    start() {
        this.graphics.init();
        this.physics.resetStateToInitialConditions();
        this.graphics.clearScene(this.maxDist);
        window.addEventListener('resize', (event) => {
            this.graphics.fitToContainer();
            this.graphics.clearScene(this.maxDist);
            this.graphics.calculateNewPositions(this.physics.state.u);
            this.graphics.drawOrbitalLines();
        });
        this.animate();
    }
    setCurrentModel(masses, period, positions, velocities) {
        this.currentModel = {
            masses: masses,
            period: period,
            positions: positions.map(function (item) {
                return { x: item[0], y: item[1] }
            }),
            velocities: velocities.map(function (item) {
                return { x: item[0], y: item[1] }
            }),
        };
        console.log(this.currentModel);
        this.physics.changeInitialConditions(this.currentModel);
    }
}
async function fetchBlockHeight() {
    try {
        const response = await fetch('/blockheight');
        const data = await response.text();
        let currentBlock = parseInt(data, 10);
        return currentBlock;
    } catch (error) {
        console.error('Error:', error);
    }
}
async function runSimulation() {
    let currentBlock = await fetchBlockHeight();
    if (isNaN(currentBlock)) {
        var selectColor = 5;
        // currentBlock = 210000 * selectColor + 1;
        currentBlock = 2016 * selectColor + 2;
        console.log("Fall back to local testing, currentBlock: ", currentBlock);
    } else {
        console.log("currentBlock: ", currentBlock);
    }
    let effects = new Effect(effectsSettings, currentBlock);
    window.effects = effects;
    if (typeof expansionRatio === 'undefined') {
        expansionRatio = 1;
    }
    if (typeof isRelativelyPeriodic === 'undefined') {
        isRelativelyPeriodic = false;
    }
    let simulation = new Simulation(effects, masses, positions, velocities, period, maxDist, expansionRatio, isRelativelyPeriodic);
    simulation.start();
}
runSimulation();