// Copyright 2009 Google Inc. All Right Reserved.

/**
 * @fileoverview China Google Maps Favorite Places campaign.
 * @author zackyma@google.com (Zacky Ma)
 */

/**
 * Namespace gweb (Global namespace for Google static pages).
 */
var gweb = gweb || {};

/**
 * Namespace gweb.favpalces (Namespace for Favorite Places landing page).
 */
gweb.favplaces = gweb.favplaces || {};

/**
 * Class creating Favorite Places landing page.
 * @param {Object.<string>} config Configuration of the landing page.
 * @constructor
 */
gweb.favplaces.App = function(config) {
  /**
   * Clone of external config object.
   * @type {!Object}
   * @private
   */
  this.config_ = config;

  /**
   * The whole my maps list.
   * @type {Object.<Array>}
   * @private
   */
  this.maps_ = {};

  /**
   * List of rendered cities.
   * @type {Object.<boolean>}
   * @private
   */
  this.renderedCities_ = {};

  /**
   * Counts of cities and maps under each city.
   * @type {Object.<(number|Object.<number>)>}
   * @private
   */
  this.count_ = {
    'city': 0,
    'authors': {}
  };

  /**
   * Store the displaying city and displaying map under each city.
   * @type {Object.<(string|Object.<string>)>}
   * @private
   */
  this.on_ = {
    'city': '',
    'authors': {}
  };

  /**
   * Store the x-axis value of the city lists.
   * @type {Object.<number>}
   * @private
   */
  this.cityListX_ = {};

  /**
   * Store the y-axis value of the city lists.
   * @type {Object.<number>}
   * @private
   */
  this.cityListY_ = {};

  /**
   * Store left nav list container width value.
   * @type {number}
   * @private
   */
  this.listContainerWidth_ = this.config_.dom.listContainer.offsetWidth;

  /**
   * The map which is displaying.
   * @type {GMap2}
   */
  this.gmap = null;
};

/**
 * Intialize the landing page.
 * @param {Object} json JSON feed generated.
 */
gweb.favplaces.App.prototype.init = function(json) {
  this.parse_(json);
  this.renderCityLists_();

  google.load('maps', '2', {
    'base_domain': 'ditu.google.cn',
    'language': 'zh-CN'
  });
  google.load('feeds', '1');

  google.setOnLoadCallback(gweb.bind(function() {
    this.gmap = new google.maps.Map2(this.config_.dom.map);
    this.gmap.addControl(new GMenuMapTypeControl());
    this.gmap.addControl(new GSmallMapControl());
    this.gmap.addControl(new GScaleControl());
    this.gmap.enableScrollWheelZoom();
    this.gmap.enableContinuousZoom();

    var defaultCity = this.config_.defaultCity;
    if (location.hash) {
      var cityMatch = /city\=([^&]+)/.exec(location.hash);
      var authorMatch = /author\=([^&]+)/.exec(location.hash);
      if (cityMatch && cityMatch[1] && this.maps_[cityMatch[1]]) {
        defaultCity = cityMatch[1];
      }
      if (authorMatch && authorMatch[1] &&
          this.getAuthor_(defaultCity, authorMatch[1])) {
        this.on_.authors[defaultCity] = authorMatch[1];
      }
    }
    this.switchCity(defaultCity);
  }, this));
};

/**
 * Parse the JSON object generated by Google Spreadsheet and
 * build the master maps object.
 * @param {Object} json JSON object to be parsed.
 * @private
 */
gweb.favplaces.App.prototype.parse_ = function(json) {
  if (json.feed.entry.length < 1) {
    return;
  }

  var regexNL = /\n/g;
  for (var i = 0, feedLen = json.feed.entry.length; i < feedLen; i++) {
    var entry = json.feed.entry[i];
    var hash = {};

    for (var mem in entry) {
      if (gweb.string.startsWith(mem, 'gsx$')) {
        var key = mem.split('$')[1];
        var value = entry[mem].$t.replace(regexNL, '');
        hash[key] = value;
      }
    }
    hash.snippet_cut = hash.snippet.length > 15 ?
        hash.snippet.substring(0, 13) + '...' :
        hash.snippet;

    if (!this.maps_[hash.city]) {
      this.maps_[hash.city] = [];
      this.renderedCities_[hash.city] = false;
      this.count_.city++;
      this.count_.authors[hash.city] = 0;
      this.on_.authors[hash.city] = '';
    }
    hash.order = this.count_.authors[hash.city];
    this.maps_[hash.city].push(hash);
    this.count_.authors[hash.city]++;
  }
};

/**
 * Generate HTML code for the city lists container and insert into DOM.
 * @private
 */
gweb.favplaces.App.prototype.renderCityLists_ = function() {
  var html = [];
  var cityCount = 0;
  var listsElem = this.config_.dom.listContainer;

  for (city in this.maps_) {
    html.push(gweb.string.subs(
        '<ul id="city-%s" style="left:%spx;top:0px;"></ul>',
        city,
        cityCount * this.listContainerWidth_));
    this.cityListX_[city] = this.listContainerWidth_ * cityCount;
    cityCount++;
  }

  listsElem.style.width = this.listContainerWidth_ * cityCount + 'px';
  listsElem.innerHTML = html.join('');
};

/**
 * Generate HTML code for one map list and insert into DOM.
 * @param {string} city The city which is gonna be rendered.
 * @private
 */
gweb.favplaces.App.prototype.renderMapList_ = function(city) {
  if (this.maps_[city] && !this.renderedCities_[city]) {
    var html = [];
    for (var i = 0, mapLen = this.maps_[city].length; i < mapLen; i++) {
      html.push(gweb.favplaces.util.templatize(this.config_.tpl.author,
          this.maps_[city][i]));
    }
    gweb.dom.getElement('city-' + city).innerHTML = html.join('');
    this.renderedCities_[city] = true;
  }
};

/**
 * Get the author object by a given id.
 * @param {string} city The author's city.
 * @param {string} id The author's id.
 * @return {(Object|boolean)} Author object queried by city and id.
 * @private
 */
gweb.favplaces.App.prototype.getAuthor_ = function(city, id) {
  var authorList = this.maps_[city];
  for (var i = 0, authorListLen = authorList.length; i < authorListLen; i++) {
    if (authorList[i].id == id) {
      return authorList[i];
    }
  }
  return false;
};

/**
 * Display the map list of a city.
 * @param {string} city The city which is gonna to display.
 */
gweb.favplaces.App.prototype.switchCity = function(city) {
  this.config_.dom.cityNameHeading.innerHTML = this.config_.cities[city].name;

  if (this.on_.city) {
    gweb.dom.getElement('nav-city-' + this.on_.city).className = '';
  }
  gweb.dom.getElement('nav-city-' + city).className = 'on';

  this.gmap.clearOverlays();
  this.renderMapList_(city);

  gweb.fx.slideX(this.config_.dom.listContainer, (0 - this.cityListX_[city]),
    gweb.bind(function() {
      var toOpenMap = '';
      if (this.on_.authors[city] != '') {
        toOpenMap = this.on_.authors[city];
      } else {
        toOpenMap = this.maps_[city][0].id;
      }
      this.openMap(city, toOpenMap, this.getAuthor_(city, toOpenMap).msid);
      this.on_.city = city;
  }, this), 500);
};

/**
 * Scrolling effect.
 * @param {string} city The city which is to be scrolled.
 * @param {number} newListTop ListTop to scroll to (in px).
 * @param {Function=} opt_callback Callback function after scrolling finished.
 * @private
 */
gweb.favplaces.App.prototype.scroll_ = function(city, newListTop,
    opt_callback) {
  var listElem = gweb.dom.getElement('city-' + city);

  if (!newListTop ||
      listElem.offsetHeight <= this.config_.dom.listContainer.offsetHeight) {
    newListTop = 0;
  }
  var callback = opt_callback || null;
  var maxAllowedListTop = this.config_.dom.listContainer.offsetHeight
      - listElem.offsetHeight;
  maxAllowedListTop = maxAllowedListTop > 0 ? 0 : maxAllowedListTop;

  if (newListTop >= 0) {
    newListTop = 0;
    gweb.dom.classes.add(this.config_.dom.navPrev, 'disabled');
  } else {
    gweb.dom.classes.remove(this.config_.dom.navPrev, 'disabled');
  }

  if (newListTop <= maxAllowedListTop) {
    newListTop = maxAllowedListTop;
    gweb.dom.classes.add(this.config_.dom.navNext, 'disabled');
  } else {
    gweb.dom.classes.remove(this.config_.dom.navNext, 'disabled');
  }

  gweb.fx.slideY(listElem, newListTop, callback, 700);
};

/**
 * Get (@code style.top) value of the on city's map list.
 * @return {number} The list's style.top value.
 * @private
 */
gweb.favplaces.App.prototype.getOnCityListTop_ = function() {
  var city = this.on_.city;
  if (city) {
    var listElem = gweb.dom.getElement('city-' + city);
    return parseFloat(listElem.style.top);
  } else {
    return 0;
  }
};

/**
 * Show and hide loading overlay.
 * @param {boolean} isShow Show or not show the loading layer.
 * @private
 */
gweb.favplaces.App.prototype.visualizeLoading_ = function(isShow) {
  this.config_.dom.loading.style.display = isShow ? 'block' : 'none';
};

/**
 * Paginate current displaying map list.
 * @param {boolean} isNext Go to next page or previous.
 */
gweb.favplaces.App.prototype.paginate = function(isNext) {
  var listTop = this.getOnCityListTop_();
  var distance = this.config_.dom.listContainer.offsetHeight;
  distance = isNext ? 0 - distance : distance;
  this.scroll_(this.on_.city, listTop + distance);
};

/**
 * Load a map from georss feed.
 * @param {Object} author The selected author object.
 * @param {Function=} opt_callback Callback function after map loaded.
 * @private
 */
gweb.favplaces.App.prototype.loadMap_ = function(author, opt_callback) {
  this.visualizeLoading_(true);

  var city = author.city, id = author.id, msid = author.msid
  var mapPin = 'images/map_pins/' + city + '/' + id + '_pin.png';
  var mapFeed = 'http://maps.google.com/maps/ms?msa=0&gl=cn&hl=zh-CN&' +
      'msid=' + msid + '&output=georss';

  var icon = new GIcon(G_DEFAULT_ICON);
  icon.image = mapPin;
  icon.shadow = 'images/shadow.png';
  icon.iconSize = new GSize(35, 35);
  icon.shadowSize = new GSize(45, 34);

  author.icon = icon;
  author.places = [];

  var infoWindowTPL = this.config_.tpl.infoWindow;
  var feed = new google.feeds.Feed(mapFeed);
  feed.setResultFormat(google.feeds.Feed.MIXED_FORMAT);
  feed.setNumEntries(50);
  feed.load(function(data) {
    if (!data.error) {
      for (var i = 0, entryLen = data.feed.entries.length;
          i < entryLen; i++) {
        var entry = data.feed.entries[i];
        var latlng = entry.xmlNode.childNodes[1].firstChild.data;
        if (!latlng) {
          continue;
        }
        author.places.push({
          'point': new GLatLng(
            latlng.split(' ')[0],
            latlng.split(' ')[1]
          ),
          'info': gweb.favplaces.util.templatize(
            infoWindowTPL, {
              'title': entry.title,
              'content': entry.content,
              'msid': msid
          })
        });
      }
      if (opt_callback) {
        opt_callback();
      }
    }
  });
};

/**
 * Render a loaded map.
 * @param {Object} author The selected author.
 * @private
 */
gweb.favplaces.App.prototype.renderMap_ = function(author) {
  var gmap = this.gmap;
  gmap.clearOverlays();

  var bounds = new GLatLngBounds();
  var firstMarker;
  var markerClickCallback = this.config_.markerClickCallback;

  for (var i = 0, placesLen = author.places.length; i < placesLen; i++) {
    gmap.addOverlay(function(index) {
      var marker = new GMarker(author.places[i].point, {'icon': author.icon});
      var info = author.places[i].info;
      GEvent.addListener(marker, 'click', function() {
        marker.openInfoWindowHtml(info);
        if (markerClickCallback) {
          markerClickCallback(author);
        }
        gmap.savePosition();
      });
      if (index == 0) {
        firstMarker = marker;
      }
      return marker;
    }(i));
    bounds.extend(author.places[i].point);
  }
  gmap.setCenter(bounds.getCenter());
  gmap.setZoom(gmap.getBoundsZoomLevel(bounds));

  var mw = gmap.getSize().width;
  var mh = gmap.getSize().height;
  var ne = gmap.fromLatLngToDivPixel(bounds.getNorthEast());
  var sw = gmap.fromLatLngToDivPixel(bounds.getSouthWest());
  var test_ne = (ne.x < (mw - 99) && ne.y > 25) ||
                (ne.x < (mw - 9) && ne.y > 70);
  var test_sw = (sw.y < (mw - 29)) ||
                (sw.x < 45 && (ne.y > 120 && ne.y < (mh - 34)));
  if (!(test_ne && test_sw)) {
    gmap.setZoom(gmap.getZoom() - 1);
  }
  if (!gmap.getZoom()) {
    gmap.setZoom(1);
  }

  GEvent.addListener(gmap, 'infowindowclose', function() {
    gmap.returnToSavedPosition();
  });

  if (this.config_.flags.openFirstInfoWindowByDefault) {
    firstMarker.openInfoWindowHtml(author.places[0].info);
  }
};

/**
 * Show selected author's map.
 * @param {Object} author The selected author.
 * @private
 */
gweb.favplaces.App.prototype.showMap_ = function(author) {
  if (!author.places || author.places.length == 0) {
    this.loadMap_(author, gweb.bind(function(author) {
      this.renderMap_(author);
      this.visualizeLoading_(false);
    }, this, author));
  } else {
    this.renderMap_(author);
  }
};

/**
 * Open a map.
 * @param {string} city The city that the author belongs to.
 * @param {string} id Author's ID.
 */
gweb.favplaces.App.prototype.openMap = function(city, id) {
  if (city == this.on_.city && this.on_.authors[city] == id) {
    return;
  }

  this.gmap.clearOverlays();

  var authorEl = gweb.dom.getElement('author-' + city + '-' + id);
  if (this.on_.authors[city] != '') {
    gweb.dom.classes.remove(
        gweb.dom.getElement('author-' + city + '-' + this.on_.authors[city]),
        'on');
  }
  gweb.dom.classes.add(authorEl, 'on');
  this.on_.authors[city] = id;

  var author = this.getAuthor_(city, id);
  var newListTop = gweb.dom.getElement('city-' + city).offsetTop -
                  ((authorEl.offsetTop +
                  gweb.dom.getElement('city-' + city).offsetTop) -
                  (this.config_.dom.listContainer.offsetHeight / 2) +
                  (authorEl.offsetHeight / 2));
  this.scroll_(city, newListTop, gweb.bind(function() {
    this.showMap_(author);
  }, this));
};

/**
 * Namespace gweb.favplaces.util (Namespace for common methods).
 */
gweb.favplaces.util = gweb.favplaces.util || {};

/**
 * Generate HTML code with data and templates.
 * @param {string} tpl Template's ID.
 * @param {!Object} data Data that to be inserted into tpl.
 * @return {string} Generated HTML code.
 */
gweb.favplaces.util.templatize = function(tpl, data) {
  var tplHTML = document.getElementById(tpl).innerHTML;
  tplHTML = tplHTML.replace(/\n/g, '').replace(/\s{2,}/g, '');

  if (data) {
    tplHTML = tplHTML.replace(/{{(.*?)}}/g, function(str, match) {
      return data[match] || '';
    });
  }

  return tplHTML;
};

