/*
Copyright 2009 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var mapState = {};

var currentParamsHash;  // The URI encoded, currently loaded state.
var currentParams = {};
var appPath = document.location.protocol + '//' +
              document.location.host + document.location.pathname;

var DEFAULT_PARAMS = {
  //sphere: 'earth',
  maptype: 'earth',
  terrain: true,
  borders: true,
  roads: true,
  buildings: true
};

var downloadButton = null;
var playTourButton = null;

/**
 * Init function called when the DOM is ready.
 */
function init() {
  $('top').style.display = 'block';

  // Handle full-height in IE6.
  if (navigator.userAgent.indexOf('MSIE 6') >= 0) {
    var resizeFn = function() {
      $('map').style.height =
          (document.body.clientHeight - $('map').offsetTop) + 'px';
    };

    resizeFn();
    google.maps.Event.addDomListener(window, 'resize', resizeFn);
  }

  $('play-tour-button').style.display = 'none';
  $('download-button').style.display = 'none';
  $('embed-link').style.display = 'none';

  downloadButton = makeButton($('download-button'));
  playTourButton = makeButton($('play-tour-button'), function() {
    if (mapState.tour) {
      mapState.map.setMapType(G_SATELLITE_3D_MAP);
      mapState.ge.getTourPlayer().setTour(mapState.tour);
      mapState.ge.getTourPlayer().play();
    }
  });

  checkParamsAndLoadKml();

  // Watch the document location hash for changes.
  window.setInterval(checkParamsAndLoadKml, 100);
}

/**
 * Util for getElementById.
 */
function $(id) {
  return document.getElementById(id);
}

/**
 * Util to add a CSS classe to an object.
 */
function addClass(node, cls) {
  node.className = node.className.replace(new RegExp(cls, 'g'), '') + ' ' + cls;
}

/**
 * Util to remove a CSS class from an object.
 */
function removeClass(node, cls) {
  node.className = node.className.replace(new RegExp(cls, 'g'), '');
}

/**
 * Util for adding interactivity to a CSS button.
 */
function makeButton(node, clickHandler) {
  var button = {
    disabled: false,
    disable: function(b) {
      if (typeof(b) == 'undefined')
        b = true;

      this.disabled = Boolean(b);

      if (b) {
        addClass(node, 'button-base-disabled');
      } else {
        removeClass(node, 'button-base-disabled');
        removeClass(node, 'button-base-hover');
        removeClass(node, 'button-base-active');
      }
    },
    onclick: clickHandler
  };

  google.maps.Event.addDomListener(node, 'mouseover', function() {
    if (!button.disabled)
      addClass(node, 'button-base-hover');
  });

  google.maps.Event.addDomListener(node, 'mouseout', function() {
    removeClass(node, 'button-base-hover');
    removeClass(node, 'button-base-active');
  });

  google.maps.Event.addDomListener(node, 'mousedown', function() {
    if (!button.disabled)
      addClass(node, 'button-base-active');
  });

  google.maps.Event.addDomListener(node, 'mouseup', function() {
    removeClass(node, 'button-base-active');
  });

  google.maps.Event.addDomListener(node, 'click', function() {
    if (!button.disabled && button.onclick)
      button.onclick();
  });

  return button;
}

/**
 * Checks for changes in the state params (the location hash)
 * and reloads the viewport if necessary.
 */
function checkParamsAndLoadKml() {
  // Don't use document.location.hash because it automatically
  // resolves URI-escaped entities.
  var docParamsHash = makeParamStr(parseParams(
      (document.location.href.match(/#.*/) || [''])[0]));

  if (docParamsHash != currentParamsHash) {
    var newParamsHash = docParamsHash;
    var newParams = parseParams(newParamsHash);

    loadKmlWithParams(newParams);

    currentParamsHash = newParamsHash;
    currentParams = newParams;
  }
}

/**
 * Merges a 'defaults' parameter dictionary into a supplied
 * parameters dictionary.
 */
function mergeDefaultParams(params, defaults) {
  for (var k in defaults) {
    if (!(k in params)) {
      params[k] = defaults[k];
    }
  }
}

/**
 * Parses a parameter string, i.e. foo=1&bar=2 into
 * a dictionary object.
 */
function parseParams(paramStr) {
  var params = {};
  paramStr = paramStr.replace(/^[?#]/, '');

  var pairs = paramStr.split('&');
  for (var i = 0; i < pairs.length; i++) {
    var p = pairs[i].split('=');
    var key = p[0] ? decodeURIComponent(p[0]) : p[0];
    var val = p[1] ? decodeURIComponent(p[1]) : p[1];
    if (val === '0')
      val = 0;
    if (val === '1')
      val = 1;
    if (key in params) {
      // Handle array values.
      if (params[key] && 'push' in params[key]) {
        params[key].push(val);
      } else {
        params[key] = [params[key], val];
      }
    } else {
      params[key] = val;
    }
  }

  return params;
}

/**
 * Creates a URL-friendly param string from a params object
 * literal.
 */
function makeParamStr(params) {
  var paramStrArr = [];

  for (var key in params) {
    var val = params[key];
    if (typeof val === 'undefined' || val === null)
      continue;

    if (typeof val === 'object' &&
        'split' in val &&
        'splice' in val &&
        val.length) {
      for (var i = 0; i < val.length; i++) {
        var subVal = val[i];
        if (subVal === false) subVal = 0;
        if (subVal === true) subVal = 1;
        paramStrArr.push(encodeURIComponent(key) + '=' +
                         encodeURIComponent(subVal.toString()));
      }
    } else {
      if (val === false) val = 0;
      if (val === true) val = 1;
      paramStrArr.push(encodeURIComponent(key) + '=' +
                       encodeURIComponent(val.toString()));
    }
  }

  return paramStrArr.join('&');
}

/**
 * Crude HTML-escaping utility function.
 */
function escapeHtml(html) {
  return html.replace(/</g, '&lt;')
             .replace(/</g, '&gt;')
             .replace(/&/g, '&amp;');
}

/**
 * Loads the KML view defined by the given params dictionary.
 */
function loadKmlWithParams(params) {
  mergeDefaultParams(params, DEFAULT_PARAMS);

  loadMap(params, function() {
    // Load the KML file in Maps using a GGeoXml object.
    if (mapState.geoXml) {
      if (params.url === mapState.url) {
        return;
      }

      mapState.map.removeOverlay(mapState.geoXml);
    }

    $('play-tour-button').style.display = 'none';
    $('download-button').style.display = 'none';
    $('embed-link').style.display = 'none';

    mapState.tour = null;
    mapState.geoXml = null;
    mapState.url = params.url;

    if (mapState.url) {
      showKmlMetaUI({ loading: true });

      // Create the GeoXml object.
      mapState.geoXml = new google.maps.GeoXml(params.url, function() {
        if (!mapState.geoXml.loadedCorrectly()) {
          showKmlMetaUI({ error: 'Error loading KML.' });
          return;
        }

        if (params.maptype == 'earth') {
          mapState.geoXml.getKml = function(callback) {
            callback(['<NetworkLink><flyToView>1</flyToView>',
                    '<Link><href>',
                    params.url.replace(/&/g, '&amp;'),
                    '</href></Link></NetworkLink>'].join(''));
          };
          mapState.map.addOverlay(mapState.geoXml);
        } else {
          mapState.map.addOverlay(mapState.geoXml);
          mapState.geoXml.gotoDefaultViewport(mapState.map);
        }

        // Load the title using the plugin (if available).
        if (mapState.ge) {
          google.earth.fetchKml(mapState.ge, params.url, function(obj) {
            if (!obj) {
              showKmlMetaUI({ error: 'Error loading KML.' });
              return;
            }

            // Look for a tour.
            walkKmlDom(obj, function() {
              if ('getType' in this &&
                  this.getType() == 'KmlTour') {
                mapState.tour = this;
                return false;
              }
            });

            if (obj.getName()) {
              showKmlMetaUI({ url: params.url, title: obj.getName() });
            } else {
              showKmlMetaUI({ url: params.url });
            }

            $('play-tour-button').style.display = mapState.tour ?
                'inline' : 'none';
          });
        } else {
          showKmlMetaUI({ url: params.url });
        }
      });

      $('download-button').style.display = 'inline';
      $('embed-link').style.display = 'inline';
    } else {
      showKmlMetaUI({ empty: true });

      $('download-button').style.display = 'none';
      $('embed-link').style.display = 'none';
    }
  });
}

/**
 * Returns the string trimmed to maxLength - 3 characters with an ellipsis
 * at the end, if necessary.
 */
function ellipsis(maxLength, str) {
  if (str.length > maxLength)
    str = str.substr(0, maxLength - 3) + '...';
  return str;
}

/**
 * Returns the basename (file name) of a URL.
 */
function basename(path) {
  return path.replace(/.*\/([^\/]+)/, '$1');
}

/**
 * Sets the UI (download button, title display, etc.) to the given title and
 * URL (or loading/error). If no title is available, uses the basename of
 * the URL.
 */
function showKmlMetaUI(options) {
  options = options || {};

  if (options.loading) {
    $('kml-title').innerHTML = 'Loading...';
    document.title = 'Google Earth Preview';
    downloadButton.onclick = null;
    $('embed-link').onclick = function(){ return false; };
  } else if (options.empty) {
    $('kml-title').innerHTML = 'No KML file specified!';
    document.title = 'Google Earth Preview';
    downloadButton.onclick = null;
    $('embed-link').onclick = function(){ return false; };
  } else if (options.error) {
    $('kml-title').innerHTML = options.error;
    document.title = 'Google Earth Preview';
    downloadButton.onclick = null;
    $('embed-link').onclick = function(){ return false; };
  } else if (options.url) {
    options.title = options.title || ellipsis(80, basename(options.url));
    $('kml-title').innerHTML = '';
    $('kml-title').appendChild(document.createTextNode(options.title));
    document.title = options.title + ' - Google Earth Preview';
    downloadButton.onclick = function() {
      //var w = window.open(url, 'KmlDownload');
      location.href = options.url;
    };

    // Create embed link.
    var embedURL = 'http://www.gmodules.com/ig/creator?' + makeParamStr({
      synd: 'open',
      url: 'http://code.google.com/apis/kml/embed/embedkmlgadget.xml',
      title: options.title,
      up_kml_url: currentParams.url,
      up_view_mode: (currentParams.maptype == 'earth') ? 'earth' : 'maps',
      up_earth_show_buildings: currentParams.buildings,
      up_earth_show_terrain: currentParams.terrain,
      up_earth_show_roads: currentParams.roads,
      up_earth_show_borders: currentParams.borders,
      up_maps_default_type: currentParams.maptype
    });

    $('embed-link').onclick = null;
    $('embed-link').href = embedURL;
  }
}

/**
 * Load a Google Map with the given app-specific params,
 * and call the given callback when done. If a Maps instance already exists,
 * it is reused. Also loads an instance of the Google Earth Plugin if possible.
 */
function loadMap(params, callback) {
  callback = callback || function(){};

  if (!mapState.map/* ||
      params.sphere != currentParams.sphere*/) {

    var mapTypes = G_DEFAULT_MAP_TYPES;
    switch (params.sphere) {
      case 'sky':
        mapTypes = G_SKY_MAP_TYPES;
        break;

      case 'mars':
        mapTypes = G_MARS_MAP_TYPES;
        break;
    }

    mapTypes.push(G_SATELLITE_3D_MAP);
    mapState.map = new google.maps.Map2($('map'), {'mapTypes': mapTypes});
    mapState.map.setCenter(new google.maps.LatLng(0, 0), 2);

    var mapUI = mapState.map.getDefaultUI();
    mapUI.maptypes.satellite = false;
    mapUI.maptypes.hybrid = false;
    mapState.map.setUI(mapUI);

    // Build shadow screen overlay.
    var zz = new google.maps.ScreenPoint(0, 1, 'fraction', 'fraction');
    var shadow = new google.maps.ScreenOverlay(
        'images/shadow.png', zz, zz,
        new google.maps.ScreenSize(1, 5, 'fraction', 'pixel'));
    mapState.map.addOverlay(shadow);

    google.maps.Event.addListener(mapState.map, 'maptypechanged', function() {
      if (mapState.programmaticMapTypeChange)
        return;

      switch (mapState.map.getCurrentMapType()) {
        case G_NORMAL_MAP:
          updateCurrentParams({ maptype: 'normal' });
          break;

        case G_SATELLITE_MAP:
          updateCurrentParams({
            maptype: 'satellite',
            roads: false,
            borders: false
          });
          break;

        case G_HYBRID_MAP:
          updateCurrentParams({
            maptype: 'satellite',
            roads: true,
            borders: true
          });
          break;

        case G_PHYSICAL_MAP:
          updateCurrentParams({ maptype: 'terrain' });
          break;

        case G_SATELLITE_3D_MAP:
          updateCurrentParams({ maptype: 'earth' });
          updateEarthWithParams(params);
          break;
      }
    });
  }

  // Load the GE Plugin if possible.
  mapState.map.getEarthInstance(function(pluginInstance) {
    mapState.ge = pluginInstance; // Could be null.

    mapState.programmaticMapTypeChange = true;
    switch (params.maptype) {
      case 'normal':
        mapState.map.setMapType(G_NORMAL_MAP);
        break;

      case 'satellite':
        mapState.map.setMapType(
            (params.borders || params.roads) ? G_HYBRID_MAP : G_SATELLITE_MAP);
        break;

      case 'terrain':
        mapState.map.setMapType(G_PHYSICAL_MAP);
        break;

      case 'earth':
        mapState.map.setMapType(G_SATELLITE_3D_MAP);
        updateEarthWithParams(params);
        break;
    }
    mapState.programmaticMapTypeChange = false;

    if (mapState.ge) {
      updateEarthWithParams(params);
    }

    callback();
  });
}

/**
 * Tries to update the GE Plugin state with the given params. If the plugin is
 * not instantiated, does nothing.
 */
function updateEarthWithParams(params) {
  if (!mapState.ge) {
    return;
  }

  mapState.ge.getNavigationControl().setVisibility(
      mapState.ge.VISIBILITY_SHOW);
  mapState.ge.getNavigationControl().getScreenXY().setXUnits(
      mapState.ge.UNITS_PIXELS);

  // Show or hide Earth layers based on given params.
  mapState.ge.getOptions().setMapType(
      (params.sphere === 'sky') ?
      mapState.ge.MAP_TYPE_SKY : mapState.ge.MAP_TYPE_EARTH);
  mapState.ge.getLayerRoot().enableLayerById(
      mapState.ge.LAYER_TERRAIN,
      Boolean(params.terrain));
  mapState.ge.getLayerRoot().enableLayerById(
      mapState.ge.LAYER_BUILDINGS,
      Boolean(params.buildings));
  mapState.ge.getLayerRoot().enableLayerById(
      mapState.ge.LAYER_BUILDINGS_LOW_RESOLUTION,
      Boolean(params.buildings));
  mapState.ge.getLayerRoot().enableLayerById(
      mapState.ge.LAYER_ROADS,
      Boolean(params.roads));
  mapState.ge.getLayerRoot().enableLayerById(
      mapState.ge.LAYER_BORDERS,
      Boolean(params.borders));
}

function updateCurrentParams(params) {
  if (params) {
    mergeDefaultParams(params, currentParams);
    currentParams = params;
  }

  document.location.hash = makeParamStr(currentParams);
}
