var GMapWidget = new (Class.create({

    initializeMap: function() {

        this.map = new GMap2(document.getElementById("map"));
        this.map.setCenter(new GLatLng(35.17380831799959, -94.5703125), 4);
        this.map.addControl(new GLargeMapControl3D());
        this.map.enableScrollWheelZoom();

        GEvent.addListener(this.map, 'dragstart', function() {
            this.map.closeInfoWindow();
        }.bind(this));

        GEvent.addListener(this.map, 'zoomend', function() {
            this.map.closeInfoWindow();
            setTimeout(function() { this.checkTypesVisibility(); }.bind(this), 300);
        }.bind(this));

        this.blobHtml = '<div class="centerPopup" id="googleMapWidgetCenterPopup">' +
                            '<h2>#{name}</h2>' +
                            '<img class="type-icon" src="/assets/images/icon-#{type}.gif" />' +
                            '<p>#{line1}</p>' +
                            '<p>#{contry} #{city} #{state} #{postalCode}</p>' +
                            '<p><a href="javascript:GMapWidget.pickLocationFromBlob(#{index});">Select this location</a></p>' +
                        '</div>';

        this.liHtml = '<li class="#{type}" style="background-image: url(/assets/images/marker-#{type}.png);">' +
                           '<h3><a href="#" class="point-show" index="#{index}">#{line1}</a></h3>' +
                           '<p>#{contry} #{city} #{state} #{postalCode}</p>' +
                           '<p><a href="#" class="point-pick" index="#{index}">Pick this location</a></p>' +
                       '</li>';

        this.list = $('marker-list');
        this.holder = $('holder');

        this.GMapMarkerManager = new GMapMarkerManager(this.map);
    },

    putCentersOnMap: function(locations) {

        this.GMapMarkerManager.clearMarkers();
        this.ctgs = { live: [], center: [], liveVisible: true, centerVisible: true };
        this.list.select('li:not(.noitem)').invoke('remove');

        this.locations = locations;

        this.locations.each(function(data, index) {

            var loc = Object.extend(data, { index: index });
            var latLng = new GLatLng(loc.latitude, loc.longitude);
            var zIndex = GOverlay.getZIndex(loc.latitude);

            var countryLevelIcon = new GIcon(G_DEFAULT_ICON);
            countryLevelIcon.image = '/assets/images/marker-' + loc.type + '.png';
            countryLevelIcon.iconSize = new GSize(24, 32);

            var streetLevelIcon = new GIcon(G_DEFAULT_ICON);
            streetLevelIcon.image = '/assets/images/pin-' + loc.type + '.png';
            streetLevelIcon.iconSize = new GSize(79, 53);
            streetLevelIcon.shadow = false;

            var countryLevelMarker = new GMarker(latLng, {
                icon: countryLevelIcon,
                zIndexProcess: function() {
                    return loc.type == 'live' ? (zIndex + 1) : zIndex;
                }
            });
            var streetLevelMarker = new GMarker(latLng, { icon: streetLevelIcon });

            this.ctgs[loc.type].push(countryLevelMarker);
            this.ctgs[loc.type].push(streetLevelMarker);

            this.GMapMarkerManager.addMarker(countryLevelMarker, 4, 13);
            this.GMapMarkerManager.addMarker(streetLevelMarker, 14, 17);

            this.list.insert(this.liHtml.interpolate(loc));

            GEvent.addListener(countryLevelMarker, 'mouseover', function() {
                this.openInfoWindow(loc);
            }.bind(this));

            GEvent.addListener(streetLevelMarker, 'mouseover', function() {
                this.openInfoWindow(loc);
            }.bind(this));

        }, this);

        $$('#marker-list a.point-show').each(function(link) {
            var loc = this.locations[link.readAttribute('index')];
            link.observe('click', function(event) {
                event.stop();
                this.openInfoWindow(loc);
            }.bind(this));
        }, this);

        $$('#marker-list a.point-pick').each(function(link) {
            var loc = this.locations[link.readAttribute('index')];
            link.observe('click', function(event) {
                event.stop();
                this.pickLocationCallback(loc);
            }.bind(this));
        }, this);

    },

    typeVisibility: function(type, visible) {
        this.map.closeInfoWindow();
        this.list[visible ? 'addClassName' : 'removeClassName'](type);
        this.ctgs[type + 'Visible'] = visible;
        this.ctgs[type].each(function(marker) {
            visible ? marker.show() : marker.hide();
        });
    },

    checkTypesVisibility: function() {
        ['live', 'center'].each(function(type) {
            this.typeVisibility(type, this.ctgs[type + 'Visible']);
        }, this);
    },

    pickLocationCallback: function(loc) {
        // To be overwrited from outside
    },

    pickLocationFromBlob: function(index) {
        this.pickLocationCallback(this.locations[index]);
    },

    openInfoWindow: function(loc) {
        var latLng = new GLatLng(loc.latitude, loc.longitude);
        this.map.closeInfoWindow();
        this.map.openInfoWindowHtml(latLng, this.blobHtml.interpolate(loc));
    },

    resizeMap: function() {
        this.holder.toggleClassName('expanded');
        setTimeout(function() {
            this.map.checkResize();
        }.bind(this), 200);
    }

}));

function GMapMarkerManager(map, opt_opts) {
    var me = this;
    me.map_ = map;
    me.mapZoom_ = map.getZoom();
    me.projection_ = map.getCurrentMapType().getProjection();

    opt_opts = opt_opts || {};
    me.tileSize_ = GMapMarkerManager.DEFAULT_TILE_SIZE_;

    var maxZoom = GMapMarkerManager.DEFAULT_MAX_ZOOM_;
    if (opt_opts.maxZoom != undefined) {
        maxZoom = opt_opts.maxZoom;
    }
    me.maxZoom_ = maxZoom;

    me.trackMarkers_ = opt_opts.trackMarkers;

    var padding;
    if (typeof opt_opts.borderPadding == "number") {
        padding = opt_opts.borderPadding;
    } else {
        padding = GMapMarkerManager.DEFAULT_BORDER_PADDING_;
    }
    // The padding in pixels beyond the viewport, where we will pre-load markers.
    me.swPadding_ = new GSize(-padding, padding);
    me.nePadding_ = new GSize(padding, -padding);
    me.borderPadding_ = padding;

    me.gridWidth_ = [];

    me.grid_ = [];
    me.grid_[maxZoom] = [];
    me.numMarkers_ = [];
    me.numMarkers_[maxZoom] = 0;

    GEvent.bind(map, "moveend", me, me.onMapMoveEnd_);

    me.removeOverlay_ = function(marker) {
        map.removeOverlay(marker);
        me.shownMarkers_--;
    };
    me.addOverlay_ = function(marker) {
        map.addOverlay(marker);
        me.shownMarkers_++;
    };

    me.resetManager_();
    me.shownMarkers_ = 0;

    me.shownBounds_ = me.getMapGridBounds_();
};

GMapMarkerManager.DEFAULT_TILE_SIZE_ = 1024;
GMapMarkerManager.DEFAULT_MAX_ZOOM_ = 17;
GMapMarkerManager.DEFAULT_BORDER_PADDING_ = 100;
GMapMarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE = 256;

GMapMarkerManager.prototype.resetManager_ = function() {
    var me = this;
    var mapWidth = GMapMarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE;
    for (var zoom = 0; zoom <= me.maxZoom_; ++zoom) {
        me.grid_[zoom] = [];
        me.numMarkers_[zoom] = 0;
        me.gridWidth_[zoom] = Math.ceil(mapWidth / me.tileSize_);
        mapWidth <<= 1;
    }
};

GMapMarkerManager.prototype.clearMarkers = function() {
    var me = this;
    me.processAll_(me.shownBounds_, me.removeOverlay_);
    me.resetManager_();
};

GMapMarkerManager.prototype.getTilePoint_ = function(latlng, zoom, padding) {
    var pixelPoint = this.projection_.fromLatLngToPixel(latlng, zoom);
    return new GPoint(
            Math.floor((pixelPoint.x + padding.width) / this.tileSize_),
            Math.floor((pixelPoint.y + padding.height) / this.tileSize_));
};

GMapMarkerManager.prototype.addMarkerBatch_ = function(marker, minZoom, maxZoom) {
    var mPoint = marker.getPoint();
    // Tracking markers is expensive, so we do this only if the
    // user explicitly requested it when creating marker manager.
    if (this.trackMarkers_) {
        GEvent.bind(marker, "changed", this, this.onMarkerMoved_);
    }

    var gridPoint = this.getTilePoint_(mPoint, maxZoom, GSize.ZERO);

    for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
        var cell = this.getGridCellCreate_(gridPoint.x, gridPoint.y, zoom);
        cell.push(marker);

        gridPoint.x = gridPoint.x >> 1;
        gridPoint.y = gridPoint.y >> 1;
    }
};

GMapMarkerManager.prototype.isGridPointVisible_ = function(point) {
    var me = this;
    var vertical = me.shownBounds_.minY <= point.y &&
                   point.y <= me.shownBounds_.maxY;
    var minX = me.shownBounds_.minX;
    var horizontal = minX <= point.x && point.x <= me.shownBounds_.maxX;
    if (!horizontal && minX < 0) {
        // Shifts the negative part of the rectangle. As point.x is always less
        // than grid width, only test shifted minX .. 0 part of the shown bounds.
        var width = me.gridWidth_[me.shownBounds_.z];
        horizontal = minX + width <= point.x && point.x <= width - 1;
    }
    return vertical && horizontal;
};

GMapMarkerManager.prototype.onMarkerMoved_ = function(marker, oldPoint, newPoint) {
    var me = this;
    var zoom = me.maxZoom_;
    var changed = false;
    var oldGrid = me.getTilePoint_(oldPoint, zoom, GSize.ZERO);
    var newGrid = me.getTilePoint_(newPoint, zoom, GSize.ZERO);
    while (zoom >= 0 && (oldGrid.x != newGrid.x || oldGrid.y != newGrid.y)) {
        var cell = me.getGridCellNoCreate_(oldGrid.x, oldGrid.y, zoom);
        if (cell) {
            if (me.removeFromArray(cell, marker)) {
                me.getGridCellCreate_(newGrid.x, newGrid.y, zoom).push(marker);
            }
        }
        if (zoom == me.mapZoom_) {
            if (me.isGridPointVisible_(oldGrid)) {
                if (!me.isGridPointVisible_(newGrid)) {
                    me.removeOverlay_(marker);
                    changed = true;
                }
            } else {
                if (me.isGridPointVisible_(newGrid)) {
                    me.addOverlay_(marker);
                    changed = true;
                }
            }
        }
        oldGrid.x = oldGrid.x >> 1;
        oldGrid.y = oldGrid.y >> 1;
        newGrid.x = newGrid.x >> 1;
        newGrid.y = newGrid.y >> 1;
        --zoom;
    }
    if (changed) {
        me.notifyListeners_();
    }
};

GMapMarkerManager.prototype.removeMarker = function(marker) {
    var me = this;
    var zoom = me.maxZoom_;
    var changed = false;
    var point = marker.getPoint();
    var grid = me.getTilePoint_(point, zoom, GSize.ZERO);
    while (zoom >= 0) {
        var cell = me.getGridCellNoCreate_(grid.x, grid.y, zoom);

        if (cell) {
            me.removeFromArray(cell, marker);
        }

        if (zoom == me.mapZoom_) {
            if (me.isGridPointVisible_(grid)) {
                me.removeOverlay_(marker);
                changed = true;
            }
        }
        grid.x = grid.x >> 1;
        grid.y = grid.y >> 1;
        --zoom;
    }
    if (changed) {
        me.notifyListeners_();
    }
};

GMapMarkerManager.prototype.addMarkers = function(markers, minZoom, opt_maxZoom) {
    var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
    for (var i = markers.length - 1; i >= 0; i--) {
        this.addMarkerBatch_(markers[i], minZoom, maxZoom);
    }

    this.numMarkers_[minZoom] += markers.length;
};

GMapMarkerManager.prototype.getOptMaxZoom_ = function(opt_maxZoom) {
    return opt_maxZoom != undefined ? opt_maxZoom : this.maxZoom_;
};

GMapMarkerManager.prototype.getMarkerCount = function(zoom) {
    var total = 0;
    for (var z = 0; z <= zoom; z++) {
        total += this.numMarkers_[z];
    }
    return total;
};

GMapMarkerManager.prototype.addMarker = function(marker, minZoom, opt_maxZoom) {
    var me = this;
    var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
    me.addMarkerBatch_(marker, minZoom, maxZoom);
    var gridPoint = me.getTilePoint_(marker.getPoint(), me.mapZoom_, GSize.ZERO);
    if (me.isGridPointVisible_(gridPoint) &&
        minZoom <= me.shownBounds_.z &&
        me.shownBounds_.z <= maxZoom) {
        me.addOverlay_(marker);
        me.notifyListeners_();
    }
    this.numMarkers_[minZoom]++;
};

GBounds.prototype.containsPoint = function(point) {
    var outer = this;
    return (outer.minX <= point.x &&
            outer.maxX >= point.x &&
            outer.minY <= point.y &&
            outer.maxY >= point.y);
};

GMapMarkerManager.prototype.getGridCellCreate_ = function(x, y, z) {
    var grid = this.grid_[z];
    if (x < 0) {
        x += this.gridWidth_[z];
    }
    var gridCol = grid[x];
    if (!gridCol) {
        gridCol = grid[x] = [];
        return gridCol[y] = [];
    }
    var gridCell = gridCol[y];
    if (!gridCell) {
        return gridCol[y] = [];
    }
    return gridCell;
};

GMapMarkerManager.prototype.getGridCellNoCreate_ = function(x, y, z) {
    var grid = this.grid_[z];
    if (x < 0) {
        x += this.gridWidth_[z];
    }
    var gridCol = grid[x];
    return gridCol ? gridCol[y] : undefined;
};

GMapMarkerManager.prototype.getGridBounds_ = function(bounds, zoom, swPadding,
                                                  nePadding) {
    zoom = Math.min(zoom, this.maxZoom_);

    var bl = bounds.getSouthWest();
    var tr = bounds.getNorthEast();
    var sw = this.getTilePoint_(bl, zoom, swPadding);
    var ne = this.getTilePoint_(tr, zoom, nePadding);
    var gw = this.gridWidth_[zoom];

    if (tr.lng() < bl.lng() || ne.x < sw.x) {
        sw.x -= gw;
    }
    if (ne.x - sw.x + 1 >= gw) {
        sw.x = 0;
        ne.x = gw - 1;
    }
    var gridBounds = new GBounds([sw, ne]);
    gridBounds.z = zoom;
    return gridBounds;
};

GMapMarkerManager.prototype.getMapGridBounds_ = function() {
    var me = this;
    return me.getGridBounds_(me.map_.getBounds(), me.mapZoom_,
            me.swPadding_, me.nePadding_);
};

GMapMarkerManager.prototype.onMapMoveEnd_ = function() {
    var me = this;
    me.objectSetTimeout_(this, this.updateMarkers_, 0);
};

GMapMarkerManager.prototype.objectSetTimeout_ = function(object, command, milliseconds) {
    return window.setTimeout(function() {
        command.call(object);
    }, milliseconds);
};

GMapMarkerManager.prototype.refresh = function() {
    var me = this;
    if (me.shownMarkers_ > 0) {
        me.processAll_(me.shownBounds_, me.removeOverlay_);
    }
    me.processAll_(me.shownBounds_, me.addOverlay_);
    me.notifyListeners_();
};

GMapMarkerManager.prototype.updateMarkers_ = function() {
    var me = this;
    me.mapZoom_ = this.map_.getZoom();
    var newBounds = me.getMapGridBounds_();

    if (newBounds.equals(me.shownBounds_) && newBounds.z == me.shownBounds_.z) {
        return;
    }

    if (newBounds.z != me.shownBounds_.z) {
        me.processAll_(me.shownBounds_, me.removeOverlay_);
        me.processAll_(newBounds, me.addOverlay_);
    } else {
        me.rectangleDiff_(me.shownBounds_, newBounds, me.removeCellMarkers_);
        me.rectangleDiff_(newBounds, me.shownBounds_, me.addCellMarkers_);
    }
    me.shownBounds_ = newBounds;

    me.notifyListeners_();
};

GMapMarkerManager.prototype.notifyListeners_ = function() {
    GEvent.trigger(this, "changed", this.shownBounds_, this.shownMarkers_);
};

GMapMarkerManager.prototype.processAll_ = function(bounds, callback) {
    for (var x = bounds.minX; x <= bounds.maxX; x++) {
        for (var y = bounds.minY; y <= bounds.maxY; y++) {
            this.processCellMarkers_(x, y, bounds.z, callback);
        }
    }
};

GMapMarkerManager.prototype.processCellMarkers_ = function(x, y, z, callback) {
    var cell = this.getGridCellNoCreate_(x, y, z);
    if (cell) {
        for (var i = cell.length - 1; i >= 0; i--) {
            callback(cell[i]);
        }
    }
};

GMapMarkerManager.prototype.removeCellMarkers_ = function(x, y, z) {
    this.processCellMarkers_(x, y, z, this.removeOverlay_);
};

GMapMarkerManager.prototype.addCellMarkers_ = function(x, y, z) {
    this.processCellMarkers_(x, y, z, this.addOverlay_);
};

GMapMarkerManager.prototype.rectangleDiff_ = function(bounds1, bounds2, callback) {
    var me = this;
    me.rectangleDiffCoords(bounds1, bounds2, function(x, y) {
        callback.apply(me, [x, y, bounds1.z]);
    });
};

GMapMarkerManager.prototype.rectangleDiffCoords = function(bounds1, bounds2, callback) {
    var minX1 = bounds1.minX;
    var minY1 = bounds1.minY;
    var maxX1 = bounds1.maxX;
    var maxY1 = bounds1.maxY;
    var minX2 = bounds2.minX;
    var minY2 = bounds2.minY;
    var maxX2 = bounds2.maxX;
    var maxY2 = bounds2.maxY;

    for (var x = minX1; x <= maxX1; x++) {  // All x in R1
        // All above:
        for (var y = minY1; y <= maxY1 && y < minY2; y++) {  // y in R1 above R2
            callback(x, y);
        }
        // All below:
        for (var y = Math.max(maxY2 + 1, minY1); // y in R1 below R2
             y <= maxY1; y++) {
            callback(x, y);
        }
    }

    for (var y = Math.max(minY1, minY2);
         y <= Math.min(maxY1, maxY2); y++) {  // All y in R2 and in R1
        for (var x = Math.min(maxX1 + 1, minX2) - 1;
             x >= minX1; x--) {  // x in R1 left of R2
            callback(x, y);
        }
        for (var x = Math.max(minX1, maxX2 + 1); // x in R1 right of R2
             x <= maxX1; x++) {
            callback(x, y);
        }
    }
};

GMapMarkerManager.prototype.removeFromArray = function(array, value, opt_notype) {
    var shift = 0;
    for (var i = 0; i < array.length; ++i) {
        if (array[i] === value || (opt_notype && array[i] == value)) {
            array.splice(i--, 1);
            shift++;
        }
    }
    return shift;
};
