/**
* @preserve Maintained by the Office of Web & New Media, Missouri State University, web@missouristate.edu
*
* Usage:
* var Overlay = new missouristate.web.TileOverlay(
*    //function to return the full URL of a tile given a set of tile coordinates and a zoom level.
*    function(x, y, z) { return "http://search.missouristate.edu/map/tilesets/baselayer/" + z + "_" + x + "_" + y + ".png"; },
*    //options with which to initialize the tile layer
*    {
*       'map': map, // optional. google.maps.Map reference.
*       'visible': true, //optional. boolean. controls initial display of the layer.
*       'minZoom': 1, // optional. minimum zoom level at which the tile layer will display.
*       'maxZoom': 19, //optional. maximum zoom level at which the tile layer will display.
*       'bounds': LayerBounds // optional, but strongly encouraged. google.maps.LatLngBounds containing all of the tiles.
*    });
*/

// Setup the namespace
var missouristate = missouristate || {};
missouristate.web = missouristate.web || {};

/**
* @constructor
* @extends {google.maps.OverlayView}
* @param {function(number, number, number): string} GetTileUrl - function that takes 3 params (x, y, z) and returns a full URL to the tile
* @param {?Object} TileOverlayOptions
*/
missouristate.web.TileOverlay = function(GetTileUrl, TileOverlayOptions) {
    this.getTileUrl = GetTileUrl;

    /**
    * @type {google.maps.Map}
    */
    this.map = null;
    this.visible = true;

    if (TileOverlayOptions) {
        TileOverlayOptions['map'] && (this.map = TileOverlayOptions['map']);
        TileOverlayOptions['visible'] && (this.visible = TileOverlayOptions['visible']);
    }

    this.div_ = null;
	/**
    * @const ID
    * @type {google.maps.LatLngBounds}
    */
    this.DIV_ID = (TileOverlayOptions && TileOverlayOptions['id']) ? TileOverlayOptions['id'] : "overlay"+Math.floor(Math.random()*0xffffff);

    /**
    * @const
    * @type {google.maps.LatLngBounds}
    */
    this.BOUNDS = (TileOverlayOptions && TileOverlayOptions['bounds']) ? TileOverlayOptions['bounds'] : null;

    /**
    * @const
    */
    this.MIN_ZOOM = (TileOverlayOptions && TileOverlayOptions['minZoom']) ? TileOverlayOptions['minZoom'] : 1;
    /**
    * @const
    */
    this.MAX_ZOOM = (TileOverlayOptions && TileOverlayOptions['maxZoom']) ? TileOverlayOptions['maxZoom'] : 19;

    //For the current zoom level, keep track of which tiles have already been drawn
    /**
    * @type {Array.<Array.<Element>>}
    */
    this.tilesDrawn = [];
    /**
    * @const
    */
    this.TILE_SIZE = 256;

    /**
    * @const
    */
    this.INITIAL_RESOLUTION = 2 * Math.PI * 6378137 / this.TILE_SIZE;

    /**
    * @const
    */
    this.ORIGIN_SHIFT = 2 * Math.PI * 6378137 / 2.0;

    /**
    * @type {google.maps.MapEventListener}
    */
    this.zoomChangedMapEventListener = null;
    /**
    * @type {google.maps.MapEventListener}
    */
    this.boundsChangedMapEventListener = null;

    /**
    * @type {google.maps.LatLngBounds}
    */
    this.drawnBounds = null;

    this.map && this.setMap(this.map);
}
missouristate.web.TileOverlay.prototype = new google.maps.OverlayView;

/**
* @param {google.maps.LatLng} latLng
* @param {number} zoom
* @returns {google.maps.Point}
* @this {missouristate.web.TileOverlay}
*/
missouristate.web.TileOverlay.prototype.fromLatLngToTileCoordinates = function(latLng, zoom) {
    //LatLng to Meters
    var mx = latLng.lng() * this.ORIGIN_SHIFT / 180.0;
    var my = (Math.log(Math.tan((90 + latLng.lat()) * Math.PI / 360.0)) / (Math.PI / 180.0)) * this.ORIGIN_SHIFT / 180.0;

    //Meters to Pixels
    var res = this.INITIAL_RESOLUTION / Math.pow(2, zoom);
    var px = (mx + this.ORIGIN_SHIFT) / res;
    var py = (my + this.ORIGIN_SHIFT) / res;

    //Pixels to Tile Coords
    var tx = Math.floor(Math.ceil(px / this.TILE_SIZE) - 1);
    var ty = Math.pow(2, zoom) - 1 - Math.floor(Math.ceil(py / this.TILE_SIZE) - 1);

    return new google.maps.Point(tx, ty);
}

/**
* @param {google.maps.Point} coords
* @param {number} zoom
* @returns {google.maps.LatLng}
* @this {missouristate.web.TileOverlay}
*/
missouristate.web.TileOverlay.prototype.fromTileCoordinatesToLatLng = function(coords, zoom) {
    //Tile Coords to Meters
    var res = this.INITIAL_RESOLUTION / Math.pow(2, zoom);
    var mx = (coords.x * this.TILE_SIZE) * res - this.ORIGIN_SHIFT;
    var my = ((Math.pow(2, zoom) - coords.y) * this.TILE_SIZE) * res - this.ORIGIN_SHIFT;

    //Meters to LatLng
    var lng = (mx / this.ORIGIN_SHIFT) * 180.0;
    var lat = 180 / Math.PI * (2 * Math.atan(Math.exp(((my / this.ORIGIN_SHIFT) * 180.0) * Math.PI / 180.0)) - Math.PI / 2.0);

    return new google.maps.LatLng(lat, lng);
}
///////////////////////////////////////////////////////////////////////////////////

/**
* @override
* @this {missouristate.web.TileOverlay}
*/
missouristate.web.TileOverlay.prototype.onAdd = function() {
    this.div_ = document.createElement("DIV");
    this.div_.style.position = "relative";
    this.div_.id = this.DIV_ID;
    if (!this.visible)
        this.div_.style.display = "none";

    this.getPanes().mapPane.appendChild(this.div_);
}

/*
* Cleanup drawn tiles
*/
missouristate.web.TileOverlay.prototype.removeAllTiles = function() {
    if (this.div_ == null)
        return;

    while (this.div_.childNodes.length > 0)
        this.div_.removeChild(this.div_.childNodes[0]);

    this.tilesDrawn = [];
    this.drawnBounds = null;
}

/**
* @override
* @this {missouristate.web.TileOverlay}
*/
missouristate.web.TileOverlay.prototype.draw = function() {
    var z = this.map.getZoom();

    //Calculate the boundaries for the tiles which overlap the viewport
    var viewportBounds = this.map.getBounds();
    var viewportTileCoordsNorthEast = this.fromLatLngToTileCoordinates(viewportBounds.getNorthEast(), z);
    var viewportTileCoordsSouthWest = this.fromLatLngToTileCoordinates(viewportBounds.getSouthWest(), z);
    viewportBounds = new google.maps.LatLngBounds(this.fromTileCoordinatesToLatLng(new google.maps.Point(viewportTileCoordsSouthWest.x, viewportTileCoordsSouthWest.y), z),
		this.fromTileCoordinatesToLatLng(new google.maps.Point(viewportTileCoordsNorthEast.x, viewportTileCoordsNorthEast.y), z));

    //Calculate the boundaries for this layer's tiles at this zoom level
    var TileCoordsNorthEast = this.fromLatLngToTileCoordinates((this.BOUNDS || viewportBounds).getNorthEast(), z);
	if (!this.BOUNDS) TileCoordsNorthEast.x++; //XXX: Weird hack to get the rightmost tiles to load when we don't set a bounds
	
    var TileCoordsSouthWest = this.fromLatLngToTileCoordinates((this.BOUNDS || viewportBounds).getSouthWest(), z);
    if (!this.BOUNDS) TileCoordsSouthWest.y++; //XXX: Weird hack to get the rightmost tiles to load when we don't set a bounds
	
	var TileLatLngBoundsForZoom = new google.maps.LatLngBounds(this.fromTileCoordinatesToLatLng(new google.maps.Point(TileCoordsSouthWest.x, TileCoordsSouthWest.y), z),
		this.fromTileCoordinatesToLatLng(new google.maps.Point(TileCoordsNorthEast.x, TileCoordsNorthEast.y), z));

    //Check to see if there are any tiles defined for this zoom level and if they fall within the viewport
    if (z < this.MIN_ZOOM || z > this.MAX_ZOOM || !viewportBounds.intersects(TileLatLngBoundsForZoom))
        return this.removeAllTiles();

    //If tiles which were previously drawn are now all out of the viewport, start over
    if (this.drawnBounds && !viewportBounds.intersects(this.drawnBounds))
        this.removeAllTiles();
    //Some of the tiles are still displayed. Loop through all the previously drawn tiles and remove those no longer within the viewport
    else if (this.drawnBounds) {
        var drawnNorthEast = this.fromLatLngToTileCoordinates(viewportBounds.getNorthEast(), z);
        var drawnSouthWest = this.fromLatLngToTileCoordinates(viewportBounds.getSouthWest(), z);

        for (var x = drawnNorthEast.x; x <= drawnSouthWest.x; x++)
            for (var y = drawnSouthWest.y; y <= drawnNorthEast.y; y++)
            if (x < viewportTileCoordsNorthEast.x || x > viewportTileCoordsSouthWest.x || y < viewportTileCoordsSouthWest.y || y > viewportTileCoordsNorthEast.y) {
            this.div_.removeChild(this.tilesDrawn["_" + x]["_" + y]);
            delete this.tilesDrawn["_" + x]["_" + y];
        }
    }
    this.drawnBounds = viewportBounds;

    var proj = this.getProjection();
    var viewportNorthEastPixel = proj.fromLatLngToDivPixel(viewportBounds.getNorthEast());
    var viewportSouthWestPixel = proj.fromLatLngToDivPixel(viewportBounds.getSouthWest());

    //Loop through all of the possible viewport tiles and see if we need to draw new tiles
    for (var x = viewportTileCoordsSouthWest.x; x <= viewportTileCoordsNorthEast.x; x++) {
        for (var y = viewportTileCoordsNorthEast.y; y <= viewportTileCoordsSouthWest.y; y++) {
            //Check to see if this is a valid tile for this overlay, and that we haven't already drawn it.
            if (x >= TileCoordsSouthWest.x && x <= TileCoordsNorthEast.x && y >= TileCoordsNorthEast.y && y <= TileCoordsSouthWest.y && (!this.tilesDrawn["_" + x] || !this.tilesDrawn["_" + x]["_" + y])) {
                var img = document.createElement("IMG");
                img.style.cssText = "position:absolute;left:" + (viewportSouthWestPixel.x + ((x - viewportTileCoordsSouthWest.x) * this.TILE_SIZE)) + "px;top:" + (viewportNorthEastPixel.y + ((y - viewportTileCoordsNorthEast.y) * this.TILE_SIZE)) + "px;-moz-user-select:none;-webkit-user-select:none;";
                img.alt = "";
                img.src = this.getTileUrl(x, y, z);
                this.div_.appendChild(img);

                this.tilesDrawn["_" + x] = this.tilesDrawn["_" + x] || [];
                this.tilesDrawn["_" + x]["_" + y] = img;
            }
        }
    }
}

/**
* @override
* @this {missouristate.web.TileOverlay}
*/
missouristate.web.TileOverlay.prototype.setMap = function(Map) {
    //If we are removing the overlay from the map, first remove the div from the map pane. After the call to setMap, this.getPanes() will return null.
    if (Map == null) {
        var Panes = this.getPanes();
        if (Panes)
            Panes.mapPane.removeChild(this.div_);

        this.zoomChangedMapEventListener && google.maps.event.removeListener(this.zoomChangedMapEventListener);
        this.zoomChangedMapEventListener = null;
        this.boundsChangedMapEventListener && google.maps.event.removeListener(this.boundsChangedMapEventListener);
        this.boundsChangedMapEventListener = null;
    }
    else {
        var t = this;
        this.zoomChangedMapEventListener = google.maps.event.addListener(this.map, 'zoom_changed', function() { t.removeAllTiles(); });
        this.boundsChangedMapEventListener = google.maps.event.addListener(this.map, 'idle', function() { t.draw() });
    }

    google.maps.OverlayView.prototype.setMap.call(this, Map);
}

/**
* @override
* @this {missouristate.web.TileOverlay}
*/
missouristate.web.TileOverlay.prototype.onRemove = function() {
    this.removeAllTiles();
    this.div_ = null;
}

/**
* @nosideeffects
* @this {missouristate.web.TileOverlay}
* @returns {boolean}
*/
missouristate.web.TileOverlay.prototype.getVisible = function() {
    return this.visible;
}

/**
* @param {boolean} Visible
* @this {missouristate.web.TileOverlay}
* @returns {boolean}
*/
missouristate.web.TileOverlay.prototype.setVisible = function(Visible) {
    if (this.div_) {
        if (Visible)
            this.div_.style.display = "block";
        else
            this.div_.style.display = "none";
    }

    this.visible = Visible;
}