/*
 *  GMAP3 Plugin for JQuery 
 *  Version   : 1.2
 *  Date      : December 3, 2010
 *  Licence   : GPL v3 : http://www.gnu.org/licenses/gpl.html  
 *  Author    : DEMONTE Jean-Baptiste
 *  Contact   : jbdemonte@gmail.com
 *  Web site  : http://night-coder.com
 *  
 *  Thanks for mailing me bug, feedback, web integration ...
 *
 *  1.2 - 2010-12-03
 *    fixed : map modification in frm functions (addmarker...) now works
 *    fixed : asynchronous actions (ie: address resolution) were bypassed by synchronous
 *            ie: addMarker[string address], enableScrollWheelZoom => before address is resolved, enableScrollWheelZoom starts
 *            => added a stack manager which push all actions and start next one once previous is finished.
 *            thanks to james for bug report 
 *    added : addStyledMap, setStyledMap      
 *         
 *  1.1 - 2010-11-10
 *    fixed : implicit init doesn't use map parameters
 *    added : getRoute, addDirectionsRenderer, setDirectionsPanel, setDirections
 *  
 *  1.0 - 2010-11-01      
 */
  
$.gmap3 = {

    /***********/
   /* PRIVATE */
  /***********/
  _ids:{},
  
  /****************************************************************************/
  /*                                  STACK
  /* object used to manage action to execute
  /****************************************************************************/
  _running:{
  },
  _stack:{
    _todo:{},
    add: function(id, todo){
      if (!this._todo[id]) this._todo[id] = [];
      this._todo[id].push(todo);
    },
    get: function(id){
      if (this._todo[id]) 
        for(var k in this._todo[id]){
          if (this._todo[id][k]) return this._todo[id][k];
        }
      return false;
    },
    ack: function(id){
      if (this._todo[id]) 
        for(var k in this._todo[id]){                     
          if (this._todo[id][k]) {
            delete this._todo[id][k];
            break;
          }
        }
        if (this.empty(id)) delete this._todo[id];
    },
    empty: function(id){
      if (!this._todo[id]) return true;    
      for(var k in this._todo[id]){
        if (this._todo[id][k]) return false
      }
      return true;
    }
    
  },
  /****************************************************************************/
  
  _default:{
    init:{
      mapTypeId : google.maps.MapTypeId.ROADMAP,
      center:{
        lat: 46.593623,
        lng: 0.342922
      },
      zoom: 10
    }
  },
  
  _geocoder: null,
  
  _getGeocoder: function(){
    if (!this._geocoder) this._geocoder = new google.maps.Geocoder();
    return this._geocoder;
  },
  
  _directionsService: null,
  
  _getDirectionsService: function(){
    if (!this._directionsService) this._directionsService = new google.maps.DirectionsService();
    return this._directionsService;
  },
  
  /**
   * @desc evaluate if the instance exist (if an id has been initialized) 
   * @param id : string instance (dom id)
   * @return bool
   **/
  _exist: function(id){
    return this._ids[ id ] && this._ids[ id ].map ? true : false;
  },
  
  _plan: function(id, todo){
    for(var k in todo) this._stack.add(id, todo[k] );
    this._run(id);    
  },
  
  _run: function(id){
    if (this._running[id]) return;
    var todo = this._stack.get(id);
    if (!todo) return;
    this._running[id] = true;
    this._proceed(id, todo);
  },
  
  _end: function(id){
    delete this._running[id];
    this._stack.ack(id);
    this._run(id);
  },
  _autoInit: function(name){
    var names = [ ':init', 
                  ':geoLatLng', 
                  ':getLatLng', 
                  ':getRoute', 
                  ':addStyledMap', 
                  ':destroy'];
    for(var k in names){
      if (names[k] == name) return false;
    }
    return true;
  },
  /**
   * @desc call functions associated
   * @param
   *  id      : string
   *  action  : string : function wanted
   *     
   *  options : {}
   *     
   *    O1    : {}
   *    O2    : {}
   *    ...
   *    On    : {}
   *      => On : option : {}
   *          action : string : function name
   *          ... (depending of functions called)
   *             
   *  args    : [] : parameters for directs call to map
   *  target? : object : replace map to call function 
   **/
  _proceed: function(id, params){
    var action = (params && params['action']) || ':init';
    var fl = action.substr(0,1);
    if (fl == '_') return; // private function
    
    if ( !this._exist(id) && this._autoInit(action) ){
      this.init(id, $.extend({}, this._default['init'], params['args'] && params['args']['map'] ? params['args']['map'] : {}), true);
    }
    if (fl == ':'){
      // framework functions
      action = action.substr(1);
      if (typeof(this[action]) == 'function'){
        params['out'] = this[action](id, $.extend({}, this._default[action], params['args'] && params['args'] ? params['args'] : [])); // call fnc and extends defaults params
      }
    } else {
      // target of a direct call
      if (params['target']){
        if (typeof(params['target'][action]) == 'function'){
          params['out'] = params['target'][action].apply(params['target'], params && params['args'] ? params['args'] : []);
        }
      // gm direct function :  no result so not rewrited, directly wrapped using array "args" as parameters (ie. enableScrollWheelZoom, addMapType, ...)
      } else if (typeof(this._ids[id].map[action]) == 'function'){
        params['out'] = this._ids[id].map[action].apply(this._ids[id].map, params && params['args'] ? params['args'] : [] );
      }
      this._end(id);
    }
  },
  
  /**
   * @desc call a function of framework or google map object of the instance
   * @param
   *  id    : string : instance
   *  name  : {} : function name
   *  
   *  ... (depending of functions called)
   **/
  _call: function(/* id, fncName [, ...] */){
    if (arguments.length < 2) return;
    if (!this._exist(arguments[0])) return ;
    if (typeof(this._ids[ arguments[0] ].map[ arguments[1] ]) != 'function') return;
    var args = [];
    for(var i=2; i<arguments.length; i++){
      args.push(arguments[i]);
    }
    return this._ids[ arguments[0] ].map[ arguments[1] ].apply( this._ids[ arguments[0] ].map, args );
  },
  
    /**********/
   /* PUBLIC */
  /**********/
  
  /**
   * @desc Destroy an existing instance
   * @param
   *  (id)     
   *  params : {}
   **/
  destroy: function(id, params){
    if ( (id == '') || (!this._exist(id)) ) return false;
    $('#'+id).html('');
    if (this._ids[ id ].dr) delete this._ids[ id ].dr;
    delete this._ids[ id ].map;
    delete this._ids[ id ];
    delete this._running[ id ];
  },
  
  /**
   * @desc Initialize google map object an attach it to the dom element (using id)
   * @param
   *  (id)     
   *  params : {}
   **/
  init: function(id, params, internal){
    if ( (id == '') || (this._exist(id)) ) return false;
    if (params && (typeof(params['center']) == 'boolean') && params['center']) return false; // wait for an address resolution
    var opts = $.extend({}, this._default['init'], params);
    if (!opts['center']) opts['center'] = {lat:this._default.init['center']['lat'], lng:this._default.init['center']['lng']};
    if (typeof(opts['center']['lat']) != 'function'){
      opts['center'] = new google.maps.LatLng(params["center"]["lat"], params["center"]["lng"]);
    }
    if (!this._ids[ id ]) this._ids[ id ] = {};
    this._ids[ id ].map = new google.maps.Map(document.getElementById(id), opts);
    
    // add previous added styles
    if (this._ids[ id ].styles) {
      for(var k in this._ids[ id ].styles){
        this._ids[ id ].map.mapTypes.set(k, this._ids[ id ].styles[k]);
      }
    }
    
    if (params['events']){
      this._attachEvents(id, this._ids[ id ].map, params['events']);  
    }
    
    if (!internal) this._end(id);
    
    return true;
  },
  
  _subcall: function(id, params, latLng){
    if (!params['map']) return;
    if (!this._exist(id))
        this.init(id, $.extend({}, params['map'], {center:latLng}));
    else { 
        if (params['map']['center']) this._call(id, "setCenter", latLng);
        if (typeof(params['map']['zoom']) != 'undefined') this._call(id, "setZoom", params['map']['zoom']);
        if (typeof(params['map']['mapTypeId'])!= 'undefined') this._call(id, "setMapTypeId", params['map']['mapTypeId']);
    }
  },
  
  /**
   * @desc Returns the geographical coordinates from an address
   * @param
   *  (id)     
   *  params : {}
   *    address?  : string  (opt1)
   *    lat?      : float   (opt2)
   *    lng?      : float   (opt2)
   *    latLng?   : LatLng  (opt3)   
   *    callback  : function( GMarker : false )
   *   method : string   
   **/
  _resolveLatLng: function(id, params, method){
    if (params['address']){
        var callback = function(results, status) {
           if (status == google.maps.GeocoderStatus.OK){
            $.gmap3[method](id, params, results[0].geometry.location);
           }
        };
        this._getGeocoder().geocode( { 'address': params['address'] }, callback );
        
    } else if (params['latLng']) {
      this[method](id, params, params['latLng']);
    } else if ( (typeof(params['lat']) != 'undefined') && (typeof(params['lng']) != 'undefined')){
      this[method](id, params, new google.maps.LatLng(params['lat'], params['lng']));
    } else {
      this[method](id, params, false);
    }
  },
  
  
  /**
   * @desc attach an event to a target 
   **/
  _attachEvent: function(id, target, name, f){
    google.maps.event.addListener(target, name, function(event) {
      f(id, target, event);
    });
  },
  
  /**
   * @desc attach events to a target 
   **/
  _attachEvents : function(id, target, evts){
      for(var name in evts){
        if (typeof(evts[name]) == 'function'){
          this._attachEvent(id, target, name, evts[name]);
        }
      }
  },
  
  /**
   * @desc Add a point to a map
   **/
  _addMarker: function(id, params, latLng ){
    if (!latLng) return;
    var marker = false;
    
    this._subcall(id, params, latLng);
    
    var opts = params['marker'] && params['marker']['options'] ? params['marker']['options'] : {};
    opts['position'] = latLng;
    opts['map'] = this._ids[ id ].map;
    
    var marker = new google.maps.Marker(opts);
    
    if ( params['marker'] ){
      if (params['marker']['events']){
        this._attachEvents(id, marker, params['marker']['events']);
      }
      if (params['marker']['methods']){
        for(var k in params['marker']['methods']){
          if (typeof(params['marker']['methods'][k]) == 'function'){
            params['marker']['methods'][k].apply(marker, params['marker']['methods'][k] ? params['marker']['methods'][k] : []);
          }
        }
      }
    }
    
    params['out'] = marker;
    if (typeof(params['callback']) == 'function') params['callback'](id, marker);
    this._end(id);
  },
  
  /**
   * @desc Add a marker
   **/
  addMarker: function(id, params){
    this._resolveLatLng(id, params, '_addMarker');
  },
  
  
  _addInfoWindow: function(id, params, latLng){ 
    this._subcall(id, params, latLng);
    var opts = params['infowindow'] && params['infowindow']['options'] ? params['infowindow']['options'] : {};
    if (latLng) opts['position'] = latLng;
    var infowindow = new google.maps.InfoWindow(opts);
    
    if ( params['infowindow'] && params['infowindow']['events'] ){
        this._attachEvents(id, infowindow, params['infowindow']['events']);
    }
    
    if (params['infowindow'] && params['infowindow']['apply']){
      for(var k in params['infowindow']['apply']){
        var c = params['infowindow']['apply'][k];
        if(!c['action']) continue;
        if (c['action'] == 'open'){
          var args = [this._ids[ id ].map];
          var i = 0;
          for(var k in c['args']) args[++i] = c['args'][k];
        } else {
          var args = c['args'];
        }
        infowindow[c['action']].apply(infowindow, args);
      }
    }
    params['out'] = infowindow;
    if (typeof(params['callback']) == 'function') params['callback'](id, infowindow);
    this._end(id);
  },
  
  addInfoWindow: function(id, params){
    this._resolveLatLng(id, params, '_addInfoWindow');
  },
  
  // replace [ lat, lng ] objet by google.maps.LatLng
  _latLng: function(mixed){
    if ( (typeof(mixed['lat']) != 'undefined') && (typeof(mixed['lat']) != 'function') ){
      return new google.maps.LatLng(mixed['lat'], mixed['lng']);
    } else {
      return mixed;
    }
  },
  
  getRoute: function(id, params){
    if (typeof(params['callback']) == 'function') {
      params['options']['origin']       = this._latLng(params['options']['origin']);
      params['options']['destination']  = this._latLng(params['options']['destination']);
      var callback = function(results, status) {
        params['out'] = status == google.maps.DirectionsStatus.OK ? results : false;
        params['callback'](id, params['out']);
      };
      this._getDirectionsService().route( params['options'], callback );
    }
    this._end(id);
  },
  
  addDirectionsRenderer: function(id, params, internal){
    if (this._ids[ id ].dr) this.removeDirectionsRenderer(id);
    var opts = params['options'] ? params['options'] : {};
    params['options']['map'] = this._ids[ id ].map;
    var panelId = params['options']['panelId'];
    if (panelId) delete params['options']['panelId'];
    this._ids[ id ].dr = new google.maps.DirectionsRenderer(params['options']);
    if (panelId) this._ids[ id ].dr.setPanel(document.getElementById(panelId));
    if (params['events']){
      this._attachEvents(id, this._ids[ id ].dr, params['events']);
    }
    params['out'] = this._ids[ id ].dr;
    if (!internal) this._end(id); 
  },
  
  setDirectionsPanel: function(id, params){
    if (this._ids[ id ].dr && params && params['id']) 
      this._ids[ id ].dr.setPanel(document.getElementById(params['id']));
    this._end(id);
  },
  
  setDirections: function(id, params){
    if (!params['directions']) return;
    if (!this._ids[ id ].dr) 
      this.addDirectionsRenderer(id, {options:params}, true);
    else {
      this._ids[ id ].dr.setDirections(params['directions']);
    }
    this._end(id);
  },
  
  removeDirectionsRenderer: function(id){
    if (this._ids[ id ] && this._ids[ id ].dr) delete this._ids[ id ].dr;
    this._end(id);
  },
  
  addPolyline: function(id, params){
    var opts = params['options'] ? params['options'] : {};
    if (params['path']){
      opts['path'] = [];
      var i = 0; 
      for(var k in params['path']){
        opts['path'][i++] = new google.maps.LatLng(params['path'][k][0], params['path'][k][1]);
      }
    }
    var poly = new google.maps.Polyline(opts);
    if (params['events']){
      this._attachEvents(id, poly, params['events']);
    }
    poly.setMap(this._ids[ id ].map);
    this._end(id);
  },
  
  addPolygon: function(id, params){
    var opts = params['options'] ? params['options'] : {};
    if (params['paths']){
      opts['paths'] = [];
      var i = 0; 
      for(var k in params['paths']){
        opts['paths'][i++] = new google.maps.LatLng(params['paths'][k][0], params['paths'][k][1]);
      }
    }
    var poly = new google.maps.Polygon(opts);
    if (params['events']){
      this._attachEvents(id, poly, params['events']);
    }
    poly.setMap(this._ids[ id ].map);
    this._end(id);
  },
  
  setStreetView: function(id, params){
    var opts = params['options'] ? params['options'] : {};
    var panorama = new  google.maps.StreetViewPanorama(document.getElementById(params['id']),opts);
    this._ids[ id ].map.setStreetView(panorama);
    this._end(id);
  },
  
  setKmlLayer: function(id, params){
    var opts = params['options'] ? params['options'] : {};
    opts['map'] = this._ids[ id ].map;
    var kml = new  google.maps.KmlLayer(params['url'], opts);
    if (params['events']){
      this._attachEvents(id, kml, params['events']);
    }
    this._end(id);
  },
  
  setTrafficLayer: function(id, params){
    var trafficLayer = new  google.maps.TrafficLayer();
    trafficLayer.setMap(this._ids[ id ].map);
    this._end(id);
  },
  
  setBicyclingLayer: function(id, params){
    var bikeLayer = new  google.maps.BicyclingLayer();
    bikeLayer.setMap(this._ids[ id ].map);
    this._end(id);
  },
  
  setGroundOverlay: function(id, params){
    if (typeof(params['bounds']['getCenter']) == 'function'){
        var bounds = params['bounds'];
    } else {
        for(var i=0; i<2; i++){
            if (typeof(params['bounds'][i]['lat']) != 'function'){
                params['bounds'][i] = new google.maps.LatLng(params['bounds'][i]['lat'],params['bounds'][i]['lng']);
            }
        }
        var bounds = new google.maps.LatLngBounds(params['bounds'][0], params['bounds'][1]);
    }
    var overlay = new  google.maps.GroundOverlay(params['url'], bounds);
    if (params['events']){
      this._attachEvents(id, overlay, params['events']);
    }
    overlay.setMap(this._ids[ id ].map);
    this._end(id);
  },
  
  /**
   * @desc Returns the geographical coordinates from an address
   * @param
   *  (id)     
   *  params : {}
   *    address   : string
   *    callback  : function( id, GeocoderResults )
   **/
  getLatLng: function(id, params){
    if (typeof(params['callback']) == 'function') {
      if (params['address']) {
        var callback = function(results, status) {
          params['out'] = status == google.maps.GeocoderStatus.OK ? results : false;
          params['callback'](id, params['out']);
        };
        this._getGeocoder().geocode( { 'address': params['address'] }, callback );
      } else {
          params['out'] = false;
          params['callback'](id, params['out']);
      }
    }
    this._end(id);
  },
  
  
  /**
   * @desc Geolocalise the user and return a LatLng
   * @param
   *  (id)     
   *  params : {}
   *    callback  : function( id, LatLng )
   **/
  geoLatLng: function(id, params){
    if (typeof(params['callback']) == 'function') {
      if(navigator.geolocation) {
        browserSupportFlag = true;
        navigator.geolocation.getCurrentPosition(function(position) {
          params['callback'](id, new google.maps.LatLng(position.coords.latitude,position.coords.longitude));
        }, function() {
          params['callback'](id, false);
        });
      } else if (google.gears) {
        browserSupportFlag = true;
        var geo = google.gears.factory.create('beta.geolocation');
        geo.getCurrentPosition(function(position) {
          params['callback'](id, new google.maps.LatLng(position.latitude,position.longitude));
        }, function() {
          params['callback'](id, false);
        });
      } else {
        params['callback'](id, false);
      }
    }
    this._end(id);
  },
  
  addStyledMap: function(id, params, internal){
    if  (params['style'] && params['id']) {
      params['out'] = new google.maps.StyledMapType(params['style'], params['options']);
      if (!this._ids[ id ]) this._ids[ id ] = {};
      if (!this._ids[ id ].styles) this._ids[ id ].styles = {};
      this._ids[ id ].styles[params['id']] = params['out'];
      if (this._ids[ id ].map) this._ids[ id ].map.mapTypes.set(params['id'], params['out']);
    }
    if (!internal) this._end(id);
  },
  
  setStyledMap: function(id, params){
    if (params['id']) {
      this.addStyledMap(id, params, true);
      if (this._ids[ id ].styles[params['id']])
        this._ids[ id ].map.setMapTypeId(params['id']);
    }
    this._end(id);
  },
  
  setDefault: function(d){
    for(var k in d){
      this._default[k] = $.extend({}, this._default[k], d[k]);
    }
  }
};


jQuery.fn.extend({
  gmap3: function(){
    var $this = $(this);
    if ($this.length > 0){
      var id = $this.attr('id');
      var todo = [];
      for(var i=0; i<arguments.length; i++){
        todo.push(arguments[i] || {});
      }         
      if (!todo.length) todo.push({});
      $.gmap3._plan(id, todo);
    }
    return $(this);
  }	
});
