// use arrow functions always

const canvasHeight = 360;
const canvasWidth = Math.round(canvasHeight * 16 / 9);

// positive = audio behind visuals
// negative = visuals behind audio
// const audioDelay = 0.200; // seconds, for bluetooth headphones
const audioDelay = 0.0;

window.addEventListener('load', () => {

    let audioContext = new (window.AudioContext || window.webkitAudioContext)();
    let analyser = audioContext.createAnalyser();

    // must be a power of 2 between 32 and 32768
    // bigger value means more smoother output
    analyser.fftSize = 4096;

    const analyserWindowLength = analyser.fftSize / audioContext.sampleRate;
    const totalAudioDelay = audioDelay + analyserWindowLength;

    let delayNode = audioContext.createDelay();
    delayNode.delayTime.value = Math.abs(totalAudioDelay);

    // gain node
    const sourceNode = audioContext.createGain();
    setupAudio(audioContext, sourceNode);

    sourceNode.connect(delayNode);
    if (totalAudioDelay < 0) {
        sourceNode.connect(audioContext.destination);
        delayNode.connect(analyser);
    } else {
        sourceNode.connect(analyser);
        delayNode.connect(audioContext.destination);
    }

    let bufferLength = analyser.frequencyBinCount;
    let FFTdata = new Uint8Array(bufferLength);



    // create canvas
    const canvas = document.createElement('canvas');
    document.body.appendChild(canvas);

    // set canvas size
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;

    const resizeCanvas = () => {
        // calculate canvs.style size based on document.body
        // fit to window, preserve aspect ratio
        const canvasAspectRatio = canvasWidth / canvasHeight;
        const windowAspectRatio = document.body.clientWidth / document.body.clientHeight;
        if (windowAspectRatio > canvasAspectRatio) {
            canvas.style.width = document.body.clientHeight * canvasAspectRatio + 'px';
            canvas.style.height = document.body.clientHeight + 'px';
        } else {
            canvas.style.width = document.body.clientWidth + 'px';
            canvas.style.height = document.body.clientWidth / canvasAspectRatio + 'px';
        }
    };

    window.addEventListener('resize', resizeCanvas);
    resizeCanvas();


    // get gl context
    const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
    if (!gl) {
        alert('Your browser does not support WebGL.');
    }

    const createShader = (gl, type, source) => {
        const shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
            gl.deleteShader(shader);
            return null;
        }
        return shader;
    };

    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(program));
    }
    gl.useProgram(program);

    // Create a quad for drawing
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [-1, -1, 1, -1, -1, 1, 1, 1];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

    gl.viewport(0, 0, canvas.width, canvas.height); // use the whole canvas
    gl.clearColor(0, 0, 0, 1); // use black when clearing

    const resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution');
    gl.uniform2f(resolutionUniformLocation, canvas.width, canvas.height);

    const timeUniformLocation = gl.getUniformLocation(program, 'u_time');
    const audioUniformLocation = gl.getUniformLocation(program, 'u_audio');
    const cameraPosUniformLocation = gl.getUniformLocation(program, 'u_camera_pos');
    const cameraDirUniformLocation = gl.getUniformLocation(program, 'u_camera_dir');
    const cameraZoomUniformLocation = gl.getUniformLocation(program, 'u_camera_zoom');
    const lightDirUniformLocation = gl.getUniformLocation(program, 'u_light_dir');
    const foldADirUniformLocation = gl.getUniformLocation(program, 'u_fold_a');
    const foldBDirUniformLocation = gl.getUniformLocation(program, 'u_fold_b');
    const translateUniformLocation = gl.getUniformLocation(program, 'u_translate');

    const calculateLevel = (minFreq, maxFreq) => {
        let binSize = audioContext.sampleRate / analyser.fftSize; // Frequency of each bin
        let startBin = Math.round(minFreq / binSize);
        let endBin = Math.round(maxFreq / binSize);
        let sum = 0;

        for (let i = startBin; i < endBin; i++) {
            // sum += FFTdata[i] * Math.log(i + 1) / 3;
            sum += FFTdata[i];
        }

        let level = sum / (endBin - startBin); // Getting the average

        return level / 255.0;
    };

    const calculateTreble = (minFreq, maxFreq) => {
        let binSize = audioContext.sampleRate / analyser.fftSize; // Frequency of each bin
        let startBin = Math.round(minFreq / binSize);
        let endBin = Math.round(maxFreq / binSize);
        let sum = 0;

        for (let i = startBin; i < endBin; i++) {
            sum += FFTdata[i] * Math.log2(i + 1) / 3;
            // sum += FFTdata[i];
        }

        let level = sum / (endBin - startBin); // Getting the average

        return level / 255.0;
    };

    const canvas2D = document.createElement('canvas');
    // document.body.appendChild(canvas2D);

    canvas2D.width = canvasWidth;
    canvas2D.height = canvasHeight;

    const canvas2DTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, canvas2DTexture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    const canvas2DTextureUniformLocation = gl.getUniformLocation(program, 'u_canvas_texture');
    const updateCanvas2DTexture = () => {
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, canvas2DTexture);
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas2D);

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, canvas2DTexture);
        gl.uniform1i(canvas2DTextureUniformLocation, 0);
    }

    const updateCanvas2D = (time) => {
        const ctx = canvas2D.getContext('2d');
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, canvas2D.width, canvas2D.height);
        // ctx.translate(canvas2D.width / 2, canvas2D.height / 2);
        const scale = canvas2D.height / 360;
        ctx.scale(scale, scale);
        const width = canvas2D.width / scale;
        const height = canvas2D.height / scale;
        drawCanvas2D(ctx, width, height, time);
    }

    const cameraPosition = {
        x: 0,
        y: 0,
        z: -4.0
    };

    const cameraDirection = {
        x: 0,
        y: 0,
        z: 1
    }

    const cameraAngles = {
        yaw: 0,
        pitch: 0
    }

    let cameraZoom = 1.0;

    const lightDirection = new UnitVectorParameter(0, 1, -1);
    const foldADirection = new UnitVectorParameter(1, 1, 0);
    const foldBDirection = new UnitVectorParameter(0, 1, 1);
    const translateDirection = {
        x: -10.0,
        y: 3.0,
        z: 0.0
    };

    const keysDown = {};

    const animate = () => {
        const time = performance.now() / 1000;
        // let time = getAudioTime + Mat
        // if (audioDelay < 0) {
        //     // visuals post-poned
        //     time -= audioDelay;
        // }

        updateCanvas2D(time);
        updateCanvas2DTexture();

        // Movement

        // 3d camera movement
        // w = forward
        // s = backward
        // a = left
        // d = right
        // q = up
        // e = down
        if (keysDown['w']) {
            cameraPosition.z += 0.1 * Math.cos(cameraAngles.yaw);
            cameraPosition.x += 0.1 * Math.sin(cameraAngles.yaw);
        }
        if (keysDown['s']) {
            cameraPosition.z -= 0.1 * Math.cos(cameraAngles.yaw);
            cameraPosition.x -= 0.1 * Math.sin(cameraAngles.yaw);
        }
        if (keysDown['a']) {
            cameraPosition.x += 0.1 * Math.cos(cameraAngles.yaw);
            cameraPosition.z -= 0.1 * Math.sin(cameraAngles.yaw);
        }
        if (keysDown['d']) {
            cameraPosition.x -= 0.1 * Math.cos(cameraAngles.yaw);
            cameraPosition.z += 0.1 * Math.sin(cameraAngles.yaw);
        }
        if (keysDown['e']) {
            cameraPosition.y += 0.1;
        }
        if (keysDown['q']) {
            cameraPosition.y -= 0.1;
        }

        // Music FFT
        analyser.getByteFrequencyData(FFTdata);
        const bassLevel = calculateLevel(20, 200);
        const midLevel = calculateLevel(300, 1000) * 1.5;
        const trebleLevel = calculateTreble(7000, 20000);
        const audioAnalysis = [bassLevel, midLevel, trebleLevel];

        // Update uniforms
        gl.uniform1f(timeUniformLocation, time);
        gl.uniform1fv(audioUniformLocation, audioAnalysis);
        gl.uniform3f(cameraPosUniformLocation, cameraPosition.x, cameraPosition.y, cameraPosition.z);
        gl.uniform3f(cameraDirUniformLocation, cameraDirection.x, cameraDirection.y, cameraDirection.z);
        gl.uniform1f(cameraZoomUniformLocation, cameraZoom);
        gl.uniform3f(lightDirUniformLocation, lightDirection.x, lightDirection.y, lightDirection.z);
        gl.uniform3f(foldADirUniformLocation, foldADirection.x, foldADirection.y, foldADirection.z);
        gl.uniform3f(foldBDirUniformLocation, foldBDirection.x, foldBDirection.y, foldBDirection.z);
        gl.uniform3f(translateUniformLocation, translateDirection.x, translateDirection.y, translateDirection.z);

        // Draw
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        // Loop
        requestAnimationFrame(animate);
    };

    animate();


    // Parameters

    const parameterModes = {
        1: 'cameraDirection',
        2: 'cameraXY',
        3: 'cameraZZoom',
        4: 'lightDirection',
        5: 'foldADirection',
        6: 'foldBDirection',
        7: 'translateXY',
        8: 'translateXZ',
    }
    let parameterMode = 1;

    canvas.addEventListener('mousemove', (event) => {
        // deactivate if pointer is in normal mode
        if (document.pointerLockElement === null) {
            return;
        }

        // 1 = cameraDirection
        if (parameterMode === 1) {
            cameraAngles.yaw -= event.movementX / 500;
            cameraAngles.pitch -= event.movementY / 500;

            // clamp pitch
            cameraAngles.pitch = Math.max(cameraAngles.pitch, -Math.PI / 2);
            cameraAngles.pitch = Math.min(cameraAngles.pitch, Math.PI / 2);

            // calculate cameraDirection
            cameraDirection.x = Math.sin(cameraAngles.yaw) * Math.cos(cameraAngles.pitch);
            cameraDirection.y = Math.sin(cameraAngles.pitch);
            cameraDirection.z = Math.cos(cameraAngles.yaw) * Math.cos(cameraAngles.pitch);
        }

        // 2 = cameraXY
        if (parameterMode === 2) {
            cameraPosition.x -= event.movementX / 300 * Math.cos(cameraAngles.yaw);
            cameraPosition.z += event.movementX / 300 * Math.sin(cameraAngles.yaw);
            cameraPosition.y -= event.movementY / 300 * Math.cos(cameraAngles.pitch);
            cameraPosition.x += event.movementY / 300 * Math.sin(cameraAngles.pitch) * Math.sin(cameraAngles.yaw);
            cameraPosition.z += event.movementY / 300 * Math.sin(cameraAngles.pitch) * Math.cos(cameraAngles.yaw);
        }

        // 3 = cameraZZoom
        if (parameterMode === 3) {
            cameraPosition.x -= event.movementY / 300 * cameraDirection.x;
            cameraPosition.y -= event.movementY / 300 * cameraDirection.y;
            cameraPosition.z -= event.movementY / 300 * cameraDirection.z;

            cameraZoom *= Math.exp(event.movementX / 1000);
        }

        // 4 = lightDirection
        if (parameterMode === 4) {
            lightDirection.rotateA(-event.movementY / 300);
            lightDirection.rotateB(event.movementX / 300);
        }

        // 5 = foldADirection
        if (parameterMode === 5) {
            foldADirection.rotateA(-event.movementY / 1000);
            foldADirection.rotateB(event.movementX / 1000);
        }

        // 6 = foldBDirection
        if (parameterMode === 6) {
            foldBDirection.rotateA(-event.movementY / 1000);
            foldBDirection.rotateB(event.movementX / 1000);
        }

        // 7 = translateXY
        if (parameterMode === 7) {
            translateDirection.x -= event.movementX / 100;
            translateDirection.y += event.movementY / 100;
        }

        // 8 = translateXZ
        if (parameterMode === 8) {
            translateDirection.x -= event.movementX / 100;
            translateDirection.z += event.movementY / 100;
        }

    });

    // click canvas = pointer lock
    canvas.addEventListener('click', () => {
        canvas.requestPointerLock();
    });

    window.addEventListener('keydown', (event) => {
        keysDown[event.key] = true;

        // arrow left/right +/- 5 seconds
        if (event.key === 'ArrowLeft') {
            setAudioTime(getAudioTime() - 5);
        }
        if (event.key === 'ArrowRight') {
            setAudioTime(getAudioTime() + 5);
        }
        // shift + arrows = 1 min
        if (event.key === 'ArrowLeft' && event.shiftKey) {
            setAudioTime(getAudioTime() - 60);
        }
        if (event.key === 'ArrowRight' && event.shiftKey) {
            setAudioTime(getAudioTime() + 60);
        }
        // spacebar = play/pause
        if (event.key === ' ') {
            if (isAudioPaused()) {
                playAudio();
            } else {
                pauseAudio();
            }
        }
        // 0–9 = parameter modes
        if (event.key >= '0' && event.key <= '9') {
            parameterMode = parseInt(event.key);
            console.log('parameter mode:', parameterModes[parameterMode]);
        }
    });

    window.addEventListener('keyup', (event) => {
        keysDown[event.key] = false;
    });
});

const vertexShaderSource = `
attribute vec4 a_position;
void main() {
    gl_Position = a_position;
}
`;

class UnitVectorParameter {
    constructor(x, y, z) {
        // normalize
        const length = Math.sqrt(x * x + y * y + z * z);
        x /= length;
        y /= length;
        z /= length;
        this.value = { x, y, z };
        // generate a vector that is perpendicular to value
        // this is used to generate a random vector perpendicular to value

        const randomUnitVector = () => {
            const a = Math.random() * 2 * Math.PI;
            const z = Math.random() * 2 - 1;
            const r = Math.sqrt(1 - z * z);
            const x = r * Math.cos(a);
            const y = r * Math.sin(a);
            return { x, y, z };
        }

        const cross = (a, b) => {
            return {
                x: a.y * b.z - a.z * b.y,
                y: a.z * b.x - a.x * b.z,
                z: a.x * b.y - a.y * b.x
            }
        }

        while (true) {
            const randomVector = randomUnitVector();
            const crossProduct = cross(this.value, randomVector);
            if (crossProduct.x !== 0 || crossProduct.y !== 0 || crossProduct.z !== 0) {
                this.perpendicularA = crossProduct;
                break;
            }
        }

        this.perpendicularB = cross(this.value, this.perpendicularA);
    }

    rotateA(angle) {
        // rotate value and perpendicularB around perpendicularA
        const cos = Math.cos(angle);
        const sin = Math.sin(angle);
        const x = this.value.x * cos + this.perpendicularB.x * sin;
        const y = this.value.y * cos + this.perpendicularB.y * sin;
        const z = this.value.z * cos + this.perpendicularB.z * sin;
        this.perpendicularB.x = -this.value.x * sin + this.perpendicularB.x * cos;
        this.perpendicularB.y = -this.value.y * sin + this.perpendicularB.y * cos;
        this.perpendicularB.z = -this.value.z * sin + this.perpendicularB.z * cos;
        this.value.x = x;
        this.value.y = y;
        this.value.z = z;

        // make sure that all components are unit vectors again
        const Ar = Math.sqrt(this.perpendicularA.x * this.perpendicularA.x + this.perpendicularA.y * this.perpendicularA.y + this.perpendicularA.z * this.perpendicularA.z);
        this.perpendicularA.x /= Ar;
        this.perpendicularA.y /= Ar;
        this.perpendicularA.z /= Ar;

        const Br = Math.sqrt(this.perpendicularB.x * this.perpendicularB.x + this.perpendicularB.y * this.perpendicularB.y + this.perpendicularB.z * this.perpendicularB.z);
        this.perpendicularB.x /= Br;
        this.perpendicularB.y /= Br;
        this.perpendicularB.z /= Br;

        const Vr = Math.sqrt(this.value.x * this.value.x + this.value.y * this.value.y + this.value.z * this.value.z);
        this.value.x /= Vr;
        this.value.y /= Vr;
        this.value.z /= Vr;
    }

    rotateB(angle) {
        // rotate value and perpendicularA around perpendicularB
        const cos = Math.cos(angle);
        const sin = Math.sin(angle);
        const x = this.value.x * cos + this.perpendicularA.x * sin;
        const y = this.value.y * cos + this.perpendicularA.y * sin;
        const z = this.value.z * cos + this.perpendicularA.z * sin;
        this.perpendicularA.x = -this.value.x * sin + this.perpendicularA.x * cos;
        this.perpendicularA.y = -this.value.y * sin + this.perpendicularA.y * cos;
        this.perpendicularA.z = -this.value.z * sin + this.perpendicularA.z * cos;
        this.value.x = x;
        this.value.y = y;
        this.value.z = z;

        // make sure that all components are unit vectors again
        const Ar = Math.sqrt(this.perpendicularA.x * this.perpendicularA.x + this.perpendicularA.y * this.perpendicularA.y + this.perpendicularA.z * this.perpendicularA.z);
        this.perpendicularA.x /= Ar;
        this.perpendicularA.y /= Ar;
        this.perpendicularA.z /= Ar;

        const Br = Math.sqrt(this.perpendicularB.x * this.perpendicularB.x + this.perpendicularB.y * this.perpendicularB.y + this.perpendicularB.z * this.perpendicularB.z);
        this.perpendicularB.x /= Br;
        this.perpendicularB.y /= Br;
        this.perpendicularB.z /= Br;

        const Vr = Math.sqrt(this.value.x * this.value.x + this.value.y * this.value.y + this.value.z * this.value.z);
        this.value.x /= Vr;
        this.value.y /= Vr;
        this.value.z /= Vr;
    }

    get x() {
        return this.value.x;
    }

    get y() {
        return this.value.y;
    }

    get z() {
        return this.value.z;
    }
}