/*
 * $Id: chart_main.js,v 1.6 2011/10/04 12:04:20 obo Exp $
 */
dojo.require("dojo.number");
dojo.require("dojo.date.locale");

dojo.declare("swx.inv.Chart", null, {

/*************************************************************************************************
 * Constructor :
 *  id : security id of chart subject
 *  key : label used to identify chart (usually ValorSymbol)
 *  domain : x-axis time domain (e.g. 30d)
 *  presentationConfig: Legacy object containing elements that affect chart presentation.
 *  runtimeConfig: Contains elements that define chart data:
 *    type : either prototype.statics.SIMPLE,ADVANCED
 *    chartLayout : function that returns chart dimensions (usually defined in siteConfig.js)
 *    skipLoading :
 *    skipMovie :
 *    expirationDate : for Eurex
 *    contractType : for Eurex
 *    strikePrice : for Eurex
 *    labelPrecision : y-axis price precision
 *    tradingStartTime : opening time of market in this security
 *    tradingEndTime: closing time of market in this security
 *************************************************************************************************/
  constructor: function(id, key, domain, presentationConfig, runtimeConfig) {

    // Save the id
    this.id = id;

    // Save the key used to find HTML elements
    this.key = key;

    // Save the domain (number of trading days to show)
    if (!domain)
      domain = "1d"; // intraday by default
    this.domain = domain;

    // Copy the runtime config
    this.runtimeConfig = runtimeConfig;

    // Decide if we are intraday, or not.
    this._isIntraday = ("1d" == domain);

    // Copy type into chart object from runtime object
    if (!runtimeConfig.type)
      runtimeConfig.type = this.statics.SIMPLE;
    this.type = runtimeConfig.type;
    if (runtimeConfig.type != this.statics.SIMPLE)
      selectedTimeRange = domain;

    // Copy chartLayout into chart object from runtime object
    if (!runtimeConfig.chartLayout)
      this._chartLayout = siteChartConfig.getSMALL_LAYOUT(); // SMALL layout by default
    else
      this._chartLayout = runtimeConfig.chartLayout;     // object containing layout of the chart

    // override default labelPrecision property from chartConfig
    // (precision varies between various Eurex products and product groups).
    if(runtimeConfig.labelPrecision){
      if(runtimeConfig.labelPrecision == "AUTO"){
        // trigger default behaviour (auto) of axis renderer by resetting
        // chartLayout.labelPrecision property (null in constructor means 'no-change')
        this._chartLayout.labelPrecision = null;
      } else
        this._chartLayout.labelPrecision = runtimeConfig.labelPrecision ;
    }

    this._completeLayout(); // complete the layout

    this.useIntradayValues = true; // changed afterwards (for simple chart)

    this.values_days = {};   // containing day ticks (for advanced graphs)
    this.values_inter = {};
    this.values_inter_zoom = {};
    this.values_intra = {};  // containing intraday ticks (max 10 days, for advanced graphs)
    this.values_zoomed = {};  // containing intraday ticks like values_intra but each day
                              // is an object member (only advanced graphs)

    this.values_domain     = {};  // values of the full domain being drawn
    this.values_displayed  = {};  // values of the drawn points (for advanced graphs)
    this.graph_values      = {};  // Graph values being displayed


    this.showedLines = {}; // save which line is shown (for advanced graphs)
    this.showedLines.id = this.id; // chart line is always displayed

    this.showedEvents = {}; // For advanced charts (news, dividends, ...)
    this.events = {}; // Save all the downloaded events (capital events/news, for advanced graphs)

    // Save the start and stop percents for advanced graphs
    this.displayedMinPercent = 0;
    this.displayedMaxPercent = 1;

    // The data updater (uses ajax to load data at the start)
    this._chartDataUpdater = new swx.inv.ChartDataUpdater(this);
    // The drawer
    this.chartDrawer =
      new swx.inv.ChartDrawer(
          runtimeConfig.type == this.statics.SIMPLE, key, this._chartLayout, this._isIntraday);

    // The ValuesHelper
    if (runtimeConfig.type == this.statics.SIMPLE) {
      this.valuesHelper = new swx.inv.ChartValuesHelper(this, true);
    } else {
      this.valuesHelper = new swx.inv.ChartValuesAdvancedHelper(this);
    }

    // boolean to know if the functions are active or not. (For advanced charts)
    this._functionsActive = false;

    // resize the holding DIV according to chart configuration
    var chartNode = dojo.query('#chart'+this.key);
    if(!isNaN(this._chartLayout.chartWidth))
      chartNode.style('width', this._chartLayout.chartWidth + "px");
    if(!isNaN(this._chartLayout.chartHeight) )
      chartNode.style('height', this._chartLayout.chartHeight + "px");

    // adjust the position of loading/'no data' message
    //
    // Save layout ptr first
    var currentLayout = this._chartLayout;
    var adjusterFun = function(node){
      // do not move the element around if its is hidden (dojo.coords returns empty w/h values)
      if(node.style.display == 'none')
        return;
      var loadingMsgCoords = dojo.coords(node);
      var leftOffset = Math.round(Math.max(currentLayout.graphWidth  - loadingMsgCoords.w,0)/2+currentLayout.marginLeft);
      var topOffset  = Math.round(Math.max(currentLayout.graphHeight - loadingMsgCoords.h,0)/2+currentLayout.marginTop);

      if(!isNaN(topOffset))
        dojo.style(node,'top', addPX(topOffset));
      if(!isNaN(leftOffset) )
        dojo.style(node,'left', addPX(leftOffset));
    };
    dojo.query('#gfxLoading'+this.key).forEach(adjusterFun);

    // 'no data' node is not rendered by defaultbut we still need to get its size
    // the solution is to render it but do not display (hide) so we can get its dimensions
    // without showing it to user
    dojo.query('#gfxEmpty'+this.key).forEach(function(node){
      dojo.style(node,'visibility', 'hidden');
      dojo.style(node,'display', 'block');
      adjusterFun(node);
      dojo.style(node,'display', 'none');
      dojo.style(node,'visibility', '');
    });

    // Set up the timer if needed (used to get the new data)
    // we only use a static one to save number of timers on a page!
    if (this.statics._timer == null)
      this.statics._startTimer();

    // Custom Configuration (for advanced charts)
    if (this.type == this.statics.ADVANCED && presentationConfig)
      chartChangeGraphFromCustomConfig(this, presentationConfig, true);

    // get the initial values and run a function with the ajax response
    //  TODO: probably we need to change following line
    this.dailyHLOCValues = {};
    if (!runtimeConfig.skipLoading) {
      this.getAjaxValues("id", this.id);
      this.getDailyHLOC();
    }
    this.statics.allCharts.push(this);

    // Object to hold a copy of the current date string
    this.dateHolder = "";
  },

/*************************************************************************************************
 * Static members ( You have to use this.statics.XXXX )                                          *
 *************************************************************************************************/
  statics: {
    // No of msec in one day
    ONE_DAY_MSEC : 3600 * 24 * 1000,

    // different types (SIMPLE: only show static data, ADVANCED: have some interactivity)
    SIMPLE   : 1,
    ADVANCED : 2,

    // different grains for data
    INTRADAY : "intraday",
    INTERMEDIATE : "intermediate",
    HISTORICAL : "historic",

    _timer: null, // a time to get the new data from (unique for all graphs to save browser's memory

    _startTimer: function() { // this is only called once at the first instance created of this class
      this._timer = setInterval(function() {
        for (var cNb=0; cNb<swx.inv.Chart.prototype.statics.allCharts.length; cNb++) {
          if (swx.inv.Chart.prototype.statics.allCharts[cNb].quotePlayer) {
            swx.inv.Chart.prototype.statics.allCharts[cNb].quotePlayer.timerTick();
          }
        }
      }, 1000);
    },
    allCharts : []
  },

/*************************************************************************************************
 * Private init function. Used to init the chart                                                 *
 *************************************************************************************************/
  _completeLayout: function() {
    this._chartLayout.surfaceWidth  = this._chartLayout.marginLeft + this._chartLayout.paddingLeft
                                    + this._chartLayout.graphWidth  + this._chartLayout.paddingRight
                                    + this._chartLayout.marginRight;
    this._chartLayout.surfaceHeight = this._chartLayout.marginTop + this._chartLayout.paddingTop
                                    + this._chartLayout.graphHeight + this._chartLayout.paddingBottom
                                    + this._chartLayout.marginBottom;

    // Avoid accumulating fudge-factors on object members by setting a latch the first time we
    // do this (stops wandering charts after repeated reloads).
    if (!this._chartLayout.isFudged) {
      if (dojo.isIE)
        this._chartLayout.marginLeft -= 2;
      else
        this._chartLayout.marginTop += 0.5;
      this._chartLayout.isFudged = true;
    }
  },
/*************************************************************************************************
 * Get ajax values (also used on the Market overview page                                        *
 *************************************************************************************************/

  getAjaxValues: function(lineType, id, newDate) {
    if (this.type == this.statics.ADVANCED) {
      var domain = this.domain;
      this._chartDataUpdater.calculateAndDownloadDataIfNeeded(
        domain, function () {changePeriod(domain);}
      );
    } else {
      this._chartDataUpdater.calculateAndDownloadDataIfNeeded(
        this.domain, null, newDate
      );
    }
  },

  /*
   * Get daily high-low-open-close values for intermediate charts. Needed for IPO-933.
   */
  getDailyHLOC: function() {
    var _url = siteChartConfig.CHART_DATA_URL + "?"
      + balancerIdDefinition + "&id=" + this.id + "&type=" + this.type
      + "&" + this._chartDataUpdater._getQueryStringOfItemToDownload("id", "6m", "");
    var chartObject = this;

    dojo.xhrGet( {
        url          : _url,
        handleAs     : "json",
        timeout      : 20000,
        preventCache : true,

        load: function(response, ioArgs) {
          chartObject.loadDailyHLOC(response.id.historical.vals);
        },

        error: function(response, ioArgs) {
          console.log("AJAX Error, HTTP status code: " + ioArgs.xhr.status +"\n"+response);
          return response;
        }
    });
  },

  /*
   * Re-format the values array into an object for easier lookup
   */
  loadDailyHLOC: function(values) {
    for (var i = 0; i < values.length; i++) {
      this.dailyHLOCValues[values[i].d] = {
        "open" : values[i].o,
        "high" : values[i].M,
        "low" : values[i].m,
        "close" : values[i].c
      };
    }
  },
/*************************************************************************************************
 * Public draw function. Used to draw the chart                                                  *
 *************************************************************************************************/
  draw: function(onlyUpdateIfPossible) {
    if (this.type == this.statics.SIMPLE) {
      // draw simple graph
      if (this.values_domain.id && this.values_domain.id.vals) {
        // If a fixed-width domain is required, pad the beginning of the values dataset to fill
        // the full width of the graph.
        if (this.runtimeConfig.isFixedWidth && !this._isIntraday) {

          // We pad back to the start of the domain
          var domainStartDate = getDomainStartDate(this.domain);
          var mSec = this.values_domain.id.vals[0].date.getTime();
          var paddingDate = new Date(mSec);
          while (domainStartDate < paddingDate) {
            mSec -= this.statics.ONE_DAY_MSEC;
            paddingDate = new Date(mSec);

            // Put only weekdays onto the result set
            if (paddingDate.getDay() > 0 && paddingDate.getDay() < 6) {
              var paddingVal = this.createPaddingVal(paddingDate);
              this.values_domain.id.vals.unshift(paddingVal);
            }
          }
        }
        var local_graph_values =
          this.valuesHelper.getSimpleGraphValuesAndUpdateRange(this.values_domain.id,
              onlyUpdateIfPossible,this._chartLayout.nonDiagonalShape);

        this.chartDrawer.drawSimple(local_graph_values, this.values_domain, local_graph_values.onlyDrawLine);
      }else{
        // no values
        dojo.style("gfxEmpty"+this.key, "display", "");
      }

    } else { // ADVANCED

      // set the values_displayed depending on the displayedMin and displayedMax percents
      this.values_displayed = this.valuesHelper.cutValuesByPercent(
        this.values_domain, this.showedLines, this.displayedMinPercent, this.displayedMaxPercent
      );

      if (this.values_displayed.id.valuesType != swx.inv.Chart.prototype.statics.INTRADAY
         && this.values_displayed.id.vals.length <= 10) {

        // We get here because we are showing historical data of 10 days or less.
        // We would like to show intraday values
        var newValues = zoomIntoIntradayFromHistorical(onlyUpdateIfPossible);
        if (newValues == null) {
          return;  // we have to download some, we will come back here
        }
        // Replace the old ones by the new ones
        this.values_displayed = newValues;

      } else if (this.values_displayed.id.vals.length >10
                && this.values_displayed.id.vals.length<=25) {

        var newValues = zoomIntoIntermediate(onlyUpdateIfPossible);
        if (newValues == null) {
          return;  // we have to download some, we will come back here
        }
        // Replace the old ones by the new ones
        this.values_displayed = newValues;
      }

      if (this.values_displayed.id.valuesType == swx.inv.Chart.prototype.statics.INTRADAY
          || this.values_displayed.id.valuesType == swx.inv.Chart.prototype.statics.INTERMEDIATE) {
        this.chartDrawer.sliderHelper.setDailyTrackLine();
      } else {
        this.chartDrawer.sliderHelper.setHistoricalTrackLine();
      }

      // mix the values_displayed together if we have more than one lin to show
      // this method modifies the values_displayed passed as parameter
      this.valuesHelper.mixValues(this.values_displayed, this.values_domain.id.oldClose);

      this.graph_values.id = this.valuesHelper.getAdvancedGraphValues(this.values_displayed.id, false, !onlyUpdateIfPossible, this.values_domain.id.oldClose);
      if (this.showedLines.idx1) {
        this.graph_values.idx1 = this.valuesHelper.getAdvancedGraphValues(this.values_displayed.idx1, true, false);
      } else {
        this.graph_values.idx1 = null;
      }
      if (this.showedLines.idx2) {
        this.graph_values.idx2 = this.valuesHelper.getAdvancedGraphValues(this.values_displayed.idx2, true, false);
      } else {
        this.graph_values.idx2 = null;
      }
      for (var i=1; i<=10; i++) {
        if (this.showedLines["ti"+i] && this.values_displayed["ti"+i]) {
          if (this.values_displayed["ti"+i].isAddon) {
            this.graph_values["ti"+i] = this.valuesHelper.getAdvancedAddonGraphValues(this.showedLines["ti"+i], this.values_displayed["ti"+i]);
          } else {
            this.graph_values["ti"+i] = this.valuesHelper.getAdvancedGraphValues(this.values_displayed["ti"+i], true, false);
          }
        } else {
          this.graph_values["ti"+i] = null;
        }
      }

      this.chartDrawer.drawAdvanced(this.graph_values, this.values_displayed);
      if (!onlyUpdateIfPossible) {
        // For the small graph in the slider, we show the whole domain (not only displayMin and displayMax percents)
        var full_domain_graph_values = {};
        full_domain_graph_values.id = this.valuesHelper.getAdvancedGraphValues(this.values_domain.id, false, true, this.values_domain.id.oldClose);

        this.chartDrawer.drawSmallChart(full_domain_graph_values);
      }
      this.chartDrawer.drawVolume(this.graph_values.id.points, this.values_displayed.id);

      this.chartDrawer.drawEvents(this.values_displayed.id, this.events, this.showedEvents);

    }

  },

  drawPreview: function() {
    this.values_displayed = this.valuesHelper.cutValuesByPercent(
      this.values_domain, this.showedLines, this.displayedMinPercent, this.displayedMaxPercent, true
    );

    var preview_graph_values = this.valuesHelper.getPreviewGraphValues(this.values_displayed.id);

    this.chartDrawer.drawPreview(preview_graph_values);
  },

  // Only for simple charts
  changeDomain: function(domain) {
    this.domain = domain;
    this.chartDrawer.changeDomain(domain);
  },

  changeId: function(id, newDate) {
    this.id = id;
    this.quotePlayer.clearUpdateEvents(); // stop updates for this chart.
    this.quotePlayer = null; // stop the player for this id. A new one will be made
    this.chartDrawer.clean();
    this._chartDataUpdater.downloadedData["id"] = null;
    this.values_intra["id"] = null;
    this.values_days["id"] = null;
    this.getAjaxValues("id", id, newDate);
  },

  /**
   * Does not really destroy the object, but stops all timers and updates, we can then
   * delete anything related to this chart.
   */
  destroy: function() {
    if (this.quotePlayer) {
      this.quotePlayer.clearUpdateEvents(); // stop updates for this chart.
      this.quotePlayer = null; // stop the player for this id.
    }
  },

  /*
   * Create an empty chart value object for a given date. This is
   * done to pad out the start of a graph when a fixed width domain is required, but there is not
   * enough real data to fill the domain. Eg, for a 3-month chart, if there is only 2 month's data,
   * we need 1 month's worth of padding values.
   *
   * This method is used only for full-day values for historical charts and should not be used
   * for intraday values.
   *
   * paddingDate: Date object for the date we want the chart value object to refer to.
   */
  createPaddingVal: function(paddingDate) {

    // Get the date of the previous weekday (used when determining newMonth and newYear).
    var previousMsec = paddingDate.getTime() - this.statics.ONE_DAY_MSEC;
    var previousDate = new Date(previousMsec);
    while (previousDate.getDay() == 0 || previousDate.getDay() == 6) {
      previousMsec -= this.statics.ONE_DAY_MSEC;
      previousDate = new Date(previousMsec);
    }

    // Construct the padding value, setting newXXX booleans as required.
    var paddingVal = {
        d: paddingDate.getTime(),
        date: paddingDate,
        hour: paddingDate.getHours(),
        dayDate: paddingDate.getDate(),
        day: paddingDate.getDay(),
        month: paddingDate.getMonth(),
        fullyear: paddingDate.getFullYear(),
        newHour: false,
        newDay: true,
        newWeek: paddingDate.getDay() == 1, // Monday
        newMonth: previousDate.getMonth() < paddingDate.getMonth(),
        newYear: previousDate.getFullYear() < paddingDate.getFullYear()};
    return paddingVal;
  }
});    // end of Chart main class


// Some functions used for small and large charts

function chartGetNiceDateFromEventDate(eventDate) {
  return chartGetNiceDate(eventDate) + " (" + chartGetNiceHourMinutes(eventDate) + ")";
};

function chartGetNiceHourMinutes(eventDate) {
  var date = tSd(eventDate);
  var hours = eventDate.getHours();
  if (hours < 10) hours = "0" + hours;
  var minutes = eventDate.getMinutes();
  if (minutes < 10) minutes = "0" + minutes;
  return hours +":"+ minutes;
};

function chartGetNiceDate(eventDate) {
  var day = eventDate.getDate();
  if (day < 10) day = "0" + day;
  var month = eventDate.getMonth()+1;
  if (month < 10) month = "0" + month;
  return day +"."+ month +"."+ eventDate.getFullYear();
};

function chartGetNiceNumber(number, fixed) {
  // convert to number, dojo.number.parse does not work as we always have en_us locale for numbers
  // and check if its a valid number
  if (number/1 == NaN || number == 0)
    return "";

  var numberFormatted = null;
  var index = null;
  if (fixed!=null && fixed/1 >= 0) {
    // first make sure that we are operation on Number object
    numberFormatted = ""+(number/1).toFixed(fixed/1);
  } else {
    numberFormatted = number + "";
  }
  index = numberFormatted.indexOf(".");
  if (index == -1) index = numberFormatted.length;

  // format this number to ###'###'##0.00 (dojo uses the locale to set the grouping seperator!)

  while (index > 3) {
    index -= 3;
    numberFormatted = numberFormatted.substring(0, index) + "'" +
    numberFormatted.substring(index);
  }

  return numberFormatted;
}

// These functions handle the seamless transition of the chart over the daylight-saving
// transitions.
function tSd(date) {
  var x = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
      date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
  x.setTime(x.getTime() + dSd(x) + 3600000);
  return x;
}

function dSd(date) {
  return is_dsd(date) ? 3600000 : 0;
}

function is_dsd(date) {
  return ((date.getTime() > getDateAtTimePoint(0,2,2,-1).getTime()) &&
          (date.getTime() < getDateAtTimePoint(0,9,2,-1).getTime()));
}

function getDateAtTimePoint(d, m, h, p) {
  var week = (p < 0) ? 7 * (p + 1) : 7 * (p - 1);
  var nm = (p < 0) ? m + 1 : m;
  var x = new Date(new Date().getUTCFullYear(), nm, 1, h, 0, 0);
  var dOff = 0;
  if (p < 0)
    x.setTime(x.getTime() - 86400000);
  if (x.getDay() != d) {
    dOff = (x.getDay() < d) ? (d - x.getDay()) : 0 - (x.getDay() - d);
    if (p < 0 && dOff > 0)
      week -= 7;
    if (p > 0 && dOff < 0)
      week += 7;
    x.setTime(x.getTime() + ((dOff + week) * 86400000));
  }
  return x;
}

/*
 * Get a starting date for a chart domain. Can only parse arguments of the form ^\d+[dmy]$,
 * ie, any number of digits followed by a single letter from d, m or y.
 * Anything else return today's date.
 */
function getDomainStartDate(domain) {
  var pattern = /^(\d+)([dmy])$/;
  var tokens = domain.match(pattern);
  if (tokens && tokens.length > 2) {
    if (tokens[2] == "d")
      return rewindDateByDays(tokens[1]);
    else if (tokens[2] == "m")
      return rewindDateByMonths(tokens[1]);
    else if (tokens[2] == "y")
      return rewindDateByYears(tokens[1]);
  }
  return new Date();
}
/*
 * Get a date nRewindDays before today.
 */
function rewindDateByDays(nRewindDays) {
  var lastDay = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  var todayDate = new Date();
  var nDay = todayDate.getDate();
  var nMon = todayDate.getMonth();
  var nYear = todayDate.getFullYear();
  for (var i = 0; i < nRewindDays; i++) {
    nDay--;
    if (nDay == 0) {
      nMon--;
      if (nMon < 0) {
        nMon = 11;
        nYear--;
      }
      nDay = lastDay[nMon];
    }
  }
  return new Date(nYear, nMon, nDay);
}
/*
 * Get a date nRewindMonths before today.
 */
function rewindDateByMonths(nRewindMonths) {
  var todayDate = new Date();
  var nDay = todayDate.getDate();
  var nMon = todayDate.getMonth();
  var nYear = todayDate.getFullYear();
  for (var i = 0; i < nRewindMonths; i++) {
    nMon--;
    if (nMon < 0) {
      nMon = 11;
      nYear--;
    }
  }
  return new Date(nYear, nMon, nDay);
}
/*
 * Get a date nRewindYears before today.
 */
function rewindDateByYears(nRewindYears) {
  var todayDate = new Date();
  var nDay = todayDate.getDate();
  var nMon = todayDate.getMonth();
  var nYear = todayDate.getFullYear() - nRewindYears;
  return new Date(nYear, nMon, nDay);
}

/*
 * Remove the "px" ending on position data, thus allowing it to be used in calculations.
 */
function rmvPX(positionData) {
  var rv = parseInt(positionData);
  return isNaN(rv) ? 0 : rv;
}

/*
 * Add the "px" ending on position data, as required by strict-mode.
 */
function addPX(positionData) {
  var rv = rmvPX(positionData); // clear pre-existing text
  return rv + "px";
}

