var map;
var infoWindow = new google.maps.InfoWindow();

// Set to true when debugging for log statements about HTTP requests.
var log = false;
var twelveHourTime = false;  // set to true to see AM/PM
var selectedRoute = null;
var forbid_editing = false;

function load(config) {
    forbid_editing = config.forbid_editing;

    sizeRouteList();
    var map_dom = document.getElementById("map");
    var mapOptions = {
        zoom: 13,
        center: config.bounds.getCenter(),
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        scaleControl: true
    };
    map = new google.maps.Map(map_dom, mapOptions);
    map.fitBounds(config.bounds);
    initIcons();
    google.maps.event.addListener(map, "moveend", callbackMoveEnd);
    google.maps.event.addListener(map, "zoomend", callbackZoomEnd);
    fetchStopsInBounds(config.bounds);
    fetchRoutes();
}

function callbackZoomEnd() {
}

function callbackMoveEnd() {
    // Map moved, search for stops near the center
    fetchStopsInBounds(map.getBounds());
}

/**
 * Fetch a sample of stops in the bounding box.
 */
function fetchStopsInBounds(bounds) {
    url = "/json/boundboxstops?n=" + bounds.getNorthEast().lat()
        + "&e=" + bounds.getNorthEast().lng()
        + "&s=" + bounds.getSouthWest().lat()
        + "&w=" + bounds.getSouthWest().lng()
        + "&limit=50";
    downloadUrl(url, callbackDisplayStopsBackground);
}

/**
 * Displays stops returned by the server on the map. Expected to be called
 * when downloadUrl finishes.
 *
 * @param {String} data JSON encoded list of list, each
 *     containing a row of stops.txt
 * @param {Number} responseCode Response code from server
 */
function callbackDisplayStops(data, responseCode) {
    if (responseCode != 200) {
        return;
    }
    clearMap();
    var stops = eval(data);
    if (stops.length == 1) {
        var marker = addStopMarkerFromList(stops[0], true);
        fetchStopInfoWindow(marker);
    } else {
        for (var i = 0; i < stops.length; ++i) {
            addStopMarkerFromList(stops[i], true);
        }
    }
}

function stopTextSearchSubmit() {
    var text = document.getElementById("stopTextSearchInput").value;
    var url = "/json/stopsearch?q=" + text;  // TODO URI escape
    downloadUrl(url, callbackDisplayStops);
}

function tripTextSearchSubmit() {
    var text = document.getElementById("tripTextSearchInput").value;
    selectTrip(text);
}

/**
 * Add stops markers to the map and remove stops no longer in the
 * background.
 */
function callbackDisplayStopsBackground(data, responseCode) {
    if (responseCode != 200) {
        return;
    }
    var stops = eval(data);
    // Make a list of all background markers
    var oldStopMarkers = {};
    for (var stopId in stopMarkersBackground) {
        oldStopMarkers[stopId] = stopMarkersBackground[stopId];
    }
    // Add new markers to the map and remove from oldStopMarkers
    for (var i = 0; i < stops.length; ++i) {
        var marker = addStopMarkerFromList(stops[i], false);
        if (oldStopMarkers[marker.stopId]) {
            delete oldStopMarkers[marker.stopId];
        }
    }
    // Delete all markers that remain in oldStopMarkers
    for (var stopId in oldStopMarkers) {
        oldStopMarkers[stopId].setMap(null);
    }
    oldStopMarkers = {};
}

/**
 * Remove all overlays from the map
 */
function clearMap() {
    boundsOfPolyLine = null;
    for (var stopId in stopMarkersSelected) {
        stopMarkersSelected[stopId].setMap(null);
    }
    for (var stopId in stopMarkersBackground) {
        stopMarkersBackground[stopId].setMap(null);
    }
    for (var i = 0; i < existingPolylines.length; ++i) {
        existingPolylines[i].setMap(null);
    }
    stopMarkersSelected = {};
    stopMarkersBackground = {};
    existingPolylines = [];
}

/**
 * Return a new GIcon used for stops
 */
function makeStopIcon(imgUrl) {
    return {
        size: new google.maps.Size(12, 20),
        anchor: new google.maps.Point(6, 20),
        url: imgUrl
    };
}

/**
 * Initialize icons. Call once during load.
 */
function initIcons() {
    iconSelected = makeStopIcon("/file/mm_20_yellow.png");
    iconBackground = makeStopIcon("/file/mm_20_blue_trans.png");
    iconBackgroundStation = makeStopIcon("/file/mm_20_red_trans.png");
}

var iconSelected;
var iconBackground;
var iconBackgroundStation;
// Map from stopId to Marker object for stops selected because they are
// part of a trip, etc
var stopMarkersSelected = {};
// Map from stopId to Marker object for stops found by the background
// passive search
var stopMarkersBackground = {};

/**
 * Add a stop to the map, given a row from stops.txt.
 */
function addStopMarkerFromList(list, selected, text) {
    return addStopMarker(list[0], list[1], list[2], list[3], list[4], selected, text);
}

/**
 * Add a stop to the map, returning the new marker
 */
function addStopMarker(stopId, stopName, stopLat, stopLon, locationType, selected, text) {
    if (stopMarkersSelected[stopId]) {
        // stop was selected
        var marker = stopMarkersSelected[stopId];
        if (text) {
            oldText = marker.getText();
            if (oldText) {
                oldText = oldText + "<br>";
            }
            marker.setText(oldText + text);
        }
        return marker;
    }
    if (stopMarkersBackground[stopId]) {
        // Stop was in the background. Either delete it from the background or
        // leave it where it is.
        if (selected) {
            stopMarkersBackground[stopId].setMap(null);
            delete stopMarkersBackground[stopId];
        } else {
            return stopMarkersBackground[stopId];
        }
    }

    var icon;
    if (selected) {
        icon = iconSelected;
    } else if (locationType == 1) {
        icon = iconBackgroundStation
    } else {
        icon = iconBackground;
    }
    var ll = new google.maps.LatLng(stopLat, stopLon);
    var markerOpts = {
        icon: icon,
        map: map,
        position: ll,
        draggable: !forbid_editing,
        anchorPoint: new google.maps.Point(0, -20)
    };
    var marker = null;
    if (selected || text) {
        if (!text) {
            text = "";  // Make sure every selected icon has a text box, even if empty
        }
        markerOpts.labelContent = text;
        markerOpts.labelClass = "tooltip";
        markerOpts.labelAnchor = new google.maps.Point(-10, 20);
        marker = new MarkerWithLabel(markerOpts);
    } else {
        marker = new google.maps.Marker(markerOpts);
    }
    marker.stopName = stopName;
    marker.stopId = stopId;
    if (selected) {
        stopMarkersSelected[stopId] = marker;
    } else {
        stopMarkersBackground[stopId] = marker;
    }
    google.maps.event.addListener(marker, "click", function () {
        fetchStopInfoWindow(marker);
    });
    google.maps.event.addListener(marker, "dragend", function () {
        document.getElementById("edit").style.visibility = "visible";
        document.getElementById("edit_status").innerHTML = "updating..."
        changeStopLocation(marker);
    });
    return marker;
}

/**
 * Sends new location of a stop to server.
 */
function changeStopLocation(marker) {
    var url = "/json/setstoplocation?id=" +
        encodeURIComponent(marker.stopId) +
        "&lat=" + encodeURIComponent(marker.getPosition().lat()) +
        "&lng=" + encodeURIComponent(marker.getPosition().lng());
    downloadUrl(url, function (data, responseCode) {
        document.getElementById("edit_status").innerHTML = unescape(data);
    });
}

/**
 * Saves the current state of the data file opened at server side to file.
 */
function saveData() {
    var url = "/json/savedata";
    downloadUrl(url, function (data, responseCode) {
        document.getElementById("edit_status").innerHTML = data;
    });
}

/**
 * Fetch the next departing trips from the stop for display in an info
 * window.
 */
function fetchStopInfoWindow(marker) {
    var url = "/json/stoptrips?stop=" + encodeURIComponent(marker.stopId) +
        "&time=" + parseTimeInput() + "&date=" + parseDateInput();
    downloadUrl(url, function (data, responseCode) {
        callbackDisplayStopInfoWindow(marker, data, responseCode);
    });
}

function callbackDisplayStopInfoWindow(marker, data, responseCode) {
    if (responseCode != 200) {
        return;
    }
    var timeTrips = eval(data);
    var html = "<b>" + marker.stopName + "</b> (" + marker.stopId + ")<br>";
    var latLng = marker.getPosition();
    html = html + "(" + latLng.lat() + ", " + latLng.lng() + ")<br>";
    html = html + "<table><tr><th>service_id<th>time<th>name</tr>";
    for (var i = 0; i < timeTrips.length; ++i) {
        var time = timeTrips[i][0];
        var tripid = timeTrips[i][1][0];
        var tripname = timeTrips[i][1][1];
        var service_id = timeTrips[i][1][2];
        var timepoint = timeTrips[i][2];
        html = html + "<tr onClick='map.closeInfoWindow();selectTrip(\"" +
            tripid + "\")'>" +
            "<td>" + service_id +
            "<td align='right'>" + (timepoint ? "" : "~") +
            formatTime(time) + "<td>" + tripname + "</tr>";
    }
    html = html + "</table>";

    infoWindow.setContent(html);
    infoWindow.open(map, marker);
}

function leadingZero(digit) {
    if (digit < 10)
        return "0" + digit;
    else
        return "" + digit;
}

function formatTime(secSinceMidnight) {
    var hours = Math.floor(secSinceMidnight / 3600);
    var suffix = "";

    if (twelveHourTime) {
        suffix = (hours >= 12) ? "p" : "a";
        suffix += (hours >= 24) ? " next day" : "";
        hours = hours % 12;
        if (hours == 0)
            hours = 12;
    }
    var minutes = Math.floor(secSinceMidnight / 60) % 60;
    var seconds = secSinceMidnight % 60;
    if (seconds == 0) {
        return hours + ":" + leadingZero(minutes) + suffix;
    } else {
        return hours + ":" + leadingZero(minutes) + ":" + leadingZero(seconds) + suffix;
    }
}

function parseTimeInput() {
    var text = document.getElementById("timeInput").value;
    var m = text.match(/([012]?\d):([012345]?\d)(:([012345]?\d))?/);
    if (m) {
        var seconds = parseInt(m[1], 10) * 3600;
        seconds += parseInt(m[2], 10) * 60;
        if (m[4]) {
            second += parseInt(m[4], 10);
        }
        return seconds;
    } else {
        if (log)
            console.log("Couldn't match " + text + " to time");
        return "";
    }
}

function parseDateInput() {
    var text = document.getElementById("startDateInput").value;
    var m = text.match(/(19|20\d\d)(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])/);
    if (m) {
        return text;
    } else {
        if (log)
            console.log("Couldn't match " + text + " to date");
        return "";
    }
}

/**
 * Create a string of dots that gets longer with the log of count.
 */
function countToRepeatedDots(count) {
    // Find ln_2(count) + 1
    var logCount = Math.ceil(Math.log(count) / 0.693148) + 1;
    return new Array(logCount + 1).join(".");
}

function fetchRoutes() {
    url = "/json/routes";
    downloadUrl(url, callbackDisplayRoutes);
}

function callbackDisplayRoutes(data, responseCode) {
    if (responseCode != 200) {
        patternDiv.appendChild(div);
    }
    var routes = eval(data);
    var routesList = document.getElementById("routeList");
    while (routesList.hasChildNodes()) {
        routesList.removeChild(routesList.firstChild);
    }
    for (i = 0; i < routes.length; ++i) {
        var routeId = routes[i][0];
        var shortName = document.createElement("span");
        shortName.className = "shortName";
        shortName.appendChild(document.createTextNode(routes[i][1] + " "));
        var routeName = routes[i][2];
        var elem = document.createElement("div");
        elem.appendChild(shortName);
        elem.appendChild(document.createTextNode(routeName));
        elem.id = "route_" + routeId;
        elem.className = "routeChoice";
        elem.title = routeName;
        elem.addEventListener("click", makeClosure(selectRoute, routeId));

        var routeContainer = document.createElement("div");
        routeContainer.id = "route_container_" + routeId;
        routeContainer.className = "routeContainer";
        routeContainer.appendChild(elem);
        routesList.appendChild(routeContainer);
    }
}

function selectRoute(routeId) {
    var routesList = document.getElementById("routeList");
    routeSpans = routesList.getElementsByTagName("div");
    for (var i = 0; i < routeSpans.length; ++i) {
        if (routeSpans[i].className == "routeChoiceSelected") {
            routeSpans[i].className = "routeChoice";
        }
    }

    // remove any previously-expanded route
    var tripInfo = document.getElementById("tripInfo");
    if (tripInfo)
        tripInfo.parentNode.removeChild(tripInfo);

    selectedRoute = routeId;
    var span = document.getElementById("route_" + routeId);
    span.className = "routeChoiceSelected";
    fetchPatterns(routeId);
}

function fetchPatterns(routeId) {
    url = "/json/routepatterns?route=" + encodeURIComponent(routeId) + "&time=" + parseTimeInput() + "&date=" + parseDateInput();
    downloadUrl(url, callbackDisplayPatterns);
}

function callbackDisplayPatterns(data, responseCode) {
    if (responseCode != 200) {
        return;
    }
    var div = document.createElement("div");
    div.className = "tripSection";
    div.id = "tripInfo";
    var firstTrip = null;
    var patterns = eval(data);
    clearMap();
    for (i = 0; i < patterns.length; ++i) {
        patternDiv = document.createElement("div")
        patternDiv.className = 'patternSection';
        div.appendChild(patternDiv)
        var pat = patterns[i];  // [patName, patId, len(early trips), trips, len(later trips), has_non_zero_trip_type]
        if (pat[5] == '1') {
            patternDiv.className += " unusualPattern"
        }
        patternDiv.appendChild(document.createTextNode(pat[0]));
        patternDiv.appendChild(document.createTextNode(", " + (pat[2] + pat[3].length + pat[4]) + " trips: "));
        patternDiv.appendChild(document.createElement("br"));
        if (pat[2] > 0) {
            patternDiv.appendChild(document.createTextNode(countToRepeatedDots(pat[2]) + " "));
        }
        for (j = 0; j < pat[3].length; ++j) {
            var trip = pat[3][j];
            var tripId = trip[1];
            if ((i == 0) && (j == 0))
                firstTrip = tripId;
            patternDiv.appendChild(document.createTextNode(" "));
            var span = document.createElement("span");
            span.appendChild(document.createTextNode(formatTime(trip[0])));
            span.id = "trip_" + tripId;
            span.addEventListener("click", makeClosure(selectTrip, tripId));
            patternDiv.appendChild(span)
            span.className = "tripChoice";
        }
        if (pat[4] > 0) {
            patternDiv.appendChild(document.createTextNode(" " + countToRepeatedDots(pat[4])));
        }
        patternDiv.appendChild(document.createElement("br"));
    }
    route = document.getElementById("route_container_" + selectedRoute);
    route.appendChild(div);
    if (tripId != null)
        selectTrip(firstTrip);
}

// Needed to get around limitation in javascript scope rules.
// See http://calculist.blogspot.com/2005/12/gotcha-gotcha.html
function makeClosure(f, a, b, c) {
    return function () {
        f(a, b, c);
    };
}

function make1ArgClosure(f, a, b, c) {
    return function (x) {
        f(x, a, b, c);
    };
}

function make2ArgClosure(f, a, b, c) {
    return function (x, y) {
        f(x, y, a, b, c);
    };
}

function selectTrip(tripId) {
    var tripInfo = document.getElementById("tripInfo");
    if (tripInfo) {
        tripSpans = tripInfo.getElementsByTagName('span');
        for (var i = 0; i < tripSpans.length; ++i) {
            tripSpans[i].className = 'tripChoice';
        }
    }
    var span = document.getElementById("trip_" + tripId);
    // Won't find the span if a different route is selected
    if (span) {
        span.className = 'tripChoiceSelected';
    }
    clearMap();
    url = "/json/tripstoptimes?trip=" + encodeURIComponent(tripId);
    downloadUrl(url, callbackDisplayTripStopTimes);
    fetchTripPolyLine(tripId);
    fetchTripRows(tripId);
}

function callbackDisplayTripStopTimes(data, responseCode) {
    if (responseCode != 200) {
        return;
    }
    var stopsTimes = eval(data);
    if (!stopsTimes) return;
    displayTripStopTimes(stopsTimes[0], stopsTimes[1], stopsTimes[2]);
}

function fetchTripPolyLine(tripId) {
    url = "/json/tripshape?trip=" + encodeURIComponent(tripId);
    downloadUrl(url, callbackDisplayTripPolyLine);
}

function callbackDisplayTripPolyLine(data, responseCode) {
    if (responseCode != 200) {
        return;
    }
    var polyData = JSON.parse(data);
    if (!polyData) return;
    displayPolyLine(polyData);
}

var existingPolylines = [];
var boundsOfPolyLine = null;

function expandBoundingBox(latLng) {
    if (boundsOfPolyLine == null) {
        boundsOfPolyLine = new google.maps.LatLngBounds(latLng, latLng);
    } else {
        boundsOfPolyLine.extend(latLng);
    }
}

/**
 * Display a line given a list of points
 *
 * @param {Object} List of lat,lng pairs along with optional color.
 */
function displayPolyLine(polyData) {
    var points = polyData['points'];
    var linePoints = Array();
    for (i = 0; i < points.length; ++i) {
        var ll = new google.maps.LatLng(points[i][0], points[i][1]);
        expandBoundingBox(ll);
        linePoints[linePoints.length] = ll;
    }
    var color = '#003399';
    if (polyData['color']) {
        color = polyData['color'];
    }
    var polyline = new google.maps.Polyline({
        path: linePoints,
        strokeColor: color,
        strokeWeight: 4,
        map: map
    });
    map.fitBounds(boundsOfPolyLine);
    existingPolylines.push(polyline);
}

function displayTripStopTimes(stops, arrivalTimes, departureTimes) {
    for (i = 0; i < stops.length; ++i) {
        var marker;
        if (arrivalTimes && arrivalTimes[i] != null) {
            label = formatTime(arrivalTimes[i])
            if (departureTimes && departureTimes[i] != null &&
                arrivalTimes[i] != departureTimes[i]) {
                label += '-' + formatTime(departureTimes[i])
            }
            marker = addStopMarkerFromList(stops[i], true, label);
        } else {
            marker = addStopMarkerFromList(stops[i], true);
        }
        expandBoundingBox(marker.getPosition());
    }
    map.fitBounds(boundsOfPolyLine);
}

function fetchTripRows(tripId) {
    url = "/json/triprows?trip=" + encodeURIComponent(tripId);
    downloadUrl(url, make2ArgClosure(callbackDisplayTripRows, tripId));
}

function callbackDisplayTripRows(data, responseCode, tripId) {
    if (responseCode != 200) {
        return;
    }
    var rows = eval(data);
    if (!rows) return;
    var html = "";
    for (var i = 0; i < rows.length; ++i) {
        var filename = rows[i][0];
        var row = rows[i][1];
        html += "<b>" + filename + "</b>: " + formatDictionary(row) + "<br>";
    }
    html += svgTag("/ttablegraph?height=100&trip=" + encodeURIComponent(tripId), "height='115' width='100%'");
    var bottombarDiv = document.getElementById("bottombar");
    bottombarDiv.style.display = "block";
    bottombarDiv.style.height = "175px";
    bottombarDiv.innerHTML = html;
    sizeRouteList();
}

/**
 * Return HTML to embed a SVG object in this page. src is the location of
 * the SVG and attributes is inserted directly into the object or embed
 * tag.
 */
function svgTag(src, attributes) {
    if (navigator.userAgent.toLowerCase().indexOf("msie") != -1) {
        if (isSVGControlInstalled()) {
            return "<embed pluginspage='http://www.adobe.com/svg/viewer/install/' src='" + src + "' " + attributes + "></embed>";
        } else {
            return "<p>Please install the <a href='http://www.adobe.com/svg/viewer/install/'>Adobe SVG Viewer</a> to get SVG support in IE</p>";
        }
    } else {
        return "<object data='" + src + "' type='image/svg+xml' " + attributes + "><p>No SVG support in your browser. Try Firefox 1.5 or newer or install the <a href='http://www.adobe.com/svg/viewer/install/'>Adobe SVG Viewer</a></p></object>";
    }
}

/**
 * Format an Array object containing key-value pairs into a human readable
 * string.
 */
function formatDictionary(d) {
    var output = "";
    var first = 1;
    for (var k in d) {
        if (first) {
            first = 0;
        } else {
            output += "&nbsp;&nbsp; ";
        }
        output += "<b>" + k + "</b>=" + d[k];
    }
    return output;
}


function windowHeight() {
// Standard browsers (Mozilla, Safari, etc.)
    if (self.innerHeight)
        return self.innerHeight;
// IE 6
    if (document.documentElement && document.documentElement.clientHeight)
        return document.documentElement.clientHeight;
// IE 5
    if (document.body)
        return document.body.clientHeight;
// Just in case.
    return 0;
}

function sizeRouteList() {
    var bottombarHeight = 0;
    var bottombarDiv = document.getElementById('bottombar');
    if (bottombarDiv.style.display != 'none') {
        bottombarHeight = document.getElementById('bottombar').offsetHeight
            + document.getElementById('bottombar').style.marginTop;
    }
    var height = windowHeight() - document.getElementById('topbar').offsetHeight - 15 - bottombarHeight;
    document.getElementById('content').style.height = height + 'px';
    if (map) {
        // Without this displayPolyLine does not use the correct map size
        //map.checkResize();
    }
}

var calStartDate = new CalendarPopup();
calStartDate.setReturnFunction("setStartDate");

function maybeAddLeadingZero(number) {
    if (number > 10) {
        return number;
    }
    return '0' + number;
}

function setStartDate(y, m, d) {
    document.getElementById('startDateInput').value = y + maybeAddLeadingZero(m) + maybeAddLeadingZero(d);
}

function downloadUrl(url, callback) {
    if (log) {
        console.log(url);
    }

    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.onload = function (e) {
        if (xhr.readyState === 4) {
            callback(xhr.responseText, xhr.status);
        }
    };
    xhr.onerror = function (e) {
        console.error("", xhr.status);
    };
    xhr.send(null);
}
