////////////////////////////////////////////////////////////////////////////////
// Setup Audio
////////////////////////////////////////////////////////////////////////////////

var audioContext;
var sampleUrls = [
    'audio/main-loop.mp3',      // 0
    'audio/glitch-1.mp3',       // 1
    'audio/glitch-2.mp3',       // 2
    'audio/glitch-3.mp3',       // 3
    'audio/glitch-4.mp3',       // 4
    'audio/glitch-5.mp3',       // 5
    'audio/bell-a.mp3',         // 6
    'audio/bell-b.mp3',         // 7
    'audio/bell-d.mp3',         // 8
    'audio/bell-e.mp3',         // 9
    'audio/bell-fSharp.mp3',    // 10
    'audio/bell-g.mp3'          // 11
    ];

function audioInit() {
    // Create audio context
    try {
        audioContext = new (window.AudioContext || window.webAudioContext || window.webkitAudioContext)();
    }
    catch(e) {
        alert('The Web Audio API is not supported in this browser');
    }
    // Load various sounds using BufferLoader class
    audioBufferLoader = new BufferLoader(audioContext, sampleUrls, doneLoadingSounds);
    audioBufferLoader.load();
    // Once Sounds are all loaded...
    function doneLoadingSounds(bufferList) {
        // Begin looped playback of main-loop.mp3
        playBufferSound(0, true);
        // Initiate Animation
        animate();
        // Remove loading screen
        document.querySelector("#loading").style.display = "none";
    }
}

function playBufferSound(soundPos, loop) { // soundPos = position of sound in loaded buffer array
    // create and configure node
    var audioSourceNode = audioContext.createBufferSource();
    audioSourceNode.buffer = audioBufferLoader.bufferList[soundPos];
    audioSourceNode.connect(audioContext.destination);
    if (loop == true) audioSourceNode.loop = true;
    audioSourceNode.start(0);
}

// BufferLoader Class
function BufferLoader(context, urlList, callback) {
    this.context = context;
    this.urlList = urlList;
    this.onload = callback;
    this.bufferList = new Array();
    this.loadCount = 0;
}
BufferLoader.prototype.loadBuffer = function(url, index) {
    // Load buffer asynchronously
    var request = new XMLHttpRequest();
    request.open("GET", url, true);
    request.responseType = "arraybuffer";
    var loader = this;
    request.onload = function() {
        // Asynchronously decode the audio file data in request.response
        loader.context.decodeAudioData(
            request.response,
            function(buffer) {
                if (!buffer) {
                    alert('error decoding file data: ' + url);
                    return;
                }
                loader.bufferList[index] = buffer;
                if (++loader.loadCount == loader.urlList.length)
                    loader.onload(loader.bufferList);
            },
            function(error) {
                console.error('decodeAudioData error', error);
            }
        );
    }
    request.onerror = function() {
        alert('BufferLoader: XHR error');
    }
    request.send();
}
BufferLoader.prototype.load = function() {
    for (var i = 0; i < this.urlList.length; ++i)
    this.loadBuffer(this.urlList[i], i);
}


////////////////////////////////////////////////////////////////////////////////
// Setup Basic Scene 
////////////////////////////////////////////////////////////////////////////////

var camera, scene, renderer, composer;
var windowScale;
var clock = new THREE.Clock();

// Create the scene
scene = new THREE.Scene();

// Create an ambient light
var light = new THREE.AmbientLight(0x777777);
scene.add( light );

// Create a directional light
var directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(400, 0, 800);
scene.add(directionalLight);

// Create a camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 270;

// Create the renderer
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 1);
document.body.appendChild(renderer.domElement);

// Window re-size
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize, false);


////////////////////////////////////////////////////////////////////////////////
// Create & Add Objects
////////////////////////////////////////////////////////////////////////////////

// Create a gradient coloured plane with strobe effect for the background,
// black at the top with two alternating (strobing) tones at the bottom.
var geometry = new THREE.PlaneGeometry(5000, 2500, 0, 0);
var wallGradientTop = new THREE.Color(0x000000);
var wallGradientBottom1 = new THREE.Color(0x00333b); 
var wallGradientBottom2 = new THREE.Color(0x002e37);
var wallGradientSwitch = wallGradientBottom1;
geometry.faces[0].vertexColors = [wallGradientTop, wallGradientBottom1, wallGradientTop];
geometry.faces[1].vertexColors = [wallGradientBottom1, wallGradientBottom1, wallGradientTop];
var wallStrobeClock = 0.0; // initialize to 0.0
var wallStrobeRate = 0.03; // strobe interval in seconds
var material = new THREE.MeshBasicMaterial({color: 0xffffff, vertexColors: THREE.VertexColors});
var wall = new THREE.Mesh(geometry, material);
wall.position.z = -500;
scene.add(wall);

// Create basic cube geometry and materials
var geometry = new THREE.BoxGeometry(20,20,20,0,0);

var cubeTexture = THREE.ImageUtils.loadTexture('img/cubeface.png');
var cubeMaterial = new THREE.MeshLambertMaterial({map: cubeTexture});
// Uncomment the line below for alternate base cube material with specular effect
// var cubeMaterial = new THREE.MeshPhongMaterial({color: 0xCCCCCC, map: cubeTexture, specular: 0x19797e});

var cubeTextureActive = THREE.ImageUtils.loadTexture('img/cubeface2-white.png');
var cubeMaterialActive = new THREE.MeshLambertMaterial({map: cubeTextureActive});

var cubePulseTexture = new THREE.ImageUtils.loadTexture('img/cubefacePulseSprite.png');
var cubePulseAnimator = new TextureAnimator( cubePulseTexture, 50, 1, 50, 30 ); // texture, #horiz, #vert, #total, duration.
var cubePulseMaterial = new THREE.MeshBasicMaterial({ map: cubePulseTexture});

// Create Geometry
surrogateRings = new THREE.Object3D();
var ring = [];
var ringSleeve = [];
for (var i=0; i<10; i++) {
    ring[i] = new THREE.Object3D();
    // Build and position a single ring.
    buildRing(ring[i]);
    ring[i].rotation.x = 90*Math.PI/180;
    ring[i].rotation.y = -30*Math.PI/180;
    ring[i].position.y = 100;
    // Position each ring instance in a circle formation
    ringSleeve[i] = new THREE.Object3D();
    ringSleeve[i].add(ring[i]);
    ringSleeve[i].rotation.z = i*(360/10)*Math.PI/180;
    // Collect everything in a final object
    surrogateRings.add(ringSleeve[i]);
}
// Add final parent object to scene
scene.add(surrogateRings);

// Helper function: Builds a single ring of cubes
function buildRing(currentRing) {
    var cube = [];
    var cubeSleeve = [];
    var cubeCount = 12; // Reducing cubeCount reduces number of cubes in each ring
    var cubeDist = cubeCount; // Can be used to modify distribution of cubes in each ring, try commented out line below...
    // var cubeCount = 7; var cubeDist = 26;
    for (var i=0; i < cubeCount; i++) {
    	cube[i] = new THREE.Mesh(geometry, cubeMaterial);
    	cube[i].position.y = 60;
    	cubeSleeve[i] = new THREE.Object3D();
    	cubeSleeve[i].add(cube[i]);
    	cubeSleeve[i].rotation.z = i*(360/cubeDist)*Math.PI/180;
    	currentRing.add(cubeSleeve[i]);    	
    }
}

// Helper function: TextureAnimator (by Lee Stemkoski)... https://stemkoski.github.io/Three.js/Texture-Animation.html
function TextureAnimator(texture, tilesHoriz, tilesVert, numTiles, tileDispDuration) 
{   
    // note: texture passed by reference, will be updated by the update function.
    this.tilesHorizontal = tilesHoriz;
    this.tilesVertical = tilesVert;
    // how many images does this spritesheet contain?
    // usually equals tilesHoriz * tilesVert, but not necessarily,
    // if there at blank tiles at the bottom of the spritesheet. 
    this.numberOfTiles = numTiles;
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 
    texture.repeat.set( 1 / this.tilesHorizontal, 1 / this.tilesVertical );
    // how long should each image be displayed?
    this.tileDisplayDuration = tileDispDuration;
    // how long has the current image been displayed?
    this.currentDisplayTime = 0;
    // which image is currently being displayed?
    this.currentTile = 0;
    this.update = function( milliSec )
    {
        this.currentDisplayTime += milliSec;
        while (this.currentDisplayTime > this.tileDisplayDuration)
        {
            this.currentDisplayTime -= this.tileDisplayDuration;
            this.currentTile++;
            if (this.currentTile == this.numberOfTiles)
                this.currentTile = 0;
            var currentColumn = this.currentTile % this.tilesHorizontal;
            texture.offset.x = currentColumn / this.tilesHorizontal;
            var currentRow = Math.floor( this.currentTile / this.tilesHorizontal );
            texture.offset.y = currentRow / this.tilesVertical;
        }
    }
}


////////////////////////////////////////////////////////////////////////////////
// Post-Processing Shaders
////////////////////////////////////////////////////////////////////////////////

composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));

var glitchEffect = new THREE.ShaderPass(THREE.DigitalGlitch);
glitchEffect.uniforms[ 'byp' ].value=1;
composer.addPass(glitchEffect);

var filmEffect = new THREE.ShaderPass(THREE.FilmShader);
filmEffect.uniforms[ 'grayscale' ].value = 0;
filmEffect.uniforms[ 'nIntensity' ].value = 0.4;
filmEffect.uniforms[ 'sIntensity' ].value = 0.5;
filmEffect.uniforms[ 'sCount' ].value = 1300;
composer.addPass(filmEffect);

var vignetteEffect = new THREE.ShaderPass(THREE.VignetteShader);
vignetteEffect.uniforms[ 'offset' ].value = 1.1;
vignetteEffect.renderToScreen = true;
composer.addPass(vignetteEffect);


////////////////////////////////////////////////////////////////////////////////
// Glitch Events
////////////////////////////////////////////////////////////////////////////////

var glitchActive, glitchNum, glitchTimeOut;
var glitchClock = 0.0;
var glitchInterval = 4.0;
var glitchVals = [
    {
        amount: 45, // Used by renderGlitchFrame() as: Math.random()/amount
        seed_xy: 0.8, // Used by renderGlitchFrame() as: THREE.Math.randFloat(-seed_xy,seed_xy)
        duration: 230 // Milliseconds
    },
    {
        amount: 100,
        seed_xy: 0.6,
        duration: 300
    },
    {
        amount: 60,
        seed_xy: 0.3,
        duration: 400
    },
    {   
        amount: 90,
        seed_xy: 1,
        duration: 150
    },
    {   
        amount: 90,
        seed_xy: 1,
        duration: 640
    }
];

function triggerGlitchEvent() {
    glitchActive = true;
    playBufferSound(glitchNum+1);
    clearTimeout(glitchTimeOut);
    glitchTimeOut = setTimeout(function() {
        glitchEffect.uniforms[ 'byp' ].value = 1;
        glitchActive = false;
    }, glitchVals[glitchNum].duration);
}

function renderGlitchFrame() {
    glitchEffect.uniforms[ 'byp' ].value = 0; // disable bypass
    glitchEffect.uniforms[ 'seed' ].value = Math.random();
    glitchEffect.uniforms[ 'amount' ].value = Math.random()/glitchVals[glitchNum].amount;
    glitchEffect.uniforms[ 'angle' ].value = THREE.Math.randFloat(-Math.PI,Math.PI);
    glitchEffect.uniforms[ 'distortion_x' ].value = THREE.Math.randFloat(0,1);
    glitchEffect.uniforms[ 'distortion_y' ].value = THREE.Math.randFloat(0,1);
    glitchEffect.uniforms[ 'seed_x' ].value = THREE.Math.randFloat(-glitchVals[glitchNum].seed_xy,glitchVals[glitchNum].seed_xy);
    glitchEffect.uniforms[ 'seed_y' ].value = THREE.Math.randFloat(-glitchVals[glitchNum].seed_xy,glitchVals[glitchNum].seed_xy);
}


////////////////////////////////////////////////////////////////////////////////
// Object Picking
////////////////////////////////////////////////////////////////////////////////

var pickVector = new THREE.Vector3();
var raycaster = new THREE.Raycaster();
var textureTimeouts ={};

function objectPick(event) {
    pickVector.set(
        ((event.clientX || event.touches[0].pageX) / window.innerWidth) * 2 - 1, // x
        - ((event.clientY || event.touches[0].pageY) / window.innerHeight) * 2 + 1, // y
        0.5 // z = 0.5 important!
    );
    pickVector.unproject(camera);
    raycaster.set(camera.position, pickVector.sub( camera.position ).normalize());
    
    var intersects = raycaster.intersectObjects( surrogateRings.children, true );

    if (intersects.length > 0) {
        var pickedID = intersects[0].object.id.toString();
        
        clearTimeout(textureTimeouts[pickedID]);
        textureTimeouts[pickedID] = setTimeout(function() {
            intersects[0].object.material = cubePulseMaterial;
        }, 2500);
        
        intersects[0].object.material = cubeMaterialActive;
        playBufferSound(Math.floor(Math.random() * (12 - 6)) + 6);
    }
}

document.addEventListener( 'click', objectPick, false );
document.addEventListener( 'touchstart', objectPick, false );


////////////////////////////////////////////////////////////////////////////////
// Animate
////////////////////////////////////////////////////////////////////////////////

// Animate
function animate() {
	requestAnimationFrame(animate);
    update();
	render();
}

// Update loop
function update() {
    var delta = clock.getDelta();
    // BG strobe effect
    wallStrobeClock += delta;
    if (wallStrobeClock > wallStrobeRate) {
        wallStrobeClock = 0.0;
        wallGradientSwitch = (wallGradientSwitch == wallGradientBottom1) ? wallGradientBottom2 : wallGradientBottom1;
        wall.geometry.faces[0].vertexColors = [wallGradientTop, wallGradientSwitch, wallGradientTop];
        wall.geometry.faces[1].vertexColors = [wallGradientSwitch, wallGradientSwitch, wallGradientTop];
        wall.geometry.colorsNeedUpdate = true;
    }
    // Rotate Rings
    for (var i=0; i glitchInterval) {
        glitchClock = 0.0;
        glitchNum = Math.floor(Math.random()*glitchVals.length);
        triggerGlitchEvent();
    }
    if (glitchActive) renderGlitchFrame();
}

// Render loop
function render() {
    // Render scene using Effect Composer
	composer.render();
}

// Initial Bootstrap Trigger: ensure all assets are loaded before calling render cycle.
window.onload = function() {
	audioInit();
};


////////////////////////////////////////////////////////////////////////////////
// Debug Glitch Triggers
////////////////////////////////////////////////////////////////////////////////

// Number keys 1-5 manually trigger Glitch events
document.onkeypress = function(e) {
    var relatedKeyStroke;
    switch (e.keyCode) {
        case 49:
            glitchNum = 0;
            relatedKeyStroke = true;
            break;
        case 50:
            glitchNum = 1;
            relatedKeyStroke = true;
            break;
        case 51:
            glitchNum = 2;
            relatedKeyStroke = true;
            break;
        case 52:
            glitchNum = 3;
            relatedKeyStroke = true;
            break;
        case 53:
            glitchNum = 4;
            relatedKeyStroke = true;
            break;
    }
    if (relatedKeyStroke) {
        glitchActive = true;
        playBufferSound(glitchNum+1);
        clearTimeout(glitchTimeOut);
        glitchTimeOut = setTimeout(function() {
            glitchEffect.uniforms[ 'byp' ].value = 1;
            glitchActive = false;
        }, glitchVals[glitchNum].duration);
    }
};