
window.LL = {

    config: {
        dataUrl: "data/line%s.json"
    },

    canvas: null,
    stations: {},
    lines: {},
    rawData: null,
    
    init: function () {
        // Retrieve data
        $.getJSON("js/lines.json", function (data) {
            LL.rawData = data;
            // Calculate stations
            LL.calculateStations();
            for (var line in LL.rawData.lines) {
                LL.calculatePositions(line);
                var container = $("#line" + line);
                if (container.length) {
                    LL.lines[line].renderer = new LL.LineRenderer(line, container);
                } else {
                    delete LL.lines[line];
                }
            }
            LL.fetchStep();
            LL.drawStep();
        });
    },
    
    drawStep: function () {
        for (var line in LL.lines) {
            LL.lines[line].renderer.draw();
        }
        setTimeout(LL.drawStep, 500);
    },
    
    fetchStep: function () {
        for (var line in LL.lines) {
            LL.lines[line].renderer.fetchTrainData();
        }
        setTimeout(LL.fetchStep, 3000);
    },
    
    // Goes through the station info and denormalises it into useable forms.
    calculateStations: function () {
        // First, get all stations and make basic objects
        for (var code in LL.rawData.stations) {
            LL.stations[code] = {
                name: LL.rawData.stations[code],
                links: [],
                x: {},
                y: {}
            };
        }
        // Then, go through all links and annotate them onto stations
        for (var line in LL.rawData.lines) {
            LL.lines[line] = {
                width: null,
                height: null,
                stations: [],
                renderer: null
            };
            LL.rawData.lines[line].display.each(function (segment) {
                segment.each(function (run) {
                    var lastStation = null;
                    run.each(function (code) {
                        if (lastStation) {
                            LL.stations[code].links.push([lastStation, line]);
                            LL.stations[lastStation].links.push([code, line]);
                        }
                        lastStation = code;
                        if (LL.lines[line].stations.toString().indexOf(code) < 0) {
                            LL.lines[line].stations.push(code);
                        }
                    });
                });
            });
        }
    },
    
    // Goes through the lines and works out where the stations should be
    // horizontally and vertically
    calculatePositions: function (line) {
        for (var line in LL.rawData.lines) {
            var x = 0;
            var ymax = 0;
            
            LL.rawData.lines[line].display.each(function (segment) {
                var y = 0;
                segment.each(function (run) {
                    // We have a run of stations. Go along it, assigning
                    // positions.
                    var subx = x;
                    run.each(function (code) {
                        LL.stations[code].x[line] = subx;
                        LL.stations[code].y[line] = y;
                        subx += 1;
                    });
                    y += 1;
                });
                x += segment[0].length - 1;
                ymax = Math.max(y, ymax);
            });
            LL.lines[line].width = x + 1;
            LL.lines[line].height = ymax;
        }
    },
    
    LineRenderer: function (line, container) {
        this.line = line;
        this.container = container;
        this.trains = [];
    }
    
}

LL.LineRenderer.prototype = {

    // Draws the line onto the provided canvas
    draw: function () {
        var self = this;
        this.canvas = this.container.find("canvas.map")[0];
        this.canvas.width = this.container.width();
        this.canvas.height = this.container.height();
        this.namesDiv = this.container.find(".names");
        this.xstep = this.canvas.width / (LL.lines[this.line].width + 1);
        this.ystep = this.canvas.height / (LL.lines[this.line].height + 1);
        this.ctx = this.canvas.getContext("2d");
        // Clear up from before
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.namesDiv.empty();
        // Draw!
        LL.rawData.lines[this.line].display.each(function (segment) {
            // Pass 1: station links
            segment.each(function (run) {
                var lastStation = null;
                run.each(function (code) {
                    if (lastStation) {
                        self.drawLink(lastStation, code);
                    }
                    lastStation = code;
                });
            });
        });
        var dir = 1;
        LL.lines[this.line].stations.each(function (code) {
            // Pass 2: stations themselves
            self.drawStation(code, dir);
            dir = -dir;
        });
        // Pass 3: trains
        self.trains.each(function (train) {
            // Work out the train's progress between stations
            var start = train[1][0] + 7;
            var end = train[2][0];
            var progress = (parseInt((new Date).getTime()/1000) - start) / (end - start);
            progress = Math.min(Math.max(progress, 0), 1);
            self.drawTrain(train[0], train[1][1], train[2][1], progress, train[3]);
        });
    },
    
    // Asynchronously fetches new train data
    fetchTrainData: function () {
        var self = this;
        $.getJSON(LL.config.dataUrl.replace("%s", this.line), function (data) {
            self.trains = data.trains;
        });
    },
    
    // Takes virtual coordinates and maps them to the screen
    coordsToScreen: function (coords) {
        return [
            (coords[0] + 1) * this.xstep,
            (coords[1] + 1) * this.ystep
        ]
    },
    
    // Returns a station's screen coordinates
    stationToScreen: function (station) {
        return this.coordsToScreen([
            LL.stations[station].x[this.line],
            LL.stations[station].y[this.line]
        ]);
    },
    
    // Draws a single line link
    drawLink: function (station1, station2) {
        this.ctx.lineWidth = 8;
        this.ctx.lineCap = "square";
        this.ctx.strokeStyle = LL.rawData.lines[this.line].color;
        this.ctx.beginPath();
        this.ctx.moveTo.apply(this.ctx, this.stationToScreen(station1));
        this.ctx.lineTo.apply(this.ctx, this.stationToScreen(station2));
        this.ctx.stroke();
    },
    
    isTerminus: function (station) {
        var numLinks = 0;
        var self = this;
        LL.stations[station].links.each(function (link) {
            if (link[1] == self.line) numLinks++;
        });
        return numLinks == 1;
    },
    
    isInterchange: function (station) {
        var numLinks = 0;
        var self = this;
        LL.stations[station].links.each(function (link) {
            if (link[1] != self.line) numLinks++;
        });
        return numLinks > 0;
    },
    
    // Draws a station
    drawStation: function (station, dir) {
        this.ctx.lineCap = "square";
        this.ctx.strokeStyle = LL.rawData.lines[this.line].color;
        var stationCoords = this.stationToScreen(station);
        // Station mark
        if (this.isTerminus(station)) {
            // Terminus
            this.ctx.lineWidth = 8;
            this.ctx.beginPath();
            this.ctx.moveTo(stationCoords[0], stationCoords[1] - 8);
            this.ctx.lineTo(stationCoords[0], stationCoords[1] + 8);
            this.ctx.stroke();
        //} else if (this.isInterchange(station)) {
            //// Interchange
            //this.ctx.strokeStyle = "#000";
            //this.ctx.fillStyle = "#fff";
            //this.ctx.lineWidth = 3;
            //this.ctx.beginPath();
            //this.ctx.arc(stationCoords[0], stationCoords[1], 7, 0, Math.PI*2, false);
            //this.ctx.fill();
            //this.ctx.stroke();
        } else {
            // Very normal station
            this.ctx.lineWidth = 5;
            this.ctx.beginPath();
            this.ctx.moveTo(stationCoords[0], stationCoords[1] - 8*dir);
            this.ctx.lineTo(stationCoords[0], stationCoords[1]);
            this.ctx.stroke();
        }
        // Station name
        var name = $("<div class='station'>" + LL.stations[station].name + "</div>");
        this.namesDiv.append(name);
        if (dir > 0) {
            name.css({
                bottom: (this.canvas.height - stationCoords[1]) + 15*dir,
                left: stationCoords[0] - (name.width()/2)
            });
        } else {
            name.css({
                top: stationCoords[1] - 15*dir,
                left: stationCoords[0] - (name.width()/2)
            });
        }
    },
    
    drawTrain: function (set_id, station1, station2, progress, endStation) {
        // Interpolate X and Y values
        var stationCoords1 = this.stationToScreen(station1);
        var stationCoords2 = this.stationToScreen(station2);
        var coords = [
            stationCoords1[0] + (stationCoords2[0] - stationCoords1[0]) * progress,
            stationCoords1[1] + (stationCoords2[1] - stationCoords1[1]) * progress
        ]
        // Decide if it's going left or right
        var left = (stationCoords1[0] < stationCoords2[0]);
        var y = left ? -2 : 2;
        // Draw a marker
        this.ctx.lineWidth = 4;
        this.ctx.strokeStyle = left ? "rgba(0, 0, 0, 0.7)" : "rgba(255, 255, 255, 0.7)";
        this.ctx.beginPath();
        this.ctx.moveTo(coords[0] - 2, coords[1] + y);
        this.ctx.lineTo(coords[0] + 2, coords[1] + y);
        this.ctx.stroke();
        // Draw set ID
        var setid = $("<div class='set'>" + set_id + "</div>");
        this.namesDiv.append(setid);
        if (left) {
            setid.css({
                bottom: (this.canvas.height - coords[1]) + 5,
                left: coords[0] - (setid.width()/2)
            });
        } else {
            setid.css({
                top: coords[1] + 5,
                left: coords[0] - (setid.width()/2)
            });
        }
    }
}

$(LL.init);

// Useful to solve JS' lack of iterators
Array.prototype.each = function (callable) {
    for (var i = 0; i < this.length; i++) {
        callable.apply(this[i], [this[i]]);
    }
}
