/**
 * Creates the UI that shows a map
 *
 * Places and Events are displayed on the map.
 * For each event, both the place and the event will be displayed,
 * in different styles. Thus, it's not necessary to manually add
 * the event's places.
 *
 * The places and events are clickable and navigate to the obj. TODO impl
 *
 * If you pass showNearPlaces, all known places in the storage that
 * are in the same area as the explicitly passed places and events
 * will be added as well, for orientation. TODO impl
 *
 * The map is zoomable and panable.
 *
 * Creates:
 * Map using Leaflet
 *
 * Depends on: Leaflet
 *    <http://leafletjs.com>
 *
 * @param container {jquery DOM Element}   <div> where to add the UI
 * @param places {Array of Place}   The places to display.
 * @param events {Array of Event}   The events to display.
 *     Each event must have (at least one) place associated with it
 *     in the relations. If it doesn't, it is ignored.
 * @param obj {Detail}   What the places are about
 * @param showNearPlaces {Storage}  (Optional)
 *     If any of the places are in the same area as places
 *     from the explicitly passed |places| and |events|, these
 *     other places will be shown, too.
 *     You may leave this empty.
 * @param successCallback {Function}   Called when the map
 *     has fully loaded
 * @param errorCallback {Function(e)}
 * @param navFunc {Function}   Called when the user
 *     clicks on an event.
 * @param editMode {Boolean}   (not supported, always false)
 *     Allow the user to add features.
 *     You have to start this process by calling result.editAddStart();
 * @param addedCallback {Function(geo {Array of GeoCoordinate})}
 *     Called when the user has finished drawing a new feature.
 *     Only for editMode == true
 * @returns result {
 *   editAddStart()
 *      Let the user draw a new feature now.
 *      Only for editMode == true
 * }
 */
function uiMap(p) {
  if (uiMapInputCheck(p) == -1) { // fixes up input parameters
    return;
  }
  assert( !p.editMode, "Use uiMapOpenLayers for map edit for now");
  var mapObj = this;
  mapObj.params = p;
  //const L = Leaflet;
  p.animatedMan = p.obj instanceof Person;

  // Create info box above map
  // Shows info about map objects on mouseover or click
  // This avoids label collision
  var curObj = null;
  var $info = $("<div class='mapinfo box' />").appendTo(p.container);
  var $events = $("<div class='mapinfo events' />").appendTo($info);
  var $name = $("<div class='mapinfo name' />").appendTo($info);
  var $infoProp = $("<div class='mapinfo properties' />").appendTo($info);
  var $time = $("<span class='mapinfo time' />").appendTo($infoProp);
  var $location = $("<span class='mapinfo location' />").appendTo($infoProp);
  var $descr = $("<div class='mapinfo shortdescr' />").appendTo($info);
  var $open = $("<div class='mapinfo open clickable' />").appendTo($info)
      .text(tr("map.info.open"));
  $open.click(function() {
    if (curObj) {
      p.navFunc(curObj);
    }
  });
  var lastFeature = null;
  function updateInfobox(obj, feature) {
    assert( !obj || obj instanceof Place || obj instanceof Event);
    curObj = obj;
    if (obj) {
      $name.removeAttr("hidden");
      $time.removeAttr("hidden");
      $open.removeAttr("hidden");
      $name.text(obj.name);
      $descr.text(shortenText(obj.descr, 120));
      if (obj instanceof Event) {
        $infoProp.removeAttr("hidden");
        $time.text(obj.time ? obj.time : ""); // TODO format time
        $location.text(obj.places.map(function(pl) { return pl.name; }).join("; "));
      } else {
        $time.text("");
        $location.text("");
        $infoProp.attr("hidden", "true");
      }
      $info.get(0).scrollIntoView(true);
    } else {
      $descr.text(tr("map.info.noneselected"));
      $name.attr("hidden", "true");
      $time.attr("hidden", "true");
      $open.attr("hidden", "true");
    }

    // Show list of events at this place
    $events.empty();
    $events.attr("hidden", "true");
    if (obj instanceof Place && !(p.obj instanceof Event)) {
      var events = p.events.filter(function(ev) { return ev.places.indexOf(obj) != -1; });
      if (events.length > 0) {
        $events.removeAttr("hidden");
        uiList({
          container : $events,
          columns : [
            { label : tr("page.place.events"), displayFunc : function(obj) {
                return obj.name;
              } },
          ],
          data : events,
          clickFunc : function(event) { p.navFunc(event,
              events,
              tr("page.events.hl")); },
        }).removeClass("list");
      }
    }

    // HACK to hide other labels,
    // because Leaflet.Label is broken on touch devices.
    // see leaflet.label.js _addLabelRevealHandlers()
    if (lastFeature) {
      lastFeature.hideLabel();
    }
    lastFeature = feature;
  };
  updateInfobox(null, null);
  // Don't show infobox when there's nothing to discover
  if (p.places.length <= 1) {
    $info.attr("hidden", "true");
  }

  // Create map
  var $map = $("<div id='map' class='map'/>").appendTo(p.container);
  var height = Math.round(gDevice.height * 0.8);
  $map.attr("style", "height: " + height + "px;");
  runLater(100, function() {
    var start = Date.now();

    var map = new L.map("map", {
      tap : false, // breaks page scrolling on mobile
      dragging : gDevice.deviceType == "desktop", // ditto
      scrollWheelZoom : false, // breaks page scrolling on mobile
    });
    map.attributionControl.setPrefix("");
    // alternative tile servers: http://{s}.tile.osm.org/{z}/{x}/{y}.png
    var mapquestAttr = '. Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
    var osmLayer = new L.TileLayer("http://otile{s}.mqcdn.com/tiles/1.0.0/{type}/{z}/{x}/{y}.png", {
        subdomains: ["1", "2", "3", "4"],
        type: "osm", // or sat
        attribution: "&copy; <a href='http://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors" + mapquestAttr,
        //attribution: "&copy; NASA Blue Marble and US Dept. of Agriculture" + mapquestAttr,
    });
    // NASA Blue Marble <http://mike.teczno.com/notes/blue-marble-tiles.html>
    // Need to mirror once we make serious traffic
    var satLayer = new L.TileLayer(
        "http://s3.amazonaws.com/com.modestmaps.bluemarble/{z}-r{y}-c{x}.jpg",
        {
        attribution: "Satellite view NASA and ModestMaps",
        maxZoom : 9,
    });
    var watercolorZoom = new L.TileLayer("http://tile.stamen.com/watercolor/{z}/{x}/{y}.jpg", {
        attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.',
        minZoom : 10,
        maxZoom : 18,
    });
    new L.LayerGroup([ satLayer, watercolorZoom ]).addTo(map);

    new L.Control.Scale().setPosition("bottomright").addTo(map);

    // Layers
    // Order matters, last one added is the one getting the mouse events
    // Routes = the lines between events
    var routesLayer = new L.FeatureGroup().addTo(map);
    var eventsLayer = new L.FeatureGroup().addTo(map);
    var placesLayer = new L.FeatureGroup().addTo(map);
    var layersControl = new L.Control.Layers().setPosition("topleft").addTo(map);
    layersControl.addBaseLayer(osmLayer, tr("map.layer.map"));
    layersControl.addBaseLayer(satLayer, tr("map.layer.sat"));
    layersControl.addOverlay(placesLayer, tr("map.layer.places"));
    layersControl.addOverlay(eventsLayer, tr("map.layer.events"));
    layersControl.addOverlay(routesLayer, tr("map.layer.routes"));

    var totalBounds = new L.LatLngBounds();
    var info = new L.control();

    function highlightFeature(e) {
      try {
        var feature = e.target;
        updateInfobox(feature.obj, feature);
      } catch (e) { p.errorCallback(e); }
    }
    function unhighlightFeature(e) {
      try {
        var feature = e.target;
        updateInfobox(feature.obj, feature);
      } catch (e) { p.errorCallback(e); }
    }

    const lineOptions = {
      weight: 2,
      opacity: 0.7,
      color: "yellow",
    };
    function featureOptions(obj) {
      return {
        fillColor: obj instanceof Place ? "blue" : "yellow",
        weight: 2,
        opacity: 0.7,
        color: "blue",
        fillOpacity: 0.5,
      };
    }
    const invisibleOptions = {
      weight: 0,
      opacity: 0,
      fillOpacity: 0,
      color: "white",
      fillColor: "white",
    };

    function placeToLatLng(place) {
      if (place.point) {
        return [ place.point.lat, place.point.long ];
      } else if (place.area) {
        var bounds = new L.LatLngBounds();
        place.area.forEach(function(point) {
          bounds.extend([ point.lat, point.long ]);
        });
        return bounds.getCenter();
      } else {
        assert(false, place.name + " has neither point nor area");
      }
    }

    function makeFeatureFromDetailObj(obj) {
      var places = obj instanceof Event ? obj.places : [ obj ];
      assert(places[0] instanceof Place, "no location found for " + obj.name);

      places.forEach(function(place) {
        var feature;
        if (place.point) {
          var featureVisible = new L.CircleMarker(
              [ place.point.lat, place.point.long ],
              featureOptions(obj));
          featureVisible.setRadius(5); // px
          featureVisible.addTo(obj instanceof Event ? eventsLayer : placesLayer);
          // Make click target larger, for mobile
          feature = new L.CircleMarker(
              [ place.point.lat, place.point.long ],
              invisibleOptions);
          feature.setRadius(10); // px
        } else if (place.area && place.area.length > 0) {
          feature = new L.Polygon(place.area.map(function(point) {
            return [ point.lat, point.long ];
          }), featureOptions(obj));
        } else {
          return; // e.g. event with second place without point
        }
        assert(feature, "no geo coords for " + obj.name);
        feature.bindLabel(obj.name); // needs plugin Leaflet.label
        feature.obj = obj;
        feature.addTo(obj instanceof Event ? eventsLayer : placesLayer);
        totalBounds.extend(feature.getBounds());

        feature.on({
            mouseover : highlightFeature,
            mouseout : unhighlightFeature,
            click : highlightFeature,
        });
      });
    }

    /**
     * Connect events in order of time,
     * to create a route on the map.
     */
    function makeLineBetweenEvents(events) {
      events = events.filter(function(ev) { return ev.places.length > 0; });
      //events = events.filter(function(ev) { return !!ev.time; });
      events = events.sort(function(a, b) { return a.time - b.time; });
      var places = [];
      events.forEach(function(ev) {
        places = places.concat(ev.places);
      });
      places = places.filter(function(place) {
        return place.point || place.area && place.area.length > 0;
      });
      if (places.length < 2) {
        return;
      }
      var feature = new L.Polyline(places.map(placeToLatLng), lineOptions);
      feature.addTo(routesLayer);

      // Add little man that walks along the route, for fun and animation
      if (p.animatedMan) {
        // NOTE: This makes the browser slow and puts it under stress.
        // Esp. the CSS animations support in Firefox seems to be buggy and
        // cause flickering and blurried text, so remove TRANSITION
        L.DomUtil.TRANSITION = false;
        var icon = new L.Icon({
          iconUrl: "static/walking.png",
          iconSize: [16, 16],
          iconAnchor: [8, 8],
        });
        new L.AnimatedMarker(feature.getLatLngs(), {
          icon : icon,
          distance : 5000,  // 5km in meters
          interval : 1000, // 1s in milliseconds
          /*onEnd : function() { -- throwing "t is undefined"
            this.start(); // continuous
          },*/
        }).addTo(map);
      }
    }

    p.places.forEach(makeFeatureFromDetailObj);
    p.events.forEach(makeFeatureFromDetailObj);

    if (p.obj instanceof Person) {
      makeLineBetweenEvents(p.events);
    }

    // place and zoom map based on features shown
    if (totalBounds.isValid()) {
      map.fitBounds(totalBounds);
      // max zoom, but only for automatic zoom
      if (map.getZoom() > 10) {
        map.setZoom(10);
      }
    }
    console.log("map took " + (Date.now() - start) + "ms");

  }, p.errorCallback);
  return mapObj;
}

/**
 * Creates the UI that shows a map
 * @see uiMap()
 *
 * Creates:
 * Map using OpenLayers
 *
 * Depends on: OpenLayers
 *    <http://www.openlayers.org>
 *
 * @see uiMap()
 * @param editMode {Boolean}   (not supported, always false)
 *     Allow the user to add features.
 *     You have to start this process by calling result.editAddStart();
 * @param addedCallback {Function(geo {Array of GeoCoordinate})}
 *     Called when the user has finished drawing a new feature.
 *     Only for editMode == true
 * @returns result {
 *   editAddStart()
 *      Let the user draw a new feature now.
 *      Only for editMode == true
 * }
 */
function uiMapOpenLayers(p) {
  if (uiMapInputCheck(p) == -1) { // fixes up input parameters
    return;
  }
  var mapObj = this;
  mapObj.params = p;

  // Create map
  var $map = $("<div id='map' class='map'/>").appendTo(p.container);
  runAsync(function() {

  //var mapE = $map.get(0);
  //console.log("map div " + mapE); console.log("map div class " + mapE.getAttribute("class"));
  var width = Math.min(800, gDevice.width);
  var height = Math.round(width / 4 * 3);
  // CSS width/height doesn't work (in time)
  $map.attr("style", "width: " + width + "px; height: " + height + "px;");
  var map = new OpenLayers.Map("map");
  map.addControl(new OpenLayers.Control.LayerSwitcher());
  map.addLayer(new OpenLayers.Layer.WMS("Geography",
      "http://vmap0.tiles.osgeo.org/wms/vmap0", { layers: "basic" }));
  //map.addLayer(new OpenLayers.Layer.OSM("Today's cities"));
  map.baseLayer.events.register("loadend", mapObj, p.successCallback);
  const WGS1984 = new OpenLayers.Projection("EPSG:4326");

  var styleFunctions = {
    label : function(feature) {
      if (p.editMode && !feature.obj) {
        return "new geo";
      }
      return feature.obj.name;
    },
  };
  var myPlaceStyle = new OpenLayers.StyleMap({
    "default": new OpenLayers.Style({
      stokeColor: "blue",
      fillColor: "white",
      fillOpacity: 0.6,
      graphicWidth: 20,
      graphicHeight: 24,
      graphicYOffset: -24,
      pointRadius: 5,
      graphicZIndex: 1,
      fontFamily: "sans-serif",
      fontSize: "10px",
      fontWeight: "bold",
      label : "${label}",
    }, { context: styleFunctions, }),
    "select": new OpenLayers.Style({
      fillColor: "red",
      fillOpacity: 1,
      pointRadius: 10,
      graphicZIndex: 5,
    }),
  });
  var myEventStyle = new OpenLayers.StyleMap({
    "default": new OpenLayers.Style({
      stokeColor: "black",
      fillColor: "yellow",
      fillOpacity: 0.6,
      graphicWidth: 20,
      graphicHeight: 24,
      graphicYOffset: -24,
      pointRadius: 5,
      graphicZIndex: 1,
      fontFamily: "sans-serif",
      fontSize: "10px",
      fontWeight: "bold",
      label : "${label}",
    }, { context: styleFunctions, }),
    "select": new OpenLayers.Style({
      fillColor: "red",
      fillOpacity: 1,
      pointRadius: 10,
      graphicZIndex: 5,
    }),
  });

  var placesLayer = new OpenLayers.Layer.Vector("Places", {
    styleMap: myPlaceStyle,
  });
  map.addLayer(placesLayer);
  var eventsLayer = new OpenLayers.Layer.Vector("Events", {
    styleMap: myEventStyle,
  });
  map.addLayer(eventsLayer);

  selectControl = new OpenLayers.Control.SelectFeature(
    [ placesLayer, eventsLayer ], {
        clickout: true,
        toggle: false,
        multiple: false,
        hover: false,
    });
  map.addControl(selectControl);
  selectControl.activate();
  placesLayer.events.on({
      "featureselected" : function(e) {
        p.navFunc(e.feature.obj);
      },
      //"featureunselected" : function(e)
  });
  eventsLayer.events.on({
      "featureselected" : function(e) {
        p.navFunc(e.feature.obj);
      },
  });

  if (p.editMode) {
    var editAddControl = new OpenLayers.Control.DrawFeature(placesLayer,
        OpenLayers.Handler.Polygon);
    editAddControl.events.register("featureadded", mapObj, function(event) {
      editAddControl.deactivate();
      var earthCoord = new OpenLayers.Geometry.LinearRing(
            event.feature.geometry.clone().getVertices());
      earthCoord.transform(
            map.getProjectionObject(), // from screen
            WGS1984 // to WGS 1984
          );
      var result = [];
      earthCoord.getVertices().forEach(function(pointOL) {
        result.push(new GeoCoordinate(pointOL.x, pointOL.y));
      });
      //console.log(dumpObject(result, "newcoord", 2));
      p.addedCallback(result);
    });
    map.addControl(editAddControl);

    mapObj.editAddStart = function() {
      editAddControl.activate();
    };
  }

  function makeFeatureFromDetailObj(obj) {
    var place = obj;
    if (obj instanceof Event) {
      place = obj.places[0];
      assert(place, "event has no place");
    }
    assert(place instanceof Place);

    var geometry
    if (place.point) {
      geometry = new OpenLayers.Geometry.Point(place.point.long, place.point.lat);
    } else if (place.area && place.area.length > 0) {
      var points = place.area.map(function(point) {
         return new OpenLayers.Geometry.Point(point.long, point.lat);
      });
      geometry = new OpenLayers.Geometry.LinearRing(points);
    } else {
      assert(false, "have no geo coords");
    }
    geometry.transform(
        WGS1984, // from WGS 1984
        map.getProjectionObject() // to screen
      );
    var feature = new OpenLayers.Feature.Vector(geometry, { label : obj.name });
    feature.obj = obj;
    return feature;
  }

  placesLayer.addFeatures(p.places.map(makeFeatureFromDetailObj));
  eventsLayer.addFeatures(p.events.map(makeFeatureFromDetailObj));
  /*placesLayer.features.concat(eventsLayer.features).forEach(function(feature) {
    console.log("have feature for " + feature.obj.name);
  });*/

  var size = new OpenLayers.Bounds();
  if (placesLayer.features.length > 0) {
    size.extend(placesLayer.getDataExtent());
  }
  if (eventsLayer.features.length > 0) {
    size.extend(eventsLayer.getDataExtent());
  }
  assert(placesLayer.features.length > 0 || eventsLayer.features.length > 0, "have no map features at all");
  // leave margin around features
  //console.log("extent left " + size.left + ", right " + size.right + ", top " + size.top + ", bottom " + size.bottom);
  //console.log("width " + size.getWidth());
  // minimum map scale for single points
  if (size.getWidth() < 0.3) {
    // I don't see a "increase size by absolute amount" function, so emulate it
    var box = size.toArray();
    var left = box[0];
    var bottom = box[1];
    var right = box[2];
    var top = box[3];
    const add = 0.2;
    size.extendXY(left - add, bottom - add);
    size.extendXY(right + add, top + add);
    //console.log("extented left " + size.left + ", right " + size.right + ", top " + size.top + ", bottom " + size.bottom);
    //console.log("width " + size.getWidth());
  }
  size = size.scale(1.2);
  map.zoomToExtent(size);

  }, p.errorCallback);
  return mapObj;
}

function uiMapInputCheck(p) {
  assert(typeof(p.navFunc) == "function", "need navFunc");
  assert(typeof(p.errorCallback) == "function", "need errorCallback");
  assert(p.container, "div: need element");
  // clean up parameters
  if ( !p.places || !p.places.length) {
    p.places = [];
  } else {
    assert(p.places[0] instanceof Place, "places: wrong type");
  }
  if ( !p.events || !p.events.length) {
    p.events = [];
  } else {
    assert(p.events[0] instanceof Event, "events: wrong type");
  }
  assert( !p.obj || p.obj instanceof Detail, "obj: wrong type");
  if (p.editMode) {
    // For orientation, and to skip the checks below
    var fake = new Place();
    fake.name = "Dead Sea";
    fake.point = new GeoCoordinate(35.5, 31.5);
    p.places.push(fake);
  }
  if ( !p.places.length && !p.events.length) {
    return -1;
  }
  p.successCallback = p.successCallback || function() {};
  assert(typeof(p.successCallback) == "function", "successCallback: need function");
  assert(typeof(p.navFunc) == "function", "navFunc: need function");

  //console.log(dumpObject(p.places, "mapPlacesIn", 2));
  //console.log(dumpObject(p.events, "mapEventsIn", 2));
  // only places that have coordinates
  p.places = p.places.filter(function(place) { return place.point || place.area && place.area.length > 0; });
  // only events that have places, with coordinates
  p.events = p.events.filter(function(event) { var place = event.places[0]; return place && (place.point || place.area && place.area.length > 0); });
  //console.log(dumpObject(p.places, "mapPlacesFiltered", 3));
  //console.log(dumpObject(p.events, "mapEventsFiltered", 3));
  // if there isn't anything to display, then don't show the map
  if (p.places.length == 0 && p.events.length == 0) {
    //console.log("nothing to show on the map");
    p.successCallback();
    return -1;
  }
  return 1;
}
