(function($) {

// Inspired by base2 and Prototype
(function(){
  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
  // The base Class implementation (does nothing)
  this.Class = function(){};
  
  // Create a new Class that inherits from this class
  Class.extend = function(prop) {
    var _super = this.prototype;
    
    // Instantiate a base class (but only create the instance,
    // don't run the init constructor)
    initializing = true;
    var prototype = new this();
    initializing = false;
    
    // Copy the properties over onto the new prototype
    for (var name in prop) {
      // Check if we're overwriting an existing function
      prototype[name] = typeof prop[name] == "function" && 
        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
        (function(name, fn){
          return function() {
            var tmp = this._super;
            
            // Add a new ._super() method that is the same method
            // but on the super-class
            this._super = _super[name];
            
            // The method only need to be bound temporarily, so we
            // remove it when we're done executing
            var ret = fn.apply(this, arguments);        
            this._super = tmp;
            
            return ret;
          };
        })(name, prop[name]) :
        prop[name];
    }
    
    // The dummy class constructor
    function Class() {
      // All construction is actually done in the init method
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
    
    // Populate our constructed prototype object
    Class.prototype = prototype;
    
    // Enforce the constructor to be what we expect
    Class.constructor = Class;

    // And make this class extendable
    Class.extend = arguments.callee;
    
    return Class;
  };
})();

$bind = function(func, obj) {
    return (function() {
        func.apply(obj, arguments);
    }); 
};

$setISO8601 = function (string) {
    var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
        "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" +
        "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
    var d = string.match(new RegExp(regexp));

    var offset = 0;
    var date = new Date(d[1], 0, 1);
    
    if (d[3]) { date.setMonth(d[3] - 1); }
    if (d[5]) { date.setDate(d[5]); }
    if (d[7]) { date.setHours(d[7]); }
    if (d[8]) { date.setMinutes(d[8]); }
    if (d[10]) { date.setSeconds(d[10]); }
    if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
    if (d[14]) { 
        offset = (Number(d[16]) * 60) + Number(d[17]);
        offset *= ((d[15] == '-') ? 1 : -1);
    }

    offset -= date.getTimezoneOffset();
    time = (Number(date) + (offset * 60 * 1000));
    var newDate = new Date();
    newDate.setTime(Number(time));
    return newDate;
};  
    
$dateToPrettyString = function(date) {
    var diff = (((new Date()).getTime() - date.getTime()) / 1000);
    var day_diff = Math.floor(diff / 86400);

    if ( isNaN(day_diff) || day_diff < 0 || day_diff >= 31 )
        return date.toLocaleDateString();

    return day_diff == 0 && (
                    diff < 60 && "just now" ||
                    diff < 120 && "1 minute ago" ||
                    diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
                    diff < 7200 && "1 hour ago" ||
                    diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
            day_diff == 1 && "Yesterday" ||
            day_diff < 7 && day_diff + " days ago" ||
            day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago";
};

var MapLocation = Class.extend({
    data: null,
    _position: null,
    _date: null,
    _prettyDate: null,

    init: function(map, data) {
        this.data = data;

        this.map = map;
        this.id = this.data.id;
        this.latitude = this.data.latitude;
        this.longitude = this.data.longitude;

        var marker = this.mapMarker = new google.maps.Marker({
            position: this.getMapPosition(),
            map: map,
            title: this.getWindowTitle(),
            clickable: this.getIsClickable()
        });
        if (this.getIsClickable())
            google.maps.event.addListener(
                marker,
                'click',
                $bind(this.openInfoWindow, this)
            );
    },

    getWindowTitle: function() { return ""; },
    getWindowContent: function() { return ""; },

    getDateAdded: function() {
        if (this._date != null)
            return this._date;
        this._date = $setISO8601(this.data.datetime);
        return this._date;
    },
    getPrettyDateAdded: function() {
        if (this._prettyDate != null)
            return this._prettyDate;
        this._prettyDate = $dateToPrettyString(this.getDateAdded());
        return this._prettyDate;
    },

    getIsClickable: function() {
        return (this.getWindowContent().length > 0 || this.getWindowTitle().length > 0);
    },

    getMapPosition: function() {
        if (this._position != null)
            return this._position;
        this._position = new google.maps.LatLng(this.latitude, this.longitude);
        return this._position;
    },
    setMapPosition: function(pos) {
        this._position = pos;
    },

    getInfoWindow: function() {
        if (this._infowindow != null)
            return this._infowindow;

        this._infowindow = new google.maps.InfoWindow({ 
            content: "<div id='popup-" + this.id + "' class='popupContent'>" + this.getWindowContent() + "</div>"
        });

        google.maps.event.addListener(
            this._infowindow,
            'domready',
            $bind(this.bindInfoWindowButtons, this)
        );

        return this._infowindow;
    },

    bindInfoWindowButtons: function() {
        var buttons = $j('#popup-' + this.id + ' button');
        var c = buttons.length;
        while (c-- > 0) {
            var button = buttons[c];

            var func = button.className + 'Clicked';
            if (this[func]) {
                $j(button).bind("click", $bind(this[func], this));
            }
        }
    },

    getIconUrl: function() {
        return "/static/images/greenpin-small.png";
    },
    getIcon: function() {
        var url = this.getIconUrl();
        if (MapLocation.Icons[url])
            return MapLocation.Icons[url];

        var icon = MapLocation.Icons[url] = new google.maps.MarkerImage(
            url,
            new google.maps.Size(12, 32),
            new google.maps.Point(0, 0),
            new google.maps.Point(12, 29)
        );
        return icon;
    },

    getShadowUrl: function() {
        return "/static/images/pin-shadow.png";
    },
    getShadow: function() {
        var url = this.getShadowUrl();
        if (MapLocation.Shadows[url])
            return MapLocation.Shadows[url];

        var shadow = MapLocation.Shadows[url] = new google.maps.MarkerImage(url,
            new google.maps.Size(29, 32),
            new google.maps.Point(6, 12),
            new google.maps.Point(6, 16)
        );
        return shadow;
    },

    openInfoWindow: function() {
        if (MapLocation['ActiveInfoWindow'])
            MapLocation.ActiveInfoWindow.close();

        this.getInfoWindow().open(this.map, this.mapMarker);
        MapLocation.ActiveInfoWindow = this.getInfoWindow();
    },
    removeFromMap: function() {
        this.mapMarker.setMap(null);
    }
});
MapLocation.Icons = {};
MapLocation.Shadows = {};

var ParkingLocation = MapLocation.extend({
    _marker: null,
    _infowindow: null,

    getWindowTitle: function() {
        return "";
    },

    deleteClicked: function(e) {
        return jQuery.ajax({
            type:     "DELETE",
            url:      "/location/" + this.id,
            success:  $bind(this.deleteClicked_success, this),
            dataType: "json"
        });
    },
    deleteClicked_success: function() {
        if (MapLocation['ActiveInfoWindow'] == this._infowindow)
            MapLocation.ActiveInfoWindow = null;

        this._infowindow.close();
        this.removeFromMap();
    }
});

var AddressLocation = MapLocation.extend({
    getWindowContent: function() {
        return this.data.address;
    },
    getWindowTitle: function() {
        return "Search Location";
    },
    getIconUrl: function() {
        return "/static/images/redpin-small.png";
    }
});

var ExistingLocation = ParkingLocation.extend({
    _geocodedAddress: null,
    init: function(map, data) {
        this._super(map, data);

        this.isConfirmed = Number(this.data.confirmed) == 1;
        this.isPaid = Number(this.data.paid) == 1;
        this.isPaidString = this.isPaid ? "$" + this.parkingCost + "/hr" : "No";
        this.parkingCost = Number(this.data.parking_cost);
        this.isRearAccessible = Number(this.data.rear_accessible) == 1;
        this.isSideAccessible = Number(this.data.side_accessible) == 1;

        var spaces = Number(this.data.number_of_spaces);
        this.numberOfSpaces = (isNaN(spaces) || !spaces) ? 1 : Number(this.data.number_of_spaces);
        this.numberOfSpacesString = this.numberOfSpaces + (this.numberOfSpaces > 1 ? " spots" : " spot");
    },

    getWindowContent: function() {
        var isSideAccessible = this.isSideAccessible ? "Yes" : "No";
        var isRearAccessible = this.isRearAccessible ? "Yes" : "No";
        var str = "<h2>" + this.getWindowTitle() + "</h2>";
        str += "<h4>" + this.getWindowSubTitle() + "</h4>";
        str += "<table cellspacing='0' cellpadding='1'>";
        str += "<tr><th># Spots:</th><td>" + this.numberOfSpacesString + "</td></tr>";
        str += "<tr><th>Side-ramp accessible:</th><td>" + isSideAccessible + "</td></tr>";
        str += "<tr><th>Rear-ramp accessible:</th><td>" + isRearAccessible + "</td></tr>";
        if (this.getPrettyDateAdded() != null && this.getPrettyDateAdded() != undefined)
            str += "<tr><th>Date Added:</th><td>" + this.getPrettyDateAdded() + "</td></tr>";
        str += "<tr><th>Paid Parking:</th><td>" + this.isPaidString + "</td></tr>";
        if (this.data.description)
            str += "<tr><th>Comments:</th><td></td></tr>";
        str += "</table>";
        if (this.data.description)
            str += "<p>" + this.data.description + "</p>";
        str += "<ol class='actions'>";
        if (PMUser.can_edit_location)
            str += "<li><button class='edit'>Edit</button></li>";
        if (PMUser.can_delete_location)
            str += "<li><button class='delete'>Delete</button></li>";
        str += "</ol>";
        return str;
    },
    getWindowTitle: function() {
        var str = this.numberOfSpacesString;
        if (this.isPaid && this.parkingCost > 0) {
            return str + ", " + this.isPaidString;
        }
        return str;
    },
    getWindowSubTitle: function() {
        if (this._geocodedAddress != null)
            return this._geocodedAddress;
        return this.data.name;
    }
});

var SuggestedLocation = ParkingLocation.extend({
    getWindowContent: function() {
        var str = "<h2>" + this.getWindowTitle() + "</h2>";
        str += "<h4>" + this.getWindowSubTitle() + "</h4>";
        str += "<table cellspacing='0' cellpadding='1'>";
        if (this.getPrettyDateAdded() != null && this.getPrettyDateAdded() != undefined)
            str += "<tr><th>Date Added:</th><td>" + this.getPrettyDateAdded() + "</td></tr>";
        if (this.data.description) {
            str += "<tr><th>Comments:</th><td></td></tr>";
            str += "<tr><td colspan='2'>" + this.data.description + "</td></tr>";
        }
        str += "</table>";
        str += "<ol class='actions'>";
        if (PMUser.can_edit_location)
            str += "<li><button class='edit'>Edit</button></li>";
        if (PMUser.can_delete_location)
            str += "<li><button class='delete'>Delete</button></li>";
        str += "</ol>";
        return str;
    },
    getWindowTitle: function() {
        return "Suggested spot";
    },
    getWindowSubTitle: function() {
        return this.data.name;
    },

    getIconUrl: function() {
        return "/static/images/purplepin-small.png";
    }

});

var ParkingMap = Class.extend({
    map: null,
    mapId: "map_canvas",
    mapFormId: "#MapAddressSearch",
    resizeTimer: null,
    addressSearchPin: null,
    locations: {},
    locationClasses: {
        "CHG::ParkingMobility::Model::Location":           ExistingLocation,
        "CHG::ParkingMobility::Model::LocationSuggestion": SuggestedLocation
    },

    init: function() {
        $j.ajaxSetup({
            async: true,
            timeout: 60000,
            dataType: 'json',
        });

        this.bindEvents();
        this.initializeMap(
            PMLocation.latitude,
            PMLocation.longitude,
            PMLocation.zoom
        );
    },

    bindEvents: function() {
        var addressForm = $j(this.mapFormId)[0];
        $j(addressForm).bind('submit', $bind(this.onSubmit_addressForm, this));

        var addressField = $j(this.mapFormId)[0].address;
        $j(addressField).bind('focus', $bind(this.onFocused_addressField, this));
    },

    onFocused_addressField: function(e) {
        var field = e.target;
        if (field.value == "Enter a Street Address")
            field.value = '';
    },

    onSubmit_addressForm: function(e) {
        this.findAddress(e.target.address.value);
        e.preventDefault();
        return false;
    },

    initializeMap: function(lat, lng, zoom) {
        var mapOptions = {
            zoom: zoom,
            center: new google.maps.LatLng(lat, lng),
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };
        var map_div = document.getElementById(this.mapId);
        this.map = new google.maps.Map(map_div, mapOptions);

        $j(window).bind("resize", $bind(this.resizeMapToWindow, this));
        google.maps.event.addListener(
            this.map,
            'bounds_changed',
            $bind(this.mapViewChanged, this)
        );

        google.maps.event.addListener(this.map, "click", function() { 
            if (MapLocation['ActiveInfoWindow']) {
                MapLocation.ActiveInfoWindow.close();
                MapLocation.ActiveInfoWindow = null;
            }
        }); 

        this.resizeMapToWindow();
    },

    resizeMapToWindow: function() {
        var mapDiv = this.map.getDiv();
        var mapNode = $j(mapDiv);
        var headerHeight = mapNode.offset().top - 1;
        var windowHeight = $j(window).height();
        mapNode.height(windowHeight - headerHeight);
        google.maps.event.trigger(this.map, 'resize');
    },

    mapViewChanged: function() {
        if (this.resizeTimer != null)
            clearTimeout(this.resizeTimer);
        if (this.xhr != null) {
            this.xhr.abort();
            this.xhr = null;
        }
        this.resizeTimer = setTimeout($bind(this.mapViewChanged_fire, this), 500);
    },

    mapViewChanged_fire: function() {
        this.resizeTimer = null;
        var bounds = this.map.getBounds();
        var ne = bounds.getNorthEast();
        var sw = bounds.getSouthWest();
        var data = {
            min_lon: sw.lng(),
            max_lon: ne.lng(),
            min_lat: sw.lat(),
            max_lat: ne.lat()
        };
        this.xhr = $j.getJSON("/map", data, $bind(this.mapViewChanged_success, this));
    },

    mapViewChanged_success: function(data, textStatus) {
        if (data && data["points"]) {
            var points = data.points;
            var c = points.length;
            while (c-- >= 0) {
                var point = points[c];
                if (!point || !point["id"])
                    continue;

                var id = point.id;
                if (this.locations[id])
                    continue;
                this.createLocation(point);
            }
        }
    },

    createLocation: function(data) {
        var classStr = data["__CLASS__"];
        if (!classStr)
            return;
        var matchedClass = classStr.match(/(.*)-/);

        var classRef = this.locationClasses[matchedClass[1]];
        if (!classRef)
            return;

        var spot = new classRef(this.map, data);
        this.locations[spot.id] = spot;
    },

    findAddress: function(address) {
        try {
            if (this.addressSearchPin) {
                try {
                    this.addressSearchPin.removeFromMap();
                } catch(e) {}
                delete this['addressSearchPin'];
            }

            if (!ParkingMap.Geocoder)
                ParkingMap.Geocoder = new google.maps.Geocoder();

            ParkingMap.Geocoder.geocode(
                {
                    address: address,
                    bounds: this.map.getBounds()
                },
                $bind(this.findAddress_callback, this)
            );
        } catch(e) {
            console.log(e);
        }
    },
    findAddress_callback: function(results, status) {
        if (status != google.maps.GeocoderStatus.OK) {
            // TODO Handle error here
            return;
        } 

        var loc = results[0].geometry.location;
        var pin = new AddressLocation(this.map, {
            id:         "FindLocation",
            latitude:   loc.lat(),
            longitude:  loc.lng(),
            address:    results[0].formatted_address
        });

        var vp = results[0].geometry.viewport;
        if (vp) {
            this.map.fitBounds(vp);
        }

        this.addressSearchPin = loc;
    }

});

var parkingmap;
$j(document).ready(function(){
    window.parkingmap = new ParkingMap();
});

})(jQuery);

