//Copyright (c) 2008, Bit Above Software
(function() {
  // Stub out console if it doesn't exist
  var console = window.console || {
    log: function() {},
    error: function() {},
    dir: function() {}
  };

  // Lazily call a function with a specific target and argument array
  function callAgain(o, args) {
    var f = args.callee;
    var args = $A(args);
    setTimeout(function() {f.apply(o, args);}, 0);
  }

  // Remove all instances of an object from an array
  var arrayRemove = function(arr, v) {
    for (var i = arr.length-1; i >= 0; i--) {
      if (arr[i] === v) {
        arr[i] = arr[arr.length-1];
        arr.length--;
      }
    }
  }

  // Set/unset a classname on a DOM element
  var setClassName = function(el, classname, flag) {
    var cns = el.className.split(' ');
    if (!flag) arrayRemove(cns, classname);
    if (flag) cns.push(classname);
    el.className = cns.join(' ');
  }

  // Extensions to Number
  Object.extend(Number.prototype, {
    meters2readable: function() {
      var miles = this/1609;
      if (miles < .1) {
        return Math.round(miles*5280/10)*10 + ' ft';
      } else if (miles < 10) {
        return Math.round(miles*10)/10 + ' mi';
      }
      return Math.round(miles) + ' mi';
    }
  });

  // Extensions to Object
  Object.extend(Object, {
    split: function(s, a, b) {
      var o = {};
      s = s.split(a);
      s.each(function(kv) {kv = kv.split(b); o[kv[0]] = kv[1]});
      return o;
    }
  });

  // Extensions to Function
  Object.extend(Function.prototype, {
    setLazy: function(delay) {
      var me = this;
      if (!this._lazyF) this._lazyF = function() {me.clearLazy(); me();}
      this._timer = setTimeout(this, delay || 0);
    },
    clearLazy: function() {
      if (this._timer) clearTimeout(this._timer);
      delete this._timer;
    },
    isLazy: function() {
      return !!this._timer;
    }
  });

  Object.extend(Date.prototype, {
    elapsed: function(msg) {
      var now = new Date();
      console.log('@t=' + Math.round((now-this)/10)/100 + ' (+' + Math.round((now-(this._lap || now))/10)/100+ '): ' + msg);
      this._lap = now;
    }
  });

  var GMAP_ERRORS = {
    400: 'The request could not be processed.',
    601: 'Missing or empty location query value',
    602: 'There is a problem with one or both of the locations.  Try checking them in <a href="http://maps.google.com" target="_blank">Google Maps</a>.  If Google Maps finds the locations, it may be that you just need to be more specific (e.g. "Concord" won\'t work, but "Concord, MA" will.)',
    603: 'The directions can not be provided for legal reasons.  (Sorry, I don\'t know what it means either!)',
    604: 'Could not compute directions between the provided locations',
    610: 'Invalid Google Maps API key'
  }

  window.NPRMap = {
    load: function() {
      window.timer = new Date();

      if (!GBrowserIsCompatible()) {
        Modal('error_panel').setMessage('Sorry, your browser is not compatible with Google Maps').show();
        return;
      }

      // Get direction row template
      NPRMap._rowTemplate = $('dir_template');
      NPRMap._rowTemplate.style.display = '';

      // Create the map
      var map = this.map = new GMap2(document.getElementById("map"));
      map.setCenter(new GLatLng(41, -96));
      map.setZoom(4);

      map.addControl(new GSmallMapControl());
      map.addControl(new GMapTypeControl());

      // Create the directions object
      NPRMap.directions = new GDirections(NPRMap.map);

      if (!NPRMap.params.from) return alert('No "from" location specified');
      if (!NPRMap.params.to) return alert('No "to" location specified');

      NPRMap.queryRoute();
    },

    queryRoute: function(from, to) {
      var dirs = NPRMap.directions;

      // Clear previous results
      Modal('working_panel').setMessage('Asking Google for directions').show();
      NPRMap.map.clearOverlays();
      ThingOverlay.setAutoArrange(false);
      ThingOverlay.onArranged = function() {
        timer.elapsed('Arranged things');
      };
      dirs.clear();

      var query = 'from:' + NPRMap.params.from + ' to:' + NPRMap.params.to;
      dirs.load(query, {getSteps:true});
      
      timer.elapsed('Issued request');

      if (!NPRMap.renderRoute.isLazy()) {
        NPRMap.renderRoute.setLazy(500);
        NPRMap.queryTimeout.setLazy(NPRMap.params.timeout || 15*1e3);
      }
    },

    queryTimeout: function() {
      NPRMap.renderRoute.clearLazy();
      Modal('error_panel').setMessage('Hmm... this is taking too long', 'Try refreshing the page to see if the problem goes away.  If this happens again, please see <a href="http://gsfn.us/t/o90">this bug report</a> for more info.').show();
    },

    renderRoute: function() {
      var dirs = NPRMap.directions;
      var status = dirs.getStatus();
      var err = status && GMAP_ERRORS[status.code];

      // If there's no error but no valid status then wait a little longer
      if (!status || (!err && status.code != 200)) {
        NPRMap.renderRoute.setLazy(500);
        return;
      }

      timer.elapsed('Received response');

      // We got a result, so cancel the taking-too-long error
      NPRMap.queryTimeout.clearLazy();
      if (err) {
        Modal('error_panel').setMessage('Could not find directions', err).show();
        return;
      }

      Modal('working_panel').setMessage('Gathering waypoints').show();

      if (dirs.getNumRoutes() == 0) {
        Modal('error_panel').setMessage('Could not find directions').show();
      }

      // Render the summary
      $('dir_summary').update(NPRMap.params.from + ' to ' + NPRMap.params.to +
        ': ' + NPRMap.directions.getSummaryHtml());

      // Go to next step
      NPRMap.gatherWaypoints.setLazy();
    },

    gatherWaypoints: function() {
      var dirs = NPRMap.directions;
      var route = dirs.getRoute(0);
      var mTotal = dirs.getDistance().meters;
      if (!route) return alert('Sorry, no route found');

      console.log('Route has ' + route.getNumSteps() + ' steps');

      // Build the list of waypoints along the route by walking the polyline
      // and picking vertices every X miles or so (this assumes the line has
      // vertices at least every X miles, but that seems to be an okay
      // assumption for google data.
      var pl = dirs.getPolyline(), plc = pl.getVertexCount();
      console.log('Line has ' + plc + ' vertices');
      var waypoints = [];
      var mCalc = 0, sofar = 0, vert, minMeters = 10*1609;
      for (var i = 1; i < plc; i++) {
        vert = pl.getVertex(i);
        // Cache the lat/lng values
        vert.zlat = vert.lat();
        vert.zlng = vert.lng();
        sofar += vert.distanceFrom(pl.getVertex(i-1));
        if (sofar > minMeters || i == plc-1) {
          mCalc += sofar;
          waypoints.push({latlng:vert, meters:mCalc});
          sofar = 0;
        }
      }
      console.log('Using ' + waypoints.length + ' waypoints');

      // Normalize distance to the total specified by google
      waypoints.each(function(pt) {
        pt.meters *= mTotal/mCalc;
      });

      Modal('working_panel').setMessage('Locating stations along route').show();
      NPRMap.tracer = new GMarker(waypoints[0].latlng);
      NPRMap.map.addOverlay(NPRMap.tracer);

      NPRMap.stationList = {};
      NPRMap.gatherStations(waypoints, waypoints.length);
    },

    gatherStations: function(pts, count) {
      var pt = pts.shift();
      NPRMap.tracer.setLatLng(pt.latlng);

      // Find all stations in range of the waypoints
      var stationList = NPRMap.stationList;
      var sInfo = [];
      Station.instances.each(function(station) {
        sInfo.push({station:station, mojo: 0, maxMojo:0, maxWaypoint:0});
      });

      // Sort by mojo
      sInfo.each(function(si) {si.mojo = si.station.getMojo(pt.latlng);});
      sInfo.sort(function(a,b) {return a.mojo > b.mojo ? -1 : (a.mojo < b.mojo ? 1:0);});

      // Add any stations w/in rane
      for (var i = 0; i < sInfo.length; i++) {
        var si = sInfo[i];
        if (si.mojo <= 0) break;

        // Remember which waypoint has the best reception for this station
        if (si.mojo > si.maxMojo) {
          si.maxMojo = si.mojo;
          si.maxWaypoint = pt;
        }

        // Add to the station list
        if (!stationList[si.station.id]) {
          stationList[si.station.id] = si;
          NPRMap.map.addOverlay(new ThingOverlay(si.station.latlng, si.station));
        }
      }

      if (pts.length) {
        callAgain(this, arguments);
      } else {
        // No more points - move on to next step
        $('shield').hide();
        Modal.hideAll();

        stationList = Object.values(stationList);
        NPRMap.map.removeOverlay(NPRMap.tracer);
        ThingOverlay.setAutoArrange(true);

        timer.elapsed('Gathered ' + stationList.length + ' stations');
        NPRMap.renderDirections(stationList);
      }
    },

    renderDirections: function(stationList) {
      // Clear the map
      var tel = $('dir_table').down('tbody');
      while (tel.childNodes.length) tel.removeChild(tel.firstChild);
      var totalDist = NPRMap.directions.getDistance().meters;

      // Gather station info
      var rows = [], lastrow = null;
      stationList.each(function(si) {
        var newrow = {
          meters: si.maxWaypoint.meters,
          rmeters: totalDist - si.maxWaypoint.meters,
          html: 'Closest point to ' + si.station.sign +
            ' ' + si.station.freq + ' in ' + si.station.place,
          classname: 'station_row'
        };
        if (lastrow && newrow.meters == lastrow.meters) {
          lastrow.html += '<br />' + newrow.html;
          newrow = lastrow;
        } else {
          rows.push(newrow)
          lastrow = newrow
        }
      });

      // Gather directions
      var route = NPRMap.directions.getRoute(0);
      var rdist = 0;
      for (var i = 0; i < route.getNumSteps(); i++) {
        var step = route.getStep(i);
        rows.push({
          meters: rdist,
          html: step.getDescriptionHtml(),
          classname: 'step_row',
          stepMeters: step.getDistance().meters
        });
        rdist  += step.getDistance().meters;
      }

      // Sort by mileage
      var sorter = function(a,b) {return a.meters < b.meters ? -1 : (a.meters > b.meters ? 1:0);};
      rows.sort(sorter);

      // Render the rows
      var rel = NPRMap._rowTemplate;
      var row;

      while (row = rows.shift()) {
        var nel = $(rel.cloneNode(true));
        setClassName(nel, row.classname, true);
        nel.down('.dir_dist').update(row.meters.meters2readable());
        nel.down('.dir_rdist').update(row.rmeters ? row.rmeters.meters2readable() : '');
        nel.down('.dir_sdist').update(row.stepMeters ? row.stepMeters.meters2readable() : '');
        nel.down('.dir_info').update(row.html);
        tel.appendChild(nel);
        $(nel).show();
      }

      timer.elapsed('Rendered directions');
    }
  }

  // Parse to/from fields out of location
  var loc = location.toString();
  var params = NPRMap.params = Object.split(loc.replace(/^[^#\?]*[#\?]+/, ''), '&', '=');
  for (var k in NPRMap.params) {
    params[k] = unescape(params[k] || '').replace(/\+/g, ' ');
  }


  //
  // Station
  //

  var nextId = 0;


  var Station = function(data) {
    Object.extend(this, data);
    this.id = nextId++;
    this.place = this.city + ', ' + this.state;
    this.latlng = new GLatLng(this.lat, this.lon);
    this.power = Station.power(this.kw);
    this.fm = /FM/.test(this.freq);
    this.zIndex = this.power;

    // Approximate range for the station
    var w;
    switch (this.power) {
      case 0: w = this.fm ? .5 : 1; break;
      case 1: w = .2; break;
      case 2: w = .3; break;
      case 3: w = .5; break;
      case 4: w = .7; break;
      case 5: w = 1; break;
    }
    this.range = 100*1609*w;

    // Crude approximation for getting min/max lat and long of the station range
    var RAD_EARTH = 6371009; // Earth radius in meters
    var d2r = Math.PI/180;
    var rlat = RAD_EARTH*Math.cos(this.lat*Math.PI/180);
    this.minlat = this.lat - this.range/RAD_EARTH*360;
    this.maxlat = this.lat + this.range/RAD_EARTH*360;
    this.minlng = this.lon - this.range/rlat*360;
    this.maxlng = this.lon + this.range/rlat*360;

    Station.instances.push(this);
  }

  Object.extend(Station, {
    INFO_URL_TEMPLATE: 'http://www.radio-locator.com/cgi-bin/finder?sr=Y&s=C&call=CALL&x=0&y=0',

    instances: [],

    power: function(kw) {
      return kw >= 100 ? 5 : (kw >= 10 ? 4 : (kw >= 1 ? 3 : (kw >= .1 ? 2 : (kw >= .01 ? 1 : 0))));
    },

    load: function() {
      while (STATIONS.length) new Station(STATIONS.shift());
    },

    powerImage: function(kw) {
      return '<img src="bars' + Station.power(kw) + '_s.png" class="power_image">';
    }
  });

  Object.extend(Station.prototype, {
    /*
     * Get the "mojo" of the station at the specified distance.  "Mojo" is an
     * intentionally ambiguous term.  This value is simply for comparing the
     * expected reception of stations to eachother.  It does not have any
     * absolute real-world meaning in terms of power, quality of reception or
     * whatever.
     */
    getMojo: function(latlng) {
      var min = this.minlat, v = latlng.zlat, max = this.maxlat;
      if (v < min || v > max) return 0;
      var min = this.minlng, v = latlng.zlng, max = this.maxlng;
      if (v < min || v > max) return 0;

      var dist = Math.max(1, this.latlng.distanceFrom(latlng));
      mojo = (this.range - dist)/this.range;
      return mojo
    },

    toString: function() {
      return this.sign + ' ' + this.freq;
    },

    toHtml: function(i) {
      return '<b>' + this.sign + '</b><br />' + this.freq;
    },

    render: function(el) {
      el.innerHTML = this.toHtml().replace(/AM |FM /, '');
      el.className = 'station ' + (this.fm ? 'fm' : 'am');
      el.style.fontSize = (this.power/2) + 9 + 'px';
      //el.style.borderWidth = (this.power/2) + 1 + 'px';
      el.style.zIndex = this.power;
    }
  });

  //
  // City
  //

  var City = function(station) {
    this.stations = [];
    this.strongest = null;
  }

  Object.extend(City, {
    citiesWithStations: function(stations, cities) {
      var cities = cities || {};
      stations.each(function(station) {
        var city = cities[station.place];
        if (!city) city = cities[station.place] = new City();
        city.addStation(station);
      });

      return cities;
    }
  });

  Object.extend(City.prototype, {
    addStation: function(station) {
      if (!this.stations.length) {
        this.latlng = station.latlng;
        this.place = station.place;
      }
      this.stations.push(station);

      if (!this.strongest || this.strongest.power < station.power) {
        this.strongest = station;
      }
    },

    toString: function() {
      return this.place;
    },

    /** Get content to display in our station list */
    toHtml: function() {
      var s = [];
      for (var i = 0; i < this.stations.length; i++) s.push(this.stations[i].toHtml());
      return '<b>' + this.place + '</b>' + s.join(', ');
    },

    /** Get content to display in google map info panel */
    toInfoPanelString: function() {
      var s = [];
      for (var i = 0; i < this.stations.length; i++) s.push(this.stations[i].toHtml());
      return '<b>' + this.place + '</b><br>' + s.join('<br>');
    }
  });

  Object.extend(window, {
    onload: function() {
      Station.load();
      NPRMap.load();
    },
    onunload: function() {
      GUnload();
    }
  });

  // Find panel widget
  window.Modal= function(el) {
    el = $(el);
    if (!el.isModal) {
      el._show = el.show;
      Object.extend(el, Modal._methods);
    }
    return el;
  };
  Object.extend(Modal, {
    _methods: {
      isModal: true,
      initialize: function(el) {
        this.el = $(el);
        modalPanels.push(this);
      },
      _hide: function() {
        this.hide();
      },
      show: function(shield) {
        Modal.hideAll();
        this._show();
        return this;
      },
      setMessage: function(msg, details) {
        var mel = this.down('.message'), del = this.down('.details');
        if (mel) mel.update(msg);
        if (del) {
          if (details) {
            del.update(details);
            del.show()
          } else {
            del.hide();
          }
        }
        return this;
      }
    },
    hideAll: function() {
      var modals = $(document.body).getElementsBySelector('.modal');
      modals.invoke('hide');
    }
  });
})();
