//---- Settings --------------------------------------------------------------//

var outerTubeColor = 'rgb(115, 115, 115)';
var midTubeColor = 'rgb(230, 230, 230)';
var innerTubeColor = 'rgb(255, 255, 255)';

var outerTubeWidth = 4;
var midTubeWidth = 2;
var innerTubeWidth = 1;

var canvasW = 800;
var canvasH = 500;

// coordinates of the central word
var masterX = canvasW/2;
var masterY = canvasH/2;

// How widely apart the past links are.
var prevSpreadX = 100;
var prevSpreadY = 100;

var bezierStiffness = 20;

var minFontSize = 15;

var maxFontSize = 30;

// The number of previous Nodes to show (includes current).
var prevCount = 4;
// The number of previous Nodes to search based on (includes current).
var prevSearchCount = 3;
// The number of results to fetch and show.
var nextCount = 5;
// The time (in milliseconds) to wait between frames.
var frameDelay = 30;
// The time (in milliseconds) to wait between adding 'next' Nodes.
var timeBetweenAdds = 200;

var maxLogLength = 0;

// It's magic!
// Higher is faster, 0.0 is slow as something that doesn't move, 1.0 is
// instant.
var magicSpeedCoefficient = 0.11;

var url = '/getWords.php';
//var url = 'dummydata.json';

//---- Core ------------------------------------------------------------------//

// Array of ALL nodes
var nodePool;

// Array of all previous Nodes
var previous;

// Array of next Nodes
var next;

var timeSinceLastAdd = 0;

// Set up initial state
function init(word) {
    nodePool = [new Node(word, 10)];
    previous = [nodePool[0]];
    next = [];

    previous[0].place(prevPos(0));
    previous[0].show();

    fetchNodes(previous.slice(0, prevSearchCount));

    mainLoop();
}

function mainLoop() {
    setTimeout(mainLoop, frameDelay);

    timeSinceLastAdd += frameDelay;
    if(results.length > 0 && timeBetweenAdds <= timeSinceLastAdd) {
        var newNode = results.shift();
        nodePool.push(newNode);
        next.push(newNode);
        newNode.place({ x: previous[0].pos.x, y: previous[0].pos.y });
        newNode.show();
        moveToPositions();
        timeSinceLastAdd = 0;
    }

    updateNodes();
}

function updateNodes() {
    var needRedraw = false;
    for(i in nodePool) {
        if(nodePool[i].isAnimating()) {
            needRedraw = true;
            nodePool[i].animate();
        }
    }
    nodePool = nodePool.filter(function (n) { return n.state.type != "dead"; });
    if(needRedraw) {
        redrawCanvas();
    }
}

// Tell all visible nodes to go to their positions.
function moveToPositions() {
    for(var i = 0; i < previous.length && i < prevCount; i++) {
        previous[i].moveTo(prevPos(i));
        previous[i].fadeTo(Math.pow(0.5, i));
    }
    for(var i in next) {
        next[i].moveTo(nextPos(i));
    }
}

function goToNode(id) {
    var match;

    // Is it one of the next elements?
    match = next.filter(function (n) { return n.divId == id; });
    if(match.length > 0) {
        // Move chosen Node into previous Nodes
        previous.unshift(match[0]);
        // The prevCount'th Node can't be seen anymore.
        if(previous[prevCount] !== undefined) {
            previous[prevCount].hide();
        }
        for(var i in next) {
            if(next[i] !== previous[0]) {
                next[i].kill();
            }
        }
        next = [];
        fetchNodes(previous.slice(0, prevCount));
        moveToPositions();
        return;
    }

    // Is it one of the previous elements?
    match = previous.filter(function (n) { return n.divId == id; });
    if(match.length > 0) {
        // go back until we find it
        while (previous[0].divId != match[0].divId) {
            // The prevCount'th Node can now be seen.
            if(previous[prevCount] !== undefined) {
                previous[prevCount].show();
            }
            previous.shift().kill();
        }
        for(i in next) {
            next[i].kill();
        }
        next = [];
        fetchNodes(previous.slice(0, prevCount));
        moveToPositions();
        return;
    }
}

var pi = Math.acos(-1);

function nextPos(i) {
    var angle = pi/2 + (i - (next.length - 1)/2 - 0.04) * pi/8;
    var radius = (3 * canvasH + masterY)/4;
    return {
        x: masterX + Math.cos(angle) * radius,
        y: Math.sin(angle) * radius
    };
}

function prevPos(i) {
    if(i == 0) {
        return { x: masterX, y: masterY };
    } else {
        return {
            x: masterX + prevSpreadX * Math.pow(-1, i),
            y: masterY - prevSpreadY * i
        };
    }
}

//---- Canvas ----------------------------------------------------------------//

function redrawCanvas() {
    var canvas = $("#canvas").get(0);
    if(canvas.getContext) {
        var ctx = canvas.getContext('2d');

        ctx.clearRect(0, 0, canvasW, canvasH);

        for(var i = 0; i < prevCount - 1 && i < previous.length - 1; i++) {
            drawTube(ctx, previous[i + 1], previous[i]);
        }
        for(var i in next) {
            drawTube(ctx, previous[0], next[i]);
        }
    }
}

// Draw a tube between two nodes
function drawTube(ctx, a, b) {
    x1 = a.pos.x;
    y1 = a.pos.y + $(a.getDiv()).height() / 2;

    x2 = b.pos.x;
    y2 = b.pos.y - $(b.getDiv()).height() / 2;

    ctx.globalAlpha = a.opacity;
    ctx.strokeStyle = outerTubeColor;
    ctx.lineWidth = outerTubeWidth;
    drawCurve(ctx, x1, y1, x2, y2);

    ctx.strokeStyle = midTubeColor;
    ctx.lineWidth = midTubeWidth;
    drawCurve(ctx, x1, y1, x2, y2);

    ctx.strokeStyle = innerTubeColor;
    ctx.lineWidth = innerTubeWidth;
    drawCurve(ctx, x1, y1, x2, y2);
}

function drawCurve(ctx, x1, y1, x2, y2) {
    log('' + x1 + ' ' + y1);
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.bezierCurveTo(
        x1, y1 + bezierStiffness, 
        x2, y2 - bezierStiffness,
        x2, y2);
    ctx.stroke();
}

//---- Query -----------------------------------------------------------------//

// Handles one query at a time, putting the newly made nodes in 'results'. If
// you make a new query before the first one returns its data, the older one
// will be cancelled.

var results = [];
var lastRequest = false;

function fetchNodes(searchNodes) {
    results = [];

    // Clear previous request
    if(lastRequest) {
        lastRequest.abort();
        lastRequest = false;
    }

    // Start the new request
    var searchTerm =
        searchNodes.map(function (n) { return n.word.replace(' ', '+'); })
                   .join("+");
    var thisRequest = $.getJSON(url, { q: searchTerm, wordLimit: nextCount },
        function(data) {
            // If a new request was started before this was called we want to
            // ignore the old results.
            if (lastRequest === thisRequest) {
                results = data.words.map(function (w) {
                    return new Node(w.word, w.count);
                });
            }
        },
        500);
    lastRequest = thisRequest;
}

function dummyJSON(url, params, callback) {
    var dummyResults = { words: [] };
    for(var i = 0; i < nextCount; i++) {
        dummyResults.words.push(dummyResult());
    }
    setTimeout(function () {callback(dummyResults)}, 500);
    return {abort: function(){}};
}

function dummyResult()
{
    return {
        word: '' + Math.floor(Math.random()*1000),
        count: Math.floor(Math.random()*29 + 1)
    }
}

//---- Nodes -----------------------------------------------------------------//

//---- Node States

// Possible Node states are:
// - change     approaching a certain size, opacity, and position
// - idle       nothing happens
// - dead       nothing happens, should be removed from list.

// Change state keeps track of target position, size, opacity
function ChangeState(pos, size, opacity, dieAfter) {
    this.type = "change";
    this.pos = pos;
    this.size = size;
    this.opacity = opacity;
    this.dieAfter = dieAfter;
}

function IdleState() {
    this.type = "idle";
}

function DeadState() {
    this.type = "dead";
}

//---- Nodes / Animation

function Node(word, count) {
    // word info
    this.word = word;
    this.count = count;
    // Node info
    this.divId = makeDivId();
    // current Node state
    this.state = new IdleState();
    this.pos = {x: 0, y: 0};
    this.size = 0;
    this.opacity = 0.0;
}

Node.prototype.place = function(pos) {
    this.pos = pos;
    // If we are moving, cancel the movement.
    if(this.state.type == "change") {
        this.state.pos = this.pos;
    }
    this.updateDiv();
};

Node.prototype.updateDiv = function () {
    var div = this.getDiv();
    // If the Node doesn't have a div but is displayed, make it one.
    if(!div && this.opacity > 0)
    {
        $('#app').prepend(
            '<div id="' + this.divId + '" class="node">'
            + '<a href="">' + this.word + '</a>'
            + '</div>');
        div = this.getDiv();

        // 'this' is mangled by creepy javascript semantics
        var divId = this.divId; 

        $(div).children().click(function () {
            goToNode(divId);
            return false;
        });
    }
    if(div && this.opacity == 0) {
        $(div).remove();
    }


    if(div) {
        var maxSize = Math.min(15, this.count) + 16;
        $(div).css('font-size', '' + (this.size * maxSize) + 'px')
              .css('opacity', this.opacity)
              .css('left', this.pos.x - $(div).width()/2)
              .css('top', this.pos.y - $(div).height()/2);
    }
}

Node.prototype.getDiv = function () {
    return $('#' + this.divId).get(0);
}

Node.prototype.isAnimating = function () {
    return this.state.type == "change";
}

var posEps = 1;
var sizeEps = 0.01;
var opacityEps = 0.01;

Node.prototype.animate = function() {
    if(this.state.type == "change") {
        this.pos.x   = interp(this.pos.x,   this.state.pos.x,   posEps);
        this.pos.y   = interp(this.pos.y,   this.state.pos.y,   posEps);
        this.size    = interp(this.size,    this.state.size,    sizeEps);
        this.opacity = interp(this.opacity, this.state.opacity, opacityEps);
        
        if(within(this.pos.x,   this.state.pos.x,  posEps) &&
           within(this.pos.y,   this.state.pos.y,  posEps) &&
           within(this.size,    this.state.size,    sizeEps) &&
           within(this.opacity, this.state.opacity, opacityEps)) {
            if(this.state.dieAfter) {
                this.state = new DeadState();
                this.opacity = 0; // forces removal of the div
            } else {
                this.state = new IdleState();
            }
        }
        this.updateDiv();
    }
}

Node.prototype.changeTo = function(opts) {
    var defaultsFrom = this.state.type == "change" ? this.state : this;
    var newOpts = {
        pos: defaultsFrom.pos,
        size: defaultsFrom.size,
        opacity: defaultsFrom.opacity
    } 
    newOpts.dieAfter =
        this.state.type == "change" ? this.state.dieAfter : false;
    
    for(i in newOpts) {
        if(opts[i] !== undefined) {
            newOpts[i] = opts[i];
        } 
    }

    this.state = new ChangeState(
        newOpts.pos,
        newOpts.size,
        newOpts.opacity,
        newOpts.dieAfter);
}

Node.prototype.moveTo = function(pos) {
    this.changeTo({ pos: pos });
}

Node.prototype.fadeTo = function (opacity) {
    this.changeTo({ opacity: opacity });
}

Node.prototype.show = function() {
    this.changeTo({ size: 1, opacity: 1 });
}

Node.prototype.hide = function() {
    this.changeTo({ size: 0, opacity: 0 });
}

Node.prototype.kill = function() {
    this.hide();
    this.changeTo({ dieAfter: true });
}

//---- Node Utility Functions

// Interpolate current towards target, once they are closer than epsilon it is
// considered to be at the target.
function interp(current, target, epsilon) {
    var newVal = (1 - magicSpeedCoefficient) * current +
                 magicSpeedCoefficient * target;
    if(within(newVal, target, epsilon)) {
        newVal = target;
    }
    return newVal;
}

// Is the difference between a and b less than epsilon?
function within(a, b, epsilon) {
    return Math.abs(a - b) < epsilon;
}

var nextIdNum = 0;

function makeDivId() {
    var divId = 'node' +  nextIdNum;
    nextIdNum++;
    return divId;
};

function log(str) {
    if(maxLogLength > 0) {
        $("#debug").append(str + "<br>");
        maxLogLength--;
    }
}

function fixDivs () {
	for(i in nodePool) {
		nodePool[i].updateDiv();
	}
}
