/*
 * 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.
 *
 */

/**
 * This is the class responsible for keeping information about the current station
 * and the departures that are currently shown on the screen. I has methods for 
 * resizing the elements, adding new departures, removing the old ones, and animating them.
 */

var Display = $.klass({
  
  station: undefined,
  numDepartures: 6,  
  messageIndex: 0,
  index: 0,
  blinkVisible: true,
  currentMessagesString: undefined,
  
  initialize: function(config, station) {
    this.station = station;
    
    this.station.addObserver($.bind(this.update, this));
    
    this.numDepartures = config.opts.numDepartures || this.numDepartures;
    
    this.messages = [];
    this.filteredMessages = [];
    this.addedIds = [];
  },
    
  /**
   * Deletes the current row.
   */
  scrap: function() {
    this.departureRows().remove();
  },
  
  /**
   * Shows or hides the platform column
   */
  setupPlatformColumn: function() {
    $("#schedule .platform").toggleClass("disabled", !this.station.hasPlatforms);
  },
  
  /**
   * Main function that updates the screen.
   */
  update: function() {
    Main.log.info("Display.update(): Start update.");
    this.setupPlatformColumn();
    
    var now = DateTime.now().setToMinuteStart();
    Main.log.info("Display.update(): now = " + now);
    var deps = this.station.getDepartures(now, this.numDepartures);
   
    Main.log.info("Display.update(): " + deps.length + " acquired departures.");
    
    document.title = this.station.stationName;
    $("#name").text(this.station.stationName);
    
    // update the departures that are unchanged
    // this is needed in case some of the departures have an update and the real time changes
    // in this case the existing real time shuold be replaced with the new real time
    this.updateExistingTimes(deps, now);
    
    var removed = this.numberOfDeparturesToRemove(now);
    Main.log.info("Display.update(): Removing " + removed + " departures.");

    // add additional departures
    this.addDepartures(this.onlyNew(deps).slice(0, (removed == 0 ? this.numDepartures : removed)), now); 
    
    var me = this;
    if (deps.length > 0) {
      
      // remove old departures
      Main.log.info("Display.update(): update departures");
      Main.log.info("           remove old departure");
      this.removeOldDepartures(now, function() {
        
        Main.log.info("         update departure times");
        me.updateTimes(now, function() {
          
          Main.log.info("         sort departures");
          me.sortDepartures();
        });
      });
    };
      
    // create a string with the station, trip and line messages from the departures 
    var messages = this.collectMessages(deps);
    
    // scroll the messages string if there are messages
    // if there are departures but no messages , hide the message panel
    if (deps.length > 0) { 
      if (messages.length > 0) {
        this.displayMessage(messages);
      } else {
        PerturbationMessage.hide();
        this.currentMessagesString = "";
      }
    }
    
    // dispplay error message if available
    var failure = this.station.failure();
    if (deps.length == 0 && failure) {
      ErrorMessage.show(failure);
    } else {
      ErrorMessage.hide();
    }
  },
  
  /**
   * Function that displays the messages.
   * Reset the perturbation div and start the marquee again
   * 
   * @param messages the messages to be added to the marquee
   */
  displayMessage: function(messages) {
    if (this.currentMessagesString != messages) {
      // hide and show the message div
      PerturbationMessage.hide();
      
      // reset the perturation div and start again
      $("#perturbation").html(
          "<p id=\"message\" messages=\"" + messages + "\">&nbsp;</p>"  
      );
      
      this.currentMessagesString = messages;
      setTimeout("PerturbationMessage.show()", 2000);
      $("#message").marquee('message');
    }
  },
  
  /**
   * Filters only the new departures.
   * @param deps, the departures got from the Station class.
   */
  onlyNew: function(deps) {
    var drows = this.departureRows();
    
    return $.grep(deps, function(dep) {
      var result = true;
      drows.each(function() {
        if ($(this).departure() && $(this).departure().sameAs(dep)) {
          result = false;
        }
      });
      return result;
    });
  },
  
  /**
   * Returns all the departures that are currently shown. 
   */  
  departureRows: function() {
    return $("#schedule tbody:not(.labels) tr:not(.proto)");
  },
  
  /**
   * Returns the prototype row, according to which, all the other rows are created.
   */
  proto: function() {
    return $("#schedule tr.proto");
  },
  
  numberOfDeparturesToRemove: function(now) {
    return this.departureRows().expired(now).size();
  },

  /**
   * Removes the old departures from view with some effect. 
   * @param now(Date), date object set to current time 
   */
  removeOldDepartures: function(now, callbackFunction) {
    var daparturesForRemoval = this.departureRows().expired(now);
    this.departureRows().expired(now).animatedRemove(callbackFunction);
  },
  
  /**
   * Remove all departures from the view with animation.
   */
  removeAllDepartures: function() {
    this.departureRows().animatedRemove();
  },
    
  addDepartures: function(deps, now) {
    var me = this;
    $.each(deps, function() {
      Main.log.info("Add new deaprture row");
      me.proto().clone().removeClass("proto")
        .insertBefore("#schedule tr.proto")
        .makeDepartureRow(this, now);
    });
    this.updateSizes();
  },
  
  /**
   * Gets the messages from the departures and puts them in one array.
   * 
   * @param deps array of departures
   * @return string representing all messages concatenated.
   */ 
  collectMessages: function(deps) {
    var me = this;
    this.filteredMessages = [];
    this.messages = [];
    this.addedIds = [];
    this.messagesString = "";
    
    $.each(deps, function() {
      // filter messages, if there is a "@@departureTime@@" string 
      // replace it with the actual departure time
      var currentDepartureMessages = this.messages;
      for (i = 0; i < currentDepartureMessages.length; i++) {
        if (currentDepartureMessages[i].text.indexOf("@@departureTime@@") != -1) {
          var time = Date.parseISO(this.iso8601_time);
          var timeString = sprintf("%02d:%02d", this.time.getHours(), this.time.getMinutes());
          
          currentDepartureMessages[i].text = 
            currentDepartureMessages[i].text.replace("@@departureTime@@", timeString);
        }
        me.messages = me.messages.concat(currentDepartureMessages[i]);
      }
      
    });
    
    // filter messages, remove messages with same id
    // and messages with passed validity time, or not yet valid
    for (i = 0; i < this.messages.length; i++) {
      // IDs of messages already added
      if ($.inArray(this.messages[i].id, this.addedIds) == -1) {
        // check if time interval is valid
        var now = DateTime.now();
        
        if ((now.minus(Date.parseISO(this.messages[i].startTime)) >= 0) 
            && (now.minus(Date.parseISO(this.messages[i].endTime)) <= 0)) {
          
          this.addedIds = this.addedIds.concat(this.messages[i].id);
          this.filteredMessages = this.filteredMessages.concat(this.messages[i]);
          
          // move newly added message so the array is sorted by id
          var j = this.filteredMessages.length-1;
          while ((j > 0) && (this.filteredMessages[j].id < this.filteredMessages[j-1].id)) {
            var tMessage = this.filteredMessages[j-1];
            this.filteredMessages[j-1] = this.filteredMessages[j];
            this.filteredMessages[j] = tMessage;
            j--;
          }
        }
      }
    }

    // compose messages string
    for (i = 0; i<this.filteredMessages.length; i++) {
      if (i > 0) {
        this.messagesString += ";";
      }
      this.messagesString += this.filteredMessages[i].text;
    }  
    
    return this.messagesString;
  },

  /**
   * Updates the times on the departures displayed in the kiosk.
   */
  updateTimes: function(now, callbackFunction) {
    this.departureRows().notExpired(now).updateTimes(now,callbackFunction);
  },
  
  /**
   * Sort the departures and the departure rows.
   */
  sortDepartures: function(now) {
    $(".departures").sortTable({
      onCol: 4
    });
  },
  
  /**
   * Updates the real times of the existing departures displayed in the kiosk.
   * This is in case some of the lines have updates.
   */
  updateExistingTimes: function(departures, now) {
    Main.log.info("Display.updateExistingTimes:");
    this.departureRows().updateRealTimes(departures, now);
  },
  
  getScheduleTableWidth: function() {
    return $("#main").width();
  },
  
  getBorderHeight: function() {
    return Math.ceil(this.cellHeight() / 80);
  },
  
  getHeaderFontSize: function() {
    return $("#main").height() / 6.5;
  },
  
  getLabelFontSize: function() {
    return (this.cellHeight() / 2) * 3/4;
  },
  
  getDepartureFontSize: function() {
    return this.cellHeight() / 2;
  },
  
  getRightPaddingForTimeInfo: function() {
    return ($(document).width() * 2) / 100;
  },
  
  getFooterFontSize: function() {
    return this.cellHeight() / 2;
  },
  
  getTimeDateBoxWidth: function() {
    return $(document).width() - ($("#image-90-opacity").width() * 1.3);
  },
  
  getTimeDateBoxHeigth: function() {
    return $("#footer").height();
  },
  
  getCopyrightBoxFontHeight: function() {
    return ($("#footer").height() - this.getFooterFontSize()) / 2;
  },
  
  getErrorFontSize: function() {
    return $("#error").height() / 1.4;
  },
  
  cellHeight: function() {
    if (jQuery.browser.mozilla == true) {
      return Math.round($("#main").height() / (this.numDepartures + 0.5));
    } 
    
    return Math.ceil($("#main").height() / (this.numDepartures + 0.5) * 39/40); //<-- the "39/40" coefficient is to compensate for the border for the other browsers
  },
  
  updateSizes: function() {
    
    this.updateSizes2();
    if ((jQuery.browser.msie == true) && (jQuery.browser.version.slice(0,1) == "6")) {
      var me = this;
      setTimeout(function() {
        me.updateSizes2();
      }, 0);
    }
  },
  
  /**
   * Changes the visibility of the departure times.
   * This method is called every half second and the visibility
   * of the departure times that have "0'" is inverted,
   * in order to get a blinking effect.
   */
  blinkDepartureTime: function() {
    if (this.blinkVisible) {
      // change to invisible
      this.blinkVisible = false;
      $(".blink").css("visibility", "hidden");
    } else {
      // change to visible
      this.blinkVisible = true;
      $(".blink").css("visibility", "visible");
    }
  },
  
  /**
   * This function is fired after a resize event. 
   * Calculates sizes according to the cell height.
   */
  updateSizes2: function() {

    $("#main").height($('body').height() * 0.76 - 1);
    $("#header").css("font-size", this.getHeaderFontSize());
    $("#footer").height($('body').height() * 0.1 + 1);
    $(".departures .line-icon").height(this.cellHeight());
    $(".departures .end-station").height(this.cellHeight());
    $(".departures .time-info").height(this.cellHeight());
    $(".departures .time-info").css("padding-right", this.getRightPaddingForTimeInfo());
    $("#schedule .labels td").height($("#main").height() - (this.numDepartures * this.cellHeight()));
    $("#schedule .line-icon").width(this.cellHeight());
    $("#schedule .labels tr td").css("font-size", this.getLabelFontSize());
    $("#schedule .departures tr td").css("font-size", this.getDepartureFontSize());
    $("#schedule tr td").css("border-bottom-width", this.getBorderHeight());
    $("#copyright-box").css("top", this.getCopyrightBoxFontHeight()); 
    $("#copyright-box").css("font-size", this.getFooterFontSize());
    $("#datetime").css("font-size", this.getFooterFontSize());
    $("#error").height($('body').height() * 0.1);
    $("#error").css("font-size", this.getErrorFontSize());
    $("#perturbation div").css("height", "100%");
    $("#perturbation").css("font-size", this.getErrorFontSize());
    $("#perturbation").css("bottom", $("#footer").height());
   
    this.checkSizes();

  },
  
  /**
   * If some of the sizes are not correct, i.e. the there is overlapping and some of the elements
   * are not the proper size, this method reduces the font size until everything is ok.
   */
  checkSizes: function() {
    for ( var int = 1; int <= 5; int++) {
      if ($('#name').height() > $('#header').height()) {
        $("#header").css("font-size", this.getHeaderFontSize() * Math.pow((4/5),int));
      }
    }

    // set margin-top to 80% of the empty space between the header and the name
    if ($('#name').height() > 0) {
      $("#name").css("margin-top", ($("#header").height() - $('#name').height())*0.8);
    }
    var cellHeight = this.cellHeight();
    
    this.departureRows().each(function() {
      for ( var int = 1; int <= 5; int++) {
        if (($(this).children(".end-station").innerHeight() > cellHeight  * 1.1) ||
            ($(this).children(".time-info").innerHeight() > cellHeight * 1.1)) {
          $(this).children(".end-station").css("font-size", cellHeight * Math.pow((3/4),int));
        }
      }
    });
    
    for ( var int = 1; int <= 5; int++) {
      if ($('#datetime').width() + this.getRightPaddingForTimeInfo()> this.getTimeDateBoxWidth()) {
        $("#datetime").css("font-size", this.getFooterFontSize() * Math.pow((4/5),int));
      }
    }
    
    $('#datetime').css("right", this.getRightPaddingForTimeInfo());
    $('#legende').css("right", this.getRightPaddingForTimeInfo());
  }
});

/**
 * jQuery departure row extensions.
 */
$.fn.extend({
  
  /**
   * Filters all elements that have an associated departure and that departure is expired.
   * 
   * @param now(aDate), returns all the departures that are older than the given date
   */
  expired: function(aDate) {
    return this.filter(function() {
      return this.departure && this.departure.realTime < aDate;
    });
  },
  
  /**
   * Filters all elements that have an associated departure and that departure is not expired.
   * 
   * @param now(aDate), returns all the departures that are not older than the given date
   */
  notExpired: function(aDate) {
    return this.filter(function() {
      return this.departure && this.departure.realTime >= aDate;
    });
  },
  
  departure: function() {
    return this.get(0).departure;
  },
  
  /**
   * Fills the cloned row with information. 
   * 
   * @param dep, one departure
   * @param now(Date), date object set to current time 
   */
  makeDepartureRow: function(dep, now) {
    Main.log.info("     end station = " + dep.end_station.name);
    Main.log.info("     line = " + dep.line.line_name);
    
    return this
      .each(function() {this.departure = dep;})
      .addClass("departure-row")
      .children(".line-icon")
        .css("color", dep.line.color())
        .css("background-color", dep.line.backgroundColor())
        .text(dep.line.line_name)
      .end()
      .children(".end-station")
        .text(dep.end_station.name)
      .end()
      .children(".time-info")
        //.css("color", dep.getTextColor())
        .removeClass("normal late").addClass(dep.getTextColorClass())
        .removeClass("rt static").addClass(dep.real_time ? "rt" : "static")
        //.css("text-decoration", dep.getTextDecoration())3
      .end()
      .children(".platform")
        .text(dep.platform)
      .end();
  },
  
  /**
   * This is called every minute to change the time info on the rows with an animation. 
   * The animation applies only for those rows that have a change in their time info. 
   * 
   * @param now(Date), date object set to current time 
   */
  updateTimes: function(now, callbackFunction) {
    var updated = this.children(".time-info").length;
    var sortDepartures = (updated > 0) ? true : false;
    
    this.children(".time-info").children("div").each(function() {
      var dep = $(this).parent().parent().departure();
      Main.log.info("Display.updateTimes(): update time for departure at time " + dep.realTime);
      
      var update = false;
      
      if ($(this).children(".weekday").text() && (dep.formattedTime(now).search(/weekday/) < 0)) { 
        update = false; 
      } else {
        update = true; 
      }
        
      if (update) {
        $(this).fadeOut(400, function() {
          
          Main.log.info("       current time " + now);
          Main.log.info("       new time " + dep.formattedTime(now));
          $(this).html(dep.formattedTime(now)).fadeIn(400, function() {
            updated--;
            Main.log.info("Updated == " + updated);
            if ((updated == 0) && (sortDepartures == true)) {
              if(typeof callbackFunction == 'function'){
                callbackFunction.call();
              }
            }
          });
        });
        
      }
      
      // if the departure time is 0', add a "blink" class to the departure
      if (dep.formattedTime(now) == "0'") { 
        $(this).addClass("blink"); 
      } else {
        $(this).removeClass("blink");
      }
    });
    
    return this;
  },
  
  /**
   * This is called before the displayed times in the kiosk are updated.
   * In case some of the departures to have an updated real time, it will be
   * replaced with the new time, and later when the displayed times are updated,
   * the new real time will be taken into consideration.
   * 
   * @param departures, the newly acquired departures from the server 
   */
  updateRealTimes: function(departures, now) {
    this.children(".time-info").children("div").each(function() {
      var dep = $(this).parent().parent().departure();
      
      for ( var i = 0; i < departures.length; i++) {
        if ((departures[i].end_station.id == dep.end_station.id)
            && (departures[i].line.lineNumber == dep.line.lineNumber)
            && (departures[i].iso8601_time == dep.iso8601_time)) {
        
//          if (departures[i].iso8601_real_time != dep.iso8601_real_time) {
            Main.log.info("Display.updateRealTimes: update real time for departure " + dep);
            dep.updateRealTime(departures[i].iso8601_real_time, departures[i].unixTime);
            $(this.parentNode).removeClass("rt static").addClass(departures[i].real_time ? "rt" : "static");
            // update the color of the time
            $(this.parentNode).removeClass("normal late").addClass(dep.getTextColorClass());
//          }
          break;
        }
        
      }
       
    });
    return this;
  },
  
  /**
   * Animates the fade-out of the leaving row and its slide-up
   */
  animatedRemove: function(callbackFunction) {
    var el = $(this);
    var rowsToRemove = this.children("td").length; 
    if (rowsToRemove == 0) {
      callbackFunction.call();
    }
    this.children("td").each(function() {  
      var color = $('body').css('background-color');
      var style = $(this).attr("style") + 
        " opacity: 0; color: " + color + "; border-bottom-color: " + color + ";"; // IE hack to keep the object transparent after the fade out
      $(this).fadeOut(500, function() {
//        if(jQuery.browser.msie) {
//          $(this).get(0).style.removeAttribute('filter');
//        }
        $(this)
          .removeAttr("style")
          .removeAttr("class")
          //create a <div> inside the <td>
          .wrapInner("<div/>") 
          // override the border color from the css, set it to the background color
          .css("border-bottom-color", color) 
          .children("div")
            //the style is set to the <div>, to have smooth slide
            .attr("style", style)
            .slideUp(800, function() {el.remove();});
        
        rowsToRemove--;
        if (rowsToRemove == 0) {
          callbackFunction.call();
        }
      });
    });
  }
  
});
