/*
 * 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,
  hasPlatforms: 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;
    this.hasPlatforms = false;
  },

  // 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.realTime >= 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() {
    this._allDepartures = [];
    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);
    });
  },
  
  /**
   * Get the next request.
   * This can be called if there aren't enough departures obtained,
   * or if the minute ticker resets the departures.
   */
  nextRequest: function() {
    return this.nextRequestFromThreshold() ||
           this.nextRequestFromTime(true);
  },
  
  /**
   * Checks if the number of obtained departures is greater than the threshold.
   */
  hasEnoughDepartures: function() {
    var now = DateTime.now();
    var pending = this.getDepartures(now);
    return (pending.length > this.threshold);
      
  },
  
  /**
   * Checks if the time frame of the request is 24 hours or more from the current time.
   * If so than no more requests should me made.
   * 
   * @param request the current request
   * @return true if the request is after 24 hours from now, false if it's not
   */
  dayPassed: function(request) {
    var now = DateTime.now();
    
    if (request.from - now > ONE_DAY) {
      return true;
    } else {
      return false;
    }
  },
  
  /**
   * 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 ((lastReq == undefined) || (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()) + "?callback=?",
      dataType: "json",
      
      success: function(data, textStatus, XMLHttpRequest) {
        // small sanity check
        if (data && $.isArray(data.departures) && data.name) {
          
          me.addSeccessfulRequest(nextInterval);
          
          me._failedRequest = null;
          me._activeRequest = null;
          
          me.stationName = data.name;
          me.hasPlatforms = data.has_platforms;
          me.injectResults(data.departures);

          if ((!me.hasEnoughDepartures()) 
              && (!me.dayPassed(me._successfulRequests[me._successfulRequests.length - 1]))) {
            //me.update();
            var r = me.nextRequest();
            if (r) {
              me.requestDepartures(r);
            }
          } else {
            me.notifyObservers();
            me._successfulRequests = [];
          } 
        } 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.realTime) > HOUR);
    }).concat(me.markCopies($.map(departures, function(dep) {
        
        dep.time = Date.parseISO(dep.iso8601_time);
        dep.realTime = Date.parseISO(dep.iso8601_real_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]);
  },

  /**
   * Marks if there are same departures in the _allDepartures array.
   */
  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( " +
        cl[0] +
        "," +
        cl[1] +
        "," +
        cl[2] +
        ")";
    },
    
    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,
    
    /**
     * Format the time of departure
     */
    formattedTime: function(now) {
      var timeDifference = Math.floor(this.realTime.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.realTime.getHours(), this.realTime.getMinutes());
        }
      
        return sprintf("%02d:%02d", this.realTime.getHours(), this.realTime.getMinutes());
      } else {
        return 'in ' + timeDifference + '\'';
      }
    },
    
    /**
     * Format the time difference between the scheduled time of departure
     * and the real departure time
     */
    formattedTimeDifference: function() {
      var timeDifference = Math.floor(this.realTime.minus(this.time) / MINUTE);
      
      if (timeDifference > 59) {     
        return sprintf("%02dh%02d\'", (timeDifference / 60), (timeDifference % 60));
      } else {
        return timeDifference + '\'';
      }
    },
    
    /**
     * Updates the real time of the departure.
     * This is called when an updates arrives and the new real time of the departure
     * is different from the current real time.
     */
    updateRealTime: function(newRealTime, newUnixTime) {
      this.unixTime = newUnixTime;
      this.iso8601_real_time = newRealTime;
      this.realTime = Date.parseISO(this.iso8601_real_time);
      
      return this;
    },
    
    /**
     * Gets the color of the departure text.
     * If the real departure time is different than the planned time (the departure runs late)
     * the color id #990000 else the color is #000000.
     */
    getTextColor: function() {
      var timeDifference = Math.floor(this.realTime.minus(this.time) / MINUTE);
      
      if (timeDifference != 0) {
        return "#990000";
      } else {
        return "#000000";
      }

    },
    
    getTextColorClass: function() {
        var timeDifference = Math.floor(this.realTime.minus(this.time) / MINUTE);
        
        if (timeDifference != 0) {
          return "late";
        } else {
          return "normal";
        }
    },
    
    /**
     * Gets the text decoration, blinking or not.
     * If the departure time is 0 minutes then the departure time should blink.
     */
    getTextDecoration: function() {
      var now = DateTime.now();
      
      if (this.formattedTime(now) == "-1'") {
        return "blink";
      } else {
        return "none";
      }
    },
    
    getBlinkClass: function() {
      var now = DateTime.now();
      
      if (this.formattedTime(now) == "-1'") {
        return "blink";
      } else {
        return "";
      }
    },
    
    /**
     * Checks if two departures are the same.
     */
    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, 
   * @return 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;
    var hour = d.getHours() + 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);
  }
});



