/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var htrace = htrace || {};

htrace.SearchResultsView = Backbone.View.extend({
  // The minimum time span we will allow between begin and end.
  MINIMUM_TIME_SPAN: 100,

  begin: 0,

  end: this.MINIMUM_TIME_SPAN,

  processNameFraction: 0.2,

  initialize: function(options) {
    this.searchResults = options.searchResults;
    this.el = options.el;
    var view = this;

    // Re-render the canvas when the window size changes.
    // Add a debouncer delay to avoid spamming render requests.
    $(window).on("resize", _.debounce(function()  {
      view.render();
    }, 250));
  },

  // Get the canvas X coordinate of a mouse click from the absolute event
  // coordinate.
  getCanvasX: function(e) {
    return e.pageX - $("#resultsCanvas").offset().left;
  },

  // Get the canvas Y coordinate of a mouse click from the absolute event
  // coordinate.
  getCanvasY: function(e) {
    return e.pageY - $("#resultsCanvas").offset().top;
  },

  handleMouseDown: function(e) {
    e.preventDefault();
    this.widgetManager.handle({
      type: "mouseDown",
      x: this.getCanvasX(e),
      y: this.getCanvasY(e),
      raw: e
    });
    this.draw();
  },

  handleMouseUp: function(e) {
    e.preventDefault();
    this.widgetManager.handle({
      type: "mouseUp",
      x: this.getCanvasX(e),
      y: this.getCanvasY(e),
      raw: e
    });
    this.draw();
  },

  handleMouseOut: function(e) {
    e.preventDefault();
    this.widgetManager.handle({
      type: "mouseOut"
    });
    this.draw();
  },

  handleMouseMove: function(e) {
    e.preventDefault();
    this.widgetManager.handle({
      type: "mouseMove",
      x: this.getCanvasX(e),
      y: this.getCanvasY(e),
      raw: e
    });
    this.draw();
  },

  handleDblclick: function(e) {
    e.preventDefault();
    this.widgetManager.handle({
      type: "dblclick",
      x: this.getCanvasX(e),
      y: this.getCanvasY(e),
      raw: e
    });
    this.draw();
  },

  render: function() {
    console.log("SearchResultsView#render.");
    $(this.el).html(_.template($("#search-results-view-template").html()));
    $('#selectedTime').attr('readonly', 'readonly');
    this.canvas = $("#resultsCanvas");
    this.ctx = this.canvas.get(0).getContext("2d");
    this.setupCoordinates();
    this.setupWidgets();
    this.scaleCanvas();
    this.draw();
    this.attachEvents();
    return this;
  },

  /*
   * Compute the ratio to use between the size of the canvas (i.e.
   * canvas.ctx.width, canvas.ctx.height) and the size in "HTML5 pixels." Note
   * that 'HTML5 pixels" don't actually correspond to screen pixels.  A line 1
   * "HTML5 pixel"  wide actually takes up multiple scren pixels, etc.
   *
   * TODO: fix this to be sharper
   */
  computeScaleFactor: function() {
    var backingStoreRatio = this.ctx.backingStorePixelRatio ||
          this.ctx.mozBackingStorePixelRatio ||
          this.ctx.msBackingStorePixelRatio ||
          this.ctx.webkitBackingStorePixelRatio ||
          this.ctx.oBackingStorePixelRatio ||
          this.ctx.backingStorePixelRatio || 1;
    return (window.devicePixelRatio || 1) / backingStoreRatio;
  },

  // Sets up the canvas size and scaling.
  scaleCanvas: function() {
    var ratio = this.computeScaleFactor();
    //console.log("scaleCanvas: cssX=" + cssX + ", cssY=" + cssY + ", ratio=" + ratio);
    $('#searchView').css('height', this.canvasY + "px");
    $('#resultsView').css('height', this.canvasY + "px");
    $('#resultsCanvas').css('height', this.canvasY + "px");
    this.ctx.canvas.width = this.viewX * ratio;
    this.ctx.canvas.height = this.canvasY * ratio;
    this.ctx.scale(ratio, ratio);
  },

  //
  // Set up the screen coordinates.
  //
  //  0              xB         xD                   xS         viewX
  //  +--------------+----------+--------------------+-----------+
  //  |TracerId      | Buttons  | Span Description   | Scrollbar |
  //  +--------------+----------+--------------------+-----------+
  //
  setupCoordinates: function() {
    this.viewX = this.canvas.parent().innerWidth();
    this.viewY = $(window).innerHeight() - $("#header").innerHeight() - 50;
    this.xB = Math.floor(this.viewX * this.processNameFraction);
    this.xD = this.xB + Math.min(75, Math.floor(this.viewX / 20));
    var scrollBarWidth = Math.min(50, Math.floor(this.viewX / 10));
    this.xS = this.viewX - scrollBarWidth;
    this.canvasY = this.viewY;
  },

  setupWidgets: function() {
    var searchResultsView = this;
    this.widgetManager = new htrace.WidgetManager({searchResultsView: this});

    var partitionWidgetWidth = Math.max(5, Math.floor(this.viewX / 300));

    // Create a SpanWidget for each span we know about
    var spanWidgetHeight = 16;
    var numResults = this.searchResults.size();
    var groupY = 0;
    for (var i = 0; i < numResults; i++) {
      var widget = new htrace.SpanGroupWidget({
        manager: this.widgetManager,
        ctx: this.ctx,
        span: this.searchResults.at(i),
        x0: 0,
        xB: this.xB + partitionWidgetWidth,
        xD: this.xD,
        xF: this.xS,
        y0: groupY,
        begin: this.begin,
        end: this.end,
        spanWidgetHeight: spanWidgetHeight
      });
      groupY = widget.yF;
    }
    if (this.canvasY < groupY) {
      this.canvasY = groupY;
    }

    // Create the draggable horizontal parition between process names and span
    // names.
    new htrace.PartitionWidget({
      el: '#resultsCanvas',
      manager: this.widgetManager,
      ctx: this.ctx,
      x0: this.xB,
      xF: this.xB + partitionWidgetWidth,
      xMin: Math.floor(this.viewX * 0.10),
      xMax: Math.floor(this.viewX * 0.90),
      y0: 0, 
      yF: groupY,
      releaseHandler: function(x) {
        searchResultsView.processNameFraction = (x / searchResultsView.viewX);
        console.log("htrace#PartitionWidget setting processNameFraction to " +
              searchResultsView.processNameFraction);
        searchResultsView.render();
      }
    });

    // Create the time cursor widget.
    var selectedTime = this.begin;
    if (this.timeCursor != null) {
      selectedTime = this.timeCursor.selectedTime;
    }
    this.timeCursor = new htrace.TimeCursor({
      manager: this.widgetManager,
      selectedTime: selectedTime,
      el: "#selectedTime"
    });
    this.timeCursor.ctx = this.ctx;
    this.timeCursor.x0 = this.xD;
    this.timeCursor.xF = this.xS;
    this.timeCursor.y0 = 0;
    this.timeCursor.yF = this.canvasY;
    this.timeCursor.begin = this.begin;
    this.timeCursor.end = this.end;
  },

  draw: function() {
    if (this.checkCanvasTooSmall()) {
      return;
    }

    // Set the background to white.
    this.ctx.save();
    this.ctx.fillStyle="#ffffff";
    this.ctx.strokeStyle="#000000";
    this.ctx.fillRect(0, 0, this.viewX, this.canvasY);
    this.ctx.restore();

    // Draw all the widgets.
    this.widgetManager.handle({type: "draw"});
  },

  checkCanvasTooSmall: function() {
    if ((this.viewX < 200) || (this.viewY < 200)) {
      this.ctx.fillStyle="#cccccc";
      this.ctx.strokeStyle="#000000";
      this.ctx.fillRect(0, 0, this.viewX, this.viewY);
      this.ctx.font = "24px serif";
      this.ctx.fillStyle="#000000";
      this.ctx.fillText("Canvas too small!", 0, 24);
      return true;
    }
    return false;
  },

  attachEvents: function() {
    // Use jquery to capture mouse events on the canvas.
    // For some reason using backbone doesn't work for getting these events.
    var view = this;
    $("#resultsCanvas").off("mousedown");
    $("#resultsCanvas").on("mousedown", function(e) {
      $("#resultsCanvas").focus();
      view.handleMouseDown(e);
    });
    $("#resultsCanvas").off("mouseup");
    $("#resultsCanvas").on("mouseup", function(e) {
      view.handleMouseUp(e);
    });
    $("#resultsCanvas").off("mouseout");
    $("#resultsCanvas").on("mouseout", function(e) {
      view.handleMouseOut(e);
    });
    $("#resultsCanvas").off("mousemove");
    $("#resultsCanvas").on("mousemove", function(e) {
      view.handleMouseMove(e);
    });
    $("#resultsCanvas").off("dblclick");
    $("#resultsCanvas").on("dblclick", function(e) {
      view.handleDblclick(e);
    });
    // Keyboard events.  These events only fire if the canvas has focus.  So if
    // you press delete when entering a time in the time dialog box, this will
    // not fire.  Etc.
    $("#resultsCanvas").off("keyup");
    $("#resultsCanvas").on("keyup", function(e) {
      if (e.keyCode == 46) { // delete key
        view.clearHandler(false);
        return false;
      } else if (e.keyCode == 90) { // z key
        view.zoomHandler();
        return false;
      } else {
        return true;
      }
    });
    $("#resultsCanvas").off("contextmenu");
    $("#resultsCanvas").on("contextmenu", function(e) {
      return false;
    });
  },

  remove: function() {
    $(window).off("resize");
    $("#resultsCanvas").off("mousedown");
    $("#resultsCanvas").off("mouseup");
    $("#resultsCanvas").off("mouseout");
    $("#resultsCanvas").off("mousemove");
    $("#resultsCanvas").off("dblclick");
    $("#resultsCanvas").off("keyup");
    $("#resultsCanvas").off("contextmenu");
    Backbone.View.prototype.remove.apply(this, arguments);
  },

  handleBeginOrEndChange: function(e, type) {
    e.preventDefault();
    var text = $(e.target).val().trim();
    var d = null;
    try {
      d = htrace.parseDate(text);
    } catch(err) {
      $("#begin").val(htrace.dateToString(this.begin));
      $("#end").val(htrace.dateToString(this.end));
      htrace.showModalWarning("Timeline " + type + " Format Error",
        "Please enter a valid time in the timeline " + type + " field.<p/>" +
        err);
      return null;
    }
    if (type === "begin") {
      this.setTimes({begin: d.valueOf()});
    } else if (type === "end") {
      this.setTimes({end: d.valueOf()});
    } else {
      throw "invalid type for handleBeginOrEndChange: expected begin or end.";
    }
    this.render();
  },

  setTimes: function(params) {
    if (params["begin"]) {
      this.begin = params["begin"];
    }
    if (params["end"]) {
      this.end = params["end"];
    }
    if (this.end < this.begin) {
      var b = this.begin;
      this.begin = this.end;
      this.end = b;
    }
    var delta = this.end - this.begin;
    if (delta < this.MINIMUM_TIME_SPAN) {
      var needed = this.MINIMUM_TIME_SPAN - delta;
      this.begin -= (needed / 2);
      this.end += (needed / 2);
    }
    $("#begin").val(htrace.dateToString(this.begin));
    $("#end").val(htrace.dateToString(this.end));
    // caller should invoke render()
  },

  clearHandler: function(clearAllIfNoneSelected) {
    console.log("invoking clearHandler.");
    var toDelete = []
    var noneSelected = true;
    for (var i = 0; i < this.searchResults.length; i++) {
      var resultSelected = false;
      var model = this.searchResults.at(i);
      htrace.treeTraverseDepthFirstPre(model,
        htrace.getReifiedChildren, 0,
          function(node, depth) {
            if (noneSelected) {
              if (node.get("selected")) {
                resultSelected = true;
              }
            }
          });
      htrace.treeTraverseDepthFirstPre(model,
        htrace.getReifiedParents, 0,
          function(node, depth) {
            if (node.get("selected")) {
              resultSelected = true;
            }
          });
      if (resultSelected) {
        if (noneSelected) {
          toDelete = [];
          noneSelected = false;
        }
        toDelete.push(model);
      } else if (noneSelected) {
        toDelete.push(model);
      }
    }
    if (noneSelected && (!clearAllIfNoneSelected)) {
      return;
    }
    ids = [];
    for (var i = 0; i < toDelete.length; i++) {
      ids.push(toDelete[i].get("spanId"));
    }
    console.log("clearHandler: removing " + JSON.stringify(ids));
    this.searchResults.remove(toDelete);
    this.render();
  },

  getSelectedSpansOrAllSpans: function() {
    // Get the list of selected spans.
    // If there are no spans selected, we return all spans.
    var ret = [];
    var noneSelected = true;
    this.applyToAllSpans(function(span) {
        if (span.get("selected")) {
          if (noneSelected) {
            ret = [];
            noneSelected = false;
          }
          ret.push(span);
        } else if (noneSelected) {
          ret.push(span);
        }
      });
    return ret;
  },

  zoomHandler: function() {
    var zoomSpans = this.getSelectedSpansOrAllSpans();
    var numResults = zoomSpans.length;
    if (numResults == 0) {
      this.setTimes({begin:0, end:this.MINIMUM_TIME_SPAN});
      this.render();
      return;
    }
    var minStart = 4503599627370496;
    var maxEnd = 0;
    for (var i = 0; i < numResults; i++) {
      var begin = zoomSpans[i].getEarliestBegin();
      if (begin < minStart) {
        minStart = begin;
      }
      var end = zoomSpans[i].getLatestEnd();
      if (end > maxEnd) {
        maxEnd = end;
      }
    }
    this.setTimes({begin: minStart, end: maxEnd});
    this.render();
  },

  // Apply a function to all spans
  applyToAllSpans: function(cb) {
    for (var i = 0; i < this.searchResults.length; i++) {
      htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
        htrace.getReifiedChildren, 0,
          function(node, depth) {
            cb(node);
          });
      htrace.treeTraverseDepthFirstPre(this.searchResults.at(i),
        htrace.getReifiedParents, 0,
          function(node, depth) {
            if (depth > 0) {
              cb(node);
            }
          });
    }
  }
});
