/*
 * Copyright (C) by Netcetera AG.
 * All rights reserved.
 *
 * The copyright to the computer program(s) herein is the property of
 * Netcetera AG, Switzerland.  The program(s) may be used and/or copied
 * only with the written permission of Netcetera AG or in accordance
 * with the terms and conditions stipulated in the agreement/contract
 * under which the program(s) have been supplied.
 *
 */

var MINUTE = 60000;
var HOUR = 60 * MINUTE;
var ONE_HOUR = 60 * MINUTE;
var ONE_DAY = 24 * ONE_HOUR;

var LONG_REQUEST = 6 * HOUR; 
var INTERVAL_LENGTH_HOURS = 3;
var INTERVAL_LENGTH_MILLIS = (24 * 60 * 60 * 1000) / (24 / INTERVAL_LENGTH_HOURS);

/**
 * A class responsible for one station. It will hold the station data and
 * maintain a buffer of departures big enough at all times. The buffer is 
 * maintained through periodic calls to the update() method. 
 */
Station = $.klass({

  // the url of the station
  stationUrl: undefined,
  stationName: undefined,
  threshold: undefined,
  longerRequest: undefined,
  
  // contains functions that are called when new data arrives
  observers: undefined,
  
  /**
   * Initializes an instance.
   * @param config the configuration. 
   */
  initialize: function(config) {
    // reinitialize
    this.threshold =  12;
    this.observers = [];
    this.longerRequest = false;
    this._allDepartures = [];
    this._successfulRequests = [];
    this._activeRequest = null;
    this._failedRequest = null;
    
    this.stationUrl = config.stationUrl;
    this.threshold = config.opts.numDepartures * 2 || this.threshold;
  },

  // the departures that the station currently has
  _allDepartures: undefined,
  // the requests that were successful
  _successfulRequests: undefined,
  // the request that is currently sent
  _activeRequest: undefined,
  _failedRequest: undefined,
  
  /**
   * Gets a list of departures, given a Date and a number of departures to be listed.
   * @param fromTime, the time from which the departures will be considered.
   * @param numberOfDepartures, the number of departures to return.
   */
  getDepartures: function(fromTime, numberOfDepartures) {
    var pending = $.grep(this._allDepartures, function(val) {
      return val.time >= fromTime;
    });
    if (numberOfDepartures) {
      return pending.slice(0, numberOfDepartures);
    } 
    return pending;
  },
  
  /**
   * Returns undefined if there is no failed request, otherwise a message to be shown.
   */
  failure: function() {
    if (this._failedRequest && this._failedRequest.status) {
      switch (this._failedRequest.status) {
        case 404: case 491: return "Nicht korrekt konfiguriert";
        case 490: return "Fahrplan ausserhalb des gültigen Bereichs";
        case 501: default: return "Service momentan ausser Betrieb.";
      }
    }
    
    return undefined;
  },
  
  /**
   * Updates the station object. The method will issue JSON requests if 
   * it is necessary 
   */
  update: function() {
    var r = this.nextRequest();
    if (r) {
      this.requestDepartures(r);
    }
  },

  addObserver: function() {
    this.observers = this.observers.concat($.makeArray(arguments));
  },
  
  notifyObservers: function() {
    var me = this;
    $.each(this.observers, function() {
      this(me);
    });
  },
  
  nextRequest: function() {
    return this.nextRequestFromThreshold() ||
           this.nextRequestFromTime();
  },
  
  hasEnoughDepartures: function() {
    var now = DateTime.now();
    var pending = this.getDepartures(now);
    return (pending.length > this.threshold);
      
  },
  
  /**
   * Fires a request to the server if there are less departures in the buffer
   * than specified there should be.
   */
  nextRequestFromThreshold: function() {
    if (!this.hasEnoughDepartures()) {
      return this.nextRequestFromTime(true);
    } 
    return undefined;
  },
  
  /**
   * Fires a request if a half of a request interval has passed.
   * @param force, if it is set to true, there will be a request for sure. 
   */
  nextRequestFromTime: function(force) {
    var t = DateTime.now();
 
    if (this._successfulRequests.length == 0 && force && !this._activeRequest)  {
      return new Request(this.stationUrl, t);
    }
    var req;    
    var lastReq = this._successfulRequests[this._successfulRequests.length - 1];

    if (t > lastReq.to) {
      req = new Request(this.stationUrl, t);
    } else if ((lastReq.to.getTime() - t.getTime()) <= (INTERVAL_LENGTH_MILLIS / 2) || force) {
      req = lastReq.nextRequest();
    }
  
    if (this._activeRequest) {
      return undefined;
    }
    return req;
  },

  /**
   * Requests new departures from the server and handles the data. It also handles bad responses. 
   * @param nextInterval, the time interval to request departures.
   */
  requestDepartures: function(nextInterval) {
    var me = this;
    
    this._activeRequest = nextInterval;
    
    $.ajax({
      type: "GET",
      url: nextInterval.toURL(),
      dataType: "json",
      
      success: function(data) {
        // small sanity check
        if (data && $.isArray(data.departures) && data.name) {
          
          me.addSeccessfulRequest(nextInterval);
          
          me._failedRequest = null;
          me._activeRequest = null;

          me.stationName = data.name;
          me.injectResults(data.departures);

          if (!me.hasEnoughDepartures()) {
            me.update();
          } else {
            me.notifyObservers();
          } 
        } else {
          me._failedRequest = me._activeRequest;
          me._failedRequest.status = 501;
        }
        
        me._activeRequest = null;
      },
      
      error: function (xhr, textStatus, errorThrown) {
        // typically only one of textStatus or errorThrown 
        // will have info
        me._failedRequest = me._activeRequest;
        me._activeRequest = null;

        if (xhr && $.inArray(xhr.status, [404, 490, 491]) >= 0) {
          me._failedRequest.status = xhr.status;
        } else {
          me._failedRequest.status = 501; 
        }
        
        me.notifyObservers();
      }
    });
  },
  
  /**
   * Adds the newly arrived departures to the station buffer, and extends them with new methods. 
   * @param departures, the new departures.
   */
  injectResults: function(departures) {
    var me = this;
    var now = DateTime.now();
    
    this._allDepartures = $.grep(this._allDepartures, function(dep) {
      return !(now.minus(dep.time) > HOUR);
    }).concat(me.markCopies($.map(departures, function(dep) {
        
        dep.time = Date.parseISO(dep.iso8601_time);
        
        $.extend(dep, me._departureMethods);
        $.extend(dep.line, me._lineMethods);
        
        return dep;
      })));
  },
  
  addSeccessfulRequest: function(req) {
    var now = DateTime.now();
    
    this._successfulRequests = $.grep(this._successfulRequests, function(r) {
      return !(now.minus(r.to) > INTERVAL_LENGTH_MILLIS * 2);
    }).concat([req]);
  },

  markCopies: function(departures) {
    previousDep = departures[0];
    for ( var int = 1; int < departures.length; int++) {
      var departure = departures[int];
      if (departure.equals(previousDep)) {
        departure.copyNumber = previousDep.copyNumber + 1;
      }
      previousDep = departure;
    }
    return departures;
  },
  
  /**
   * Methods for drawing the line icon.
   */
  _lineMethods: {
    cssColor: function(cl) {
      return "rgb( " +
        Math.round(cl[0] * 255) +
        "," +
        Math.round(cl[1] * 255) +
        "," +
        Math.round(cl[2] * 255) +
        ")";
    },
    
    color: function() {
      return this.cssColor(this.colors.fg);
    },
    
    backgroundColor: function() {
      return this.cssColor(this.colors.bg);
    }
  },
  
  /**
   * Methods for the time info of the departures
   */
  _departureMethods: {
    
    
    copyNumber: 0,
    
    formattedTime: function(now) {
      var timeDifference = Math.floor(this.time.minus(now) / MINUTE);
      
      if (timeDifference > 30) {
        var next3am = now.clone().setTo3amNextDay();
        
        if (this.time >= next3am) {
          return sprintf("<div class=\"weekday\">%s</div><div>%02d:%02d</div>", this.time.getWeekDayString(),
                  this.time.getHours(), this.time.getMinutes());
        }
        
        return sprintf("%02d:%02d", this.time.getHours(), this.time.getMinutes());
      } else {
        return  'in ' + timeDifference + '\'';
      }
    },
    
    equals: function(other) {
      return this.time.equals(other.time) &&
          this.line.line_name == other.line.line_name &&
          this.end_station.href == other.end_station.href;
    },
    
    sameAs: function(other) {
      return this.time.equals(other.time) &&
          this.line.line_name == other.line.line_name &&
          this.end_station.href == other.end_station.href &&
          this.copyNumber == other.copyNumber;
    }
  }
});



/**
 * class representing a single request. The request will initialize itself
 * with the correct values, depending on the from and to dates given.
 */
Request = $.klass({
  /**
   * Initializer. 
   * @param base, the base url (the station url)
   * @param from the from date. If not present, the class will calculate the 
   *  correct interval around the date.
   * @param to date (optional). If present the exact from and to will be used.
   */
  initialize: function(base, from, to) {
    this.base = base;
    
    if (to) {
      this.from = from;
      this.to = to;
    } else {
      this.from = this.startOfInterval(from);
      this.to = this.endOfInterval(from);
    }
    
    return this;
  },
  
  /**
   * returns a request object that follows this request. 
   * @param longerRequest, if undefined or false returns a regular request, 
   * if true returns a 24 hour request
   */
  nextRequest: function() {
    var from = new Date(this.to.getTime());
    return new Request(
      this.base,
      new Date(from.setToNextMinute()), 
      new Date(this.to.getTime() + INTERVAL_LENGTH_MILLIS))
  },
  
  startOfInterval: function(aDate) {
    var d = aDate.clone().setToHourStart();
    var hour = Math.floor(d.getHours() / INTERVAL_LENGTH_HOURS) * INTERVAL_LENGTH_HOURS;
    d.setHours(hour);
    
    return d;
  },
  
  endOfInterval: function(aDate) {
    var d = aDate.clone().setToHourStart();
    var hour = Math.floor(d.getHours() / INTERVAL_LENGTH_HOURS) * INTERVAL_LENGTH_HOURS + INTERVAL_LENGTH_HOURS - 1;
    d.setHours(hour);
    d.setMinutes(59);
    d.setSeconds(59);
    d.setMilliseconds(999);
    return d;
  },
  
  toURL: function() {
    return encodeURI(this.base + "/" + this.from.toISOString() + "/"  + this.to.toISOString());
  },
  
  equals: function(other) {
    return this.base == other.base && this.from.equals(other.from) && this.to.equals(other.to);
  }
});


