//
//  ENCPPresenter.js
//  CoverPage
//
//  Created by Zdzislaw Losvik on 17/04/2013.
//  Copyright (c) 2013 Evernote. All rights reserved.
//


(function(jquery) {

  var $ = jquery;

  $.fn.extend({
    reverse: function() {
        return this.pushStack(this.get().reverse(), arguments);
    },

    isOnScreen: function(scrollPosition) {
      if (!this.is(":visible")) {
        return false;
      }
      var docViewBottom = scrollPosition + window.encpPlatform.viewPort().height;
      var elemTop = this.offset().top;
      var elemBottom = elemTop + this.height();
      return ((elemBottom <= docViewBottom) && (elemTop >= scrollPosition));
    },

    isBelowViewPort: function(scrollPosition) {
      if (!this.is(":visible")) {
        return false;
      }
      var docViewBottom = scrollPosition + window.encpPlatform.viewPort().height;
      var elemTop = this.offset().top;
      var elemBottom = elemTop + this.height();
      return elemBottom > docViewBottom;
    },

    isAboveViewPort: function(scrollPosition) {
      if (!this.is(":visible")) {
        return false;
      }
      var elemTop = this.offset().top;
      return elemTop < scrollPosition;
    }

  });

  function ENCPPresenter() {
  }

  ENCPPresenter.prototype = {

    REFERENCE_SCREEN_RESOLUTION: 1440,
    REFERENCE_FONT_SIZE_IN_PX: 30,
    STANDARD_EM_SIZE_IN_PX: 16,
    DEFAULT_SCROLL_OVERLAP: 90,
    MIN_IMAGE_SIZE: 100.0,
    MAX_IMAGE_DISPLAY_HEIGHT_FACTOR: 90.0 / 100.0,
    MAX_IMAGE_DISPLAY_WIDTH_FACTOR: 70.0 / 100.0,
    HEIGHT_CHANGED_EVENT: 'heightChanged',

    /** Track how many times we reached the end of the note */
    didReachEndOfNoteCount: 0,

    /** remember old document height to calculate new scrollTop() after resizing */
    previousDocumentHeight: 0,
    previousScrollTop: 0,

    /** Scroll position */
    lastScrollPosition: 0,
    targetScrollPosition: -1,
    scrollMarker: null,
    markerPosition: -1,
    isTouched: false,
    isRecoveringScrollPosition: false,
    isScrolling: false,
    scrollStack: [], // scroll position history

    /** reference screen resolution */
    referenceScreenResolution: 0,
    referenceFontSize: 0,

    /** The focused link object */
    focused: null,

    /** A key code for the last pressed key */
    lastKeyPress: undefined,

    /** Available style sheets */
    styleSheets: [],

    /** Element cache in the page to determine the scroll boundary */
    visibleImageElements: undefined,
    visibleTextElements: $(),
    snappableElements: $(),

    onMediaClick: function (mediaNode) {
      var $mediaNode = $(mediaNode);
      if (!$mediaNode.hasClass('encp-can-open-attachment')) {
        return;
      }
      var mediaFrame = this.elementFrame($mediaNode.find('.encp-media-preview img').get(0));
      window.encpPlatform.didClickMediaWithParameters({
        'src': $mediaNode.attr("data-media-url"),
        'mime': $mediaNode.attr("data-media-mime"),
        'filename': $mediaNode.attr("data-media-filename"),
        'mediaFrame': mediaFrame
      });
    },

    elementFrame: function (element) {
      var $element = $(element);
      var offset = $element.offset();
      return {X: offset.left, Y: offset.top, Width: $element.width(), Height: $element.height()};
    },

    prepareExitGalleryModeForMediaAtPath: function(mediaPath, rootElement) {
      var self = this;
      if (!rootElement) {
        rootElement = $("body").get(0);
      }
      var $imageNode = $(rootElement).find("[data-media-url$='" + mediaPath + "'] .encp-media-preview img");
      if ($imageNode.length == 0) {
        return null;
      }
      var scrollPosition = self.scrollPosition();
      if (!$imageNode.isOnScreen(scrollPosition)) {
        this.scrollIntoView($imageNode.get(0));
      }
      var mediaFrame = self.elementFrame($imageNode.get());
      return JSON.stringify({'mediaFrame': mediaFrame});
    },

    appendDocumentShadows: function () {
      var self = this;
      $(".encp-media-preview img").each(function () {
        var $image = $(this);
        //noinspection JSUnresolvedFunction
        var mediaElement = $image.closest('.encp-media-container')
        var mediaURL = mediaElement.attr("data-media-url");
        var mediaMIME = mediaElement.attr("data-media-mime");
        window.encpPlatform.requestDocumentInformationForURL(encodeURI(mediaURL), mediaMIME,
            function (isDocument, numberOfPages, isRetina, predefinedName) {
              // success callback
              if (isDocument) {
                self.appendDocumentShadowsToImage($image, numberOfPages);
              }
              if (isRetina) {
                $image.addClass("encp-retina");
              }
              if (predefinedName) {
                $image.addClass('encp-predefined-preview');
                $image.addClass('en-preview-' + predefinedName);
              }
            },
            function () {
              // error callback
            });
      });
    },

    appendDocumentShadowsToImage: function(image, numberOfPages) {
      image.addClass("encp-document");
      image.wrap($("<div class='encp-media-container-document'></div>"));
      var theContainer = image.parent();
      if (numberOfPages > 1) {
        theContainer.append($("<div class='encp-shadow encp-first-shadow'></div>"));
      }
      if (numberOfPages > 2) {
        theContainer.append($("<div class='encp-shadow encp-second-shadow'></div>"));
      }
    },

    proceedLayout: function() {
      var self = this;
      var pageBreaksEnabled = (self.pageBreaksEnabled() && self.isNoteSource());
      ENCPDeferred
        .doNow()
        .then(function() {
          // Layout procedure - step 1 : scaling page.
          self.scalePresentation();
          if (pageBreaksEnabled) {
            self.resetPageBreaks();
          }
          // CP-567 - workaround image layout problem by remove/adding the encp-media-preview class. This forces webkit layout.
          $(".encp-media-preview").addClass("encp-layout").removeClass("encp-media-preview");
        })
        .then(function() {
          // Layout procedure - step 2 : resizing elements on the page.
          self.layoutTitle($('#ENCPClone')[0]);
          self.adjustRetinaImages();
          $(".encp-layout").addClass("encp-media-preview").removeClass("encp-layout");
          self.setupElements();
          if (pageBreaksEnabled) {
            self.setupPageBreaks();
          }
        })
        .then(function() {
          // Layout procedure - step 3 : recovering scroll position if necessary.
          if (self.previousDocumentHeight && this.previousScrollTop) {
            $(window).scrollTop(self.previousScrollTop * $(document).height() / self.previousDocumentHeight);
            self.previousScrollTop = 0;
          }
          self.isUpdatingLayout = false;
          self.recoverScrollPosition();
        })
        .done();
    },

    updateLayout: function() {
      var self = this;
      var oldViewPort = self.viewPort();
      ENCPDeferred
        .doNow(function() {
          // Notify host starting layout procedure.
          window.encpPlatform.willStartLayouting();
          self.isUpdatingLayout = true;
        })
        .then(function() {
          // Check viewPort update so that we can execute layout specific procedure.
          var newViewPort = self.viewPort();
          if (window.encpPlatform.hasViewPortBeenOverriden() &&
              oldViewPort.height == newViewPort.height &&
              oldViewPort.width == newViewPort.width) {
            self.isUpdatingLayout = false;
            self.recoverScrollPosition();
          } else {
            self.proceedLayout();
          }
        })
        .waitUntil(function() {
          return !self.isUpdatingLayout;
        })
        .then(function() {
          // Notify host finishing layout procedure.
          window.encpPlatform.didFinishLayouting();
        })
        .done();
    },

    registerResizeHandler: function() {
      var self = this;
      $(window).on("resize", function() {
        self.updateLayout();
      });
    },

    updateDocumentHeight: function() {
      var self = this;
      var documentHeight = $(document).height();
      if (documentHeight != self.previousDocumentHeight) {
        self.previousDocumentHeight = documentHeight;
        document.dispatchEvent(new Event(self.HEIGHT_CHANGED_EVENT));
      }
    },

    willChangeScreen: function() {
      this.previousScrollTop = $(window).scrollTop();
      this.updateDocumentHeight();
    },

    setReferenceSize: function(newResolution,newFontSize) {
      if (newResolution <= 0 || newFontSize <= 0) {
        newResolution = this.REFERENCE_SCREEN_RESOLUTION;
        newFontSize = this.REFERENCE_FONT_SIZE_IN_PX;
      }
      if (this.referenceScreenResolution != newResolution ||
          this.referenceFontSize != newFontSize) {
        this.referenceScreenResolution = newResolution;
        this.referenceFontSize = newFontSize;
      }
      this.scalePresentation();
      if (this.pageBreaksEnabled() && this.isNoteSource()) {
        this.resetPageBreaks();
        this.setupPageBreaks();
      }
      return true;
    },

    zoomFactorInPercent: function() {
      return (this.referenceFontSize * 100 / this.STANDARD_EM_SIZE_IN_PX) * this.viewPort().width / this.referenceScreenResolution;
    },

    scalePresentation: function() {
      var self = this;
      var $html = $("html");
      var percent = 0;
      if (self.isNoteSource()) {
        percent = this.zoomFactorInPercent();
        $html.css("font-size", "" + percent + "%" );
      } else if (!$html.hasClass("encp-image-only")) {
        window.encpPlatform.adjustZoomLevel($html.find('#ENCPClone'),self.referenceScreenResolution);
      }
      this.adjustImageSize();
      this.centerErrorMessage();
    },

    centerErrorMessage: function () {
        var $error = $('.encp-error');
        if ($error.length == 0) return;
        $error.css({
            'position': 'absolute',
            'height': $error.height(),
            'margin-top': Math.round($error.height() / 2) * -1
        });
    },

    proceedPageDown: function() {
      var self = this;
      if (self.focused && self.focused.hasClass('encp-note-link')) {
        window.location.href = self.focused.attr('href');
        return;
      }
      if (self.isEndOfNote()) {
        window.encpPlatform.navigateToNextNote();
        return;
      }
      self.scrollToNextBoundary();
    },

    proceedPageUp: function() {
      var self = this;
      if (self.isBeginningOfNote()) {
        window.encpPlatform.navigateToPreviousNote();
        return;
      }
      self.scrollToPreviousBoundary();
    },

    registerKeydownHandler: function () {
      var self = this;
      $(window).on("keydown", function (event) {
        if (self.isScrolling) {
          event.preventDefault();
          event.returnValue = false;
          return;
        }
        if (event.keyCode == 32
            && self.lastKeyPress == event.keyCode
            && self.targetScrollPosition == -1) {
          // allow space key long-press
          self.lastKeyPress = undefined;
        }
        if (self.lastKeyPress !== undefined) {
          event.preventDefault();
          event.returnValue = false;
          return;
        }
        var callback = null;
        if (event.keyCode == 34) { // handle page down
          callback = self.proceedPageDown;
        } else if (event.keyCode == 33) { // handle page up
          callback = self.proceedPageUp;
        } else if (event.keyCode == 32) { // handle space key
          if (event.altKey || event.ctrlKey || event.metaKey) {
            callback = window.encpPlatform.beep;
          } else {
            callback = (event.shiftKey) ? self.scrollToPreviousBoundary : self.scrollToNextBoundary;
          }
        } else if (event.keyCode == 72 && event.ctrlKey) {
          // control+h (toggle page breaks)
          if (self.isNoteSource()) {
            self.togglePageBreaks();
          } else {
            window.encpPlatform.beep();
          }
        }
        if (callback) {
          callback.call(self);
          self.lastKeyPress = event.keyCode;
          event.preventDefault();
          event.returnValue = false;
        }
      });
      $(window).on("keyup", function(event) {
        if (self.lastKeyPress === event.keyCode) {
          self.lastKeyPress = undefined;
          event.preventDefault();
          event.returnValue = false;
        }
      });
    },

    registerClickListener: function() {
      var self = this;
      $(".encp-media-container").on("click", function (event) {
        event.preventDefault();
        var $target = $(event.target);
        if ($target.parents().andSelf().is("img, .encp-attachment-box")) {
          self.onMediaClick(this);
        }
      });
    },

    disableInputElements: function() {
      $("input").attr("disabled", "true");
    },

    registerFocusListener: function() {
      var self = this;
      var anchors = $('a');
      anchors.on('focus', function (event) {
        self.focused = $(this);
      });
      anchors.on('blur', function (event) {
        if (self.focused && self.focused == $(this)) {
          self.focused = null;
        }
      });
    },

    validNoteLinks: function() {
      return $(".encp-note-link:not(.encp-broken-link)");
    },

    setupInitialFocus: function() {
      var self = this;
      var noteLinks = self.validNoteLinks();
      if (!noteLinks.length || $(window).scrollTop() > 0 || self.focused) {
        return;
      }
      var firstLink = $(noteLinks.get(0));
      if (firstLink.offset().top < self.viewPort().height) {
        firstLink.focus();
      }
    },

    setupStyleSheets: function() {
      var self = this;
      $('link[rel*=stylesheet]').each(function() {
        var style = this;
        if ($(style).attr('title')) {
          style.disabled = ($(style).attr('rel').indexOf('alt') > -1);
          self.styleSheets.push(style);
        }
      });
    },

    setupElements: function() {
      var self = this;
      self.visibleImageElements = undefined;
      self.snappableElements = $();
      self.visibleTextElements = $();
      var visibleNonImageElements = $("body").find("*:visible").not("img, br");
      self.snappableElements = visibleNonImageElements.filter(function() {
        var $elem = $(this);
        if ($elem.is(".checkbox, hr, tr")) {
          return true;
        }
        var children = $elem.children().not('br, #en-editor-last-insertion-point');
        return (children.length == 0 && $.trim($elem.text()).length > 0);
      });
      self.visibleTextElements = visibleNonImageElements.filter(function() {
        return $.trim($(this).text()).length > 0;
      });
    },

    pageBreakOffset: function($nextPageBreak) {
      if ($nextPageBreak.length > 0) {
        return $nextPageBreak.offset().top;
      } else {
        return $(document).height();
      }
    },

    togglePageBreaks: function() {
      var self = this;
      if (self.pageBreaksEnabled()) {
        self.resetPageBreaks();
      } else {
        self.setupPageBreaks();
      }
    },

    pageBreaksEnabled: function() {
      return $("hr.encp-page-break").length > 0;
    },

    resetPageBreaks: function() {
      // Reset state
      var $body = $("body");
      var $visibleElements = $body.find("*:visible");
      var $pageBreaks = $visibleElements.filter("hr.encp-page-break");
      if ($pageBreaks.length > 0) {
        $body.css("margin-bottom", "");
        $visibleElements.filter(".encp-shrunken-image").css("height", "").css("width", "").removeClass("encp-shrunken-image");
        $pageBreaks.css("margin-bottom", "").css("margin-top", "").css("padding-bottom","");
        $visibleElements.removeClass("encp-page-break");
      }
    },

    setupPageBreaks: function() {
      var self = this;
      var windowHeight = self.viewPort().height;
      var $body = $("body");
      var $visibleElements = $body.find("*");

      // process page breaks
      var $hrElements = $visibleElements.filter("hr");
      $hrElements.each(function (hrIndex) {
        var $this = $(this);
        var index = $visibleElements.index($this);
        var $nextPageBreak = $();
        var endIndex = $visibleElements.length - 1 ;
        if (hrIndex < $hrElements.length - 1) {
          $nextPageBreak = $($hrElements.get(hrIndex + 1));
          endIndex = $visibleElements.index($nextPageBreak);
        }
        var $elementsInBetween = $visibleElements.slice(index + 1, endIndex);
        $elementsInBetween.filter(".encp-media-container").addClass("encp-inside-page");

        $this.addClass("encp-page-break");
        // add margins to the top/bottom hr to fill the screen
        var bottomOfPage = self.pageBreakOffset($nextPageBreak);
        var pageSize = bottomOfPage - $this.offset().top;
        if (pageSize <= windowHeight) {
          function adjustMargins(margin) {
            // CP-1277: Use padding-bottom as margin-bottom doesn't work inside <li> elements
            $this.css("padding-bottom", margin + "px");
            if ($nextPageBreak.length > 0) {
              $nextPageBreak.css("margin-top", margin + "px");
            } else {
              $body.css("margin-bottom", margin + "px");
            }
          }
          var theMargin = (windowHeight - pageSize) / 2;
          adjustMargins(theMargin);
          // we need to do it a second time... no idea why.
          pageSize = self.pageBreakOffset($nextPageBreak) - $this.offset().top;
          theMargin += (windowHeight - pageSize) / 2;
          adjustMargins(theMargin);
        } else {
          var marginTop = $this.css("margin-top");
          if (marginTop.length == 0 || marginTop == "0px") {
            $this.css("margin-top", "2rem");
          }
        }
      });
    },

    setup: function() {
      var self = this;

      self.referenceScreenResolution = self.REFERENCE_SCREEN_RESOLUTION;
      self.referenceFontSize = self.REFERENCE_FONT_SIZE_IN_PX;


      // Register event listeners
      if (window.encpPlatform.registerEventListeners) {
        window.encpPlatform.registerEventListeners();
      }
      self.registerImageLoadListener();
      self.registerClickListener();
      self.registerScrollListener();
      if (window.encpPlatform.shouldHandleResizing()) {
        self.registerResizeHandler();
      }
      self.registerBrokenLinkListener();

      self.scalePresentation();
      self.registerKeydownHandler();
      self.registerFocusListener();
      self.appendDocumentShadows();
      self.disableInputElements();

      self.previousDocumentHeight = $(document).height();
      self.updateHeightInterval = window.setInterval(function() {
        self.updateDocumentHeight();
      },200);

      $(window).focus( function() {
        self.setupInitialFocus();
      });
    },

    tearDown: function() {
      ENCPDeferred.tearDown();
    },

    didReachEndOfNote: function(allowSwitchToNextNote) {
      var self = this;

      // JQuery doesn't take into account scale/zoom factor
      // http://bugs.jquery.com/ticket/11114
      var documentHeight = $(document).height();
      var zoomAsString = $("html").css("zoom");
      if (zoomAsString) {
        var zoom = parseFloat(zoomAsString);
        // Its weird, but JQuery return proper value in case zoom > 1.0
        // {CP-1290} No helptip when advancing to the end of a clipper note
        // {CP-939} Remote & Mavericks: cannot advance in the end of the clipper note
        if (zoom < 1.0) {
          documentHeight = (documentHeight * zoom);
        }
      }

      if ($(window).scrollTop() + self.viewPort().height >= documentHeight) {
        // Do not switch
        if (!allowSwitchToNextNote && self.didReachEndOfNoteCount == 1) {
          return false;
        }
        self.didReachEndOfNoteCount += 1;
        window.encpPlatform.didReachEndOfNote(self.didReachEndOfNoteCount);
        return true;
      } else {
        if (self.didReachEndOfNoteCount > 0) {
          self.didReachEndOfNoteCount = 0;
          window.encpPlatform.didReachEndOfNote(self.didReachEndOfNoteCount);
        }
      }
      return false;
    },

    markScrollPosition: function() {
      var self = this;
      if (self.isUpdatingLayout || self.isRecoveringScrollPosition) {
        return;
      }
      if (self.scrollMarker) {
        self.scrollMarker.removeClass('encp-scroll-marker');
      }
      var viewPort = self.viewPort();
      var centerElement = document.elementFromPoint(viewPort.width / 2, viewPort.height / 2);
      var $centerElement = centerElement ? $(centerElement) : $('body');
      if ($centerElement.innerHeight() > viewPort.height) {
        self.markerPosition = ($(window).scrollTop() + viewPort.height / 2) / $(document).height();
      } else {
        self.markerPosition = -1;
      }
      self.scrollMarker = $centerElement;
      self.scrollMarker.addClass('encp-scroll-marker');
      self.targetScrollPosition = -1;
    },

    recoverScrollPosition: function() {
      var self = this;
      if (self.isUpdatingLayout ||
          self.isTouched ||
          self.targetScrollPosition > -1 ||
          !self.scrollMarker) {
        return;
      }
      var viewPort = self.viewPort();
      if (self.markerPosition < 0) {
        var centerElement = document.elementFromPoint(viewPort.width / 2, viewPort.height / 2);
        if (centerElement && $(centerElement).is('.encp-scroll-marker')) {
          return;
        }
      }
      self.isRecoveringScrollPosition = true;
      if (self.markerPosition > 0) {
        self.scrollToTopOffset(($(document).height() * self.markerPosition) -  viewPort.height / 2);
      } else {
        self.scrollIntoView(self.scrollMarker);
      }
      self.markScrollPosition(); // keep marker position up-to-date
    },

    registerScrollListener: function () {
      var self = this;
      self.didReachEndOfNoteCount = 0;
      window.encpPlatform.didScrollToPosition(self.lastScrollPosition);
      window.encpPlatform.didReachEndOfNote(self.didReachEndOfNoteCount);
      var $window = $(window);

      // var listenerId = Date.now();
      // console.log('registering scroll listener ' + listenerId);
      // $window.on("scroll", function () {
      //   console.log(['scroll event triggered for listener id:', listenerId, 'window.scrollTop:', $(window).scrollTop()].join(' '));
      //   if ($window.scrollTop() == self.targetScrollPosition) {
      //     self.targetScrollPosition = -1;
      //   }
      //   if (self.isRecoveringScrollPosition) {
      //     self.isRecoveringScrollPosition = false;
      //   } else {
      //     self.markScrollPosition();
      //   }
      //   self.lastScrollPosition = $window.scrollTop();
      //   window.encpPlatform.didScrollToPosition(self.lastScrollPosition);
      //   self.didReachEndOfNote(false);
      //   self.targetScrollPosition = -1;
      // });
      $window.on('touchstart', function() {
        self.isTouched = true;
      });
      $window.on('touchend', function() {
        self.isTouched = false;
      });
    },

    registerBrokenLinkListener: function() {
      $(".encp-broken-link").on("mousedown", function(event) {
        event.preventDefault();
        return false;
      });
    },

    computeAspectRatios: function() {
      var self = this;
      $(".encp-media-container img").each(function() {
        var image = this;
        var $image = $(image);
        if (!$image.hasClass("encp-document")) {
          var mediaElement = $image.closest('.encp-media-container')
          var mediaURL = mediaElement.attr("data-media-url");
          var mediaMIME = mediaElement.attr("data-media-mime");
          window.encpPlatform.requestDocumentInformationForURL(encodeURI(mediaURL), mediaMIME,
          function (isDocument, numberOfPages, isRetina, predefinedName) {
            // success callback
            if (isRetina) {
              $image.addClass("encp-retina");
            }
            self.computeAspectRatio(image);
          },
          function () {
            // error callback
          });
        } else {
          self.computeAspectRatio(image);
        }
      });
      self.adjustRetinaImages();
    },

    computeAspectRatio: function(element) {
      var self = this;
      var $element = $(element);
      if (element.naturalWidth < element.naturalHeight) {
        $element.addClass("encp-portrait");
      } else {
        $element.addClass("encp-landscape");
      }
      $element.addClass("encp-loaded");
      if ($element.hasClass("encp-retina")) {
        self.adjustRetinaImage(element);
      }
    },

    adjustRetinaImages: function() {
      var self = this;
      $(".encp-retina").each(function() {
        self.adjustRetinaImage(this);
      });
    },

    adjustRetinaImage: function(image) {
      var $image = $(image);
      var $window = $(window);
      var retinaHeight = image.naturalHeight / 2;
      var retinaWidth = image.naturalWidth / 2;
      $image.css("height", "");
      $image.css("width", "");
      // Take sizes from CSS into account
      if (retinaHeight < ($window.height() * this.MAX_IMAGE_DISPLAY_HEIGHT_FACTOR) &&
          retinaWidth < ($window.width() * this.MAX_IMAGE_DISPLAY_WIDTH_FACTOR)) {
        $image.css("height", retinaHeight + "px");
        $image.css("width", retinaWidth + "px");
      }
    },

    registerImageLoadListener: function () {
      var self = this;
      $(".encp-media-container img").bind("load", function() {
        self.computeAspectRatio(this);
      });
    },

    presentationPageLoaded: function (rootElement) {
      this.layoutTitle(rootElement);
      this.setupStyleSheets();
      // compute aspect ratios because image load listener doesn't work in all cases.
      this.computeAspectRatios();
      if (this.isNoteSource()) {
        // CP-1848: line spacing
        $('div > br:only-child').parent().filter(function() {
          return ($(this).text().length == 0);
        }).addClass('encp-line-spacer');
        // setup page breaks after images are loaded
        this.setupPageBreaks();
      }
      this.setupElements();
    },

    layoutTitle: function(rootElement) {
      var $html = $('html');
      var $root = $(rootElement);
      var $title = $("div.encp-note-title", $root);
      if (this.isNoteSource()) {
        try {
          if (!$html.hasClass('encp-nocontents-in-title') && $root.has('hr')) {
            var $firstBreak = $('hr', $root).first();
            if ($firstBreak.length > 0 && $title.next() && $title.next().offset().top == $firstBreak.offset().top) {
              var $bodyContents = $root.contents();
              var start = $bodyContents.index($title) + 1;
              var end = $bodyContents.index($firstBreak);
              while (end == -1) {
                $firstBreak = $firstBreak.parent();
                end = $bodyContents.index($firstBreak);
                if ($firstBreak.is($root)) {
                  break;
                }
              }
              if (start > 0 && end > -1 && $bodyContents.slice(start,end).text().trim().length == 0) {
                $html.addClass("encp-nocontents-in-title");
              }
            }
          }
        } catch(e) {
          this.log('failed getting the first contents before the first break. : ' + e);
        };
      }
      if ($html.hasClass("encp-nocontents-in-title")) {
        $title.css("margin-top", "0px").css("margin-bottom", "0px").css("padding-top", "0px").css("padding-bottom", "0px");
        if ($title.length > 0) {
          var height = $title.height() + (parseFloat($title.css('margin-top')) || 0) + (parseFloat($title.css('margin-bottom')) || 0);
          var margin = (this.viewPort().height - height) / 2;
          if (margin < 0) {
            margin = 0;
          }
          $title.css("margin-top", margin + "px").css("margin-bottom", margin + "px");
        }
      }
    },


    scrollIntoView: function(element) {
      $(window).scrollTop($(element).offset().top + ($(element).height() / 2) - (this.viewPort().height / 2))
      self.targetScrollPosition = -1;
    },

    getMediaItems: function () {
      var items = [];
      var self = this;
      $(".encp-media-container").each(function () {
        var src = $(this).attr("data-media-url");
        var mime = $(this).attr("data-media-mime");
        var filename = $(this).attr('data-media-filename');
        if (src && mime) {
          items.push({
            'src' : src,
            'mime' : mime,
            'filename' : filename
          });
        }
      });
      return items;
    },

    getProgressContext: function(progress) {
      var self = this;
      var radius = 50;
      var lineWidth = 6;

      if (!self.canvas) {
        self.canvas = document.createElement('canvas');
      }
      self.canvas.setAttribute('width',(radius * 2) + '');
      self.canvas.setAttribute('height',(radius * 2) + '');

      var center = {x:radius, y:radius};
      var $html = $('html');

      var context = self.canvas.getContext('2d');
      context.beginPath();
      context.moveTo(center.x, center.y);
      context.arc(center.x, center.y, radius - lineWidth, 1.5 * Math.PI, 1.5 * Math.PI + progress * 2 * Math.PI, false);
      context.moveTo(center.x, center.y);
      context.closePath();
      context.fillStyle = $html.css('color');
      context.fill();
      context.beginPath();
      context.arc(center.x, center.y, radius - lineWidth, 0, 2 * Math.PI, false);
      context.closePath();
      context.lineWidth = lineWidth;
      context.strokeStyle = $html.css('color');
      context.stroke();
      return self.canvas.toDataURL();
    },

    updateMediaItem: function(item, mediaElements) {
      if (typeof item == 'string') {
        item = JSON.parse(item);
      }
      if (item.identifier === undefined || item.mimetype === undefined) {
        return;
      }

      var self = this;
      if (!mediaElements) {
        mediaElements = $(".encp-media-container[data-media-url$='" + item.identifier + "']");
      }
      $(mediaElements).each(function() {
        var $mediaNode = $(this);
        var $box = $mediaNode.find(".encp-attachment-box")
        var $icon = $mediaNode.find(".encp-attachment-icon");
        var iconImage = $icon.get(0);
        if (item.loadProgress < 1.0) {
          $icon.off('load');
          if (item.loadProgress > 0.0) {
            $icon.attr('src',self.getProgressContext(item.loadProgress));
            $icon.addClass('encp-attachment-progress');
          } else if (item.iconURL) {
            $icon.attr('src',item.iconURL);
          }
          var $file = $mediaNode.find(".encp-attachment-filename").empty();
          if (item.title && item.title != item.filename) {
            $file.append($('<em />').text(item.title));
          }
          if (item.filename) {
            $file.append($('<span />').addClass('encp-attachment-filename').text(item.filename));
          }
          if (item.information) {
            $file.append($('<span />').text(item.information));
          }
          return;
        }
        if (item.previewURL) {
          $icon.on('load',function() {
            $icon.addClass('en-media');
            $icon.css('width',iconImage.naturalWidth + 'px');
            $box.css('width',iconImage.naturalWidth + 'px');
            $box.addClass('encp-attachment-previewing');
            var $info = $mediaNode.find(".encp-attachment-info").empty();
            if (item.iconURL) {
              $info.append($('<img />').attr('src',item.iconURL));
            }
            if (item.information) {
              $info.append($('<span />').addClass('encp-attachment-info-detail').text(item.information));
            }
            if (item.title) {
              $info.append($('<span />').addClass('encp-attachment-info-title').text(item.title));
            }
            if (item.canPlay) {
              $box.addClass('encp-attachment-playable');
            }
            ENCPDeferred.
              doNow(function() {
                self.computeImageSize(iconImage,self.maximumImageBounds());
              }).
              then(function() {
                self.recoverScrollPosition();
              }).
              done();
            $icon.off('load');
          });
        }
        var iconURL = item.previewURL ? item.previewURL : item.iconURL;
        if (iconURL) {
          $icon.attr('src', iconURL);
          if ($icon.hasClass('encp-attachment-progress')) {
            $icon.removeClass('encp-attachment-progress');
          }
        }
      });
    },

    isNoteSource: function() {
      return $("html").hasClass("notesource");
    },

    sourceType: function() {
      return $('html').is('.notesource') ? 'notesource' : 'websource';
    },

    getPreviousBoundary: function() {
      var self = this;

      var scrollPosition = self.scrollPosition();
      var viewPortHeight = self.viewPort().height;
      var defaultPreviousPosition = scrollPosition - viewPortHeight + self.DEFAULT_SCROLL_OVERLAP;

      if (!self.isNoteSource()) {
        return defaultPreviousPosition;
      }

      if (this.scrollStack.length > 0 && this.scrollStack[this.scrollStack.length - 1] < scrollPosition) {
        return this.scrollStack.pop();
      }

      var centerOfViewPort = scrollPosition + Math.round(viewPortHeight / 2);

      // Find images that need to scrolled into view
      if (self.visibleImageElements === undefined) {
        self.visibleImageElements = $("body").find("img:visible");
      }
      var imageCandidates = $();
      if (self.visibleImageElements.length > 0) {
        imageCandidates = self.visibleImageElements.filter(function() {
          var element = $(this);
          return (element.offset().top + element.height()) < centerOfViewPort;
        }).reverse();
      }

      // Find text elements that are invisible
      var invisibleTextCandidates = $();
      var $visiblePageBreakElements = $();
      if (self.snappableElements.length > 0) {
        invisibleTextCandidates = self.snappableElements.filter(function() {
          return $(this).isAboveViewPort(scrollPosition);
        });
        $visiblePageBreakElements = self.snappableElements.filter("hr").filter(function() {
          var $element = $(this);
          return $element.isOnScreen(scrollPosition);
        });
      }
      var invisibleTextCandidate = invisibleTextCandidates.last();

      // Find all elements that are on screen
      var upperViewPortElements = $();
      if (self.visibleTextElements.length > 0) {
        upperViewPortElements = self.visibleTextElements.filter(function() {
          var element = $(this);
          return element.isOnScreen(scrollPosition) // element is visible
            && (element.offset().top + element.height() < centerOfViewPort); // element bottom is above the middle
        });
      }

      var candidate = $();
      if ($visiblePageBreakElements.length > 0) {
        candidate = $visiblePageBreakElements;
      } else if (imageCandidates.length > 0 && (invisibleTextCandidate == null || imageCandidates.offset().top > invisibleTextCandidate.offset().top)) {
        // If there's no text element between the image and the current position, take the image
        candidate = imageCandidates;
      } else if (upperViewPortElements.length > 0) {
        // Find element that is the top of the view port
        candidate = upperViewPortElements.first();
        var candidateBottom = candidate.offset().top + $(candidate).height();
        var expectedTop = candidateBottom - viewPortHeight + (candidateBottom - scrollPosition);
        // find invisible element that needs to be revealed
        candidate = $();
        invisibleTextCandidates.each(function() {
          var element = $(this);
          if (element.offset().top >= expectedTop) {
            candidate = element;
            return false;
          }
          return true;
        });
      } else if (invisibleTextCandidate) {
        // Scroll to the next invisible text
        candidate = invisibleTextCandidate;
      }

      var newOffsetTop = 0;
      if (candidate.length > 0) {
        // Scroll to next boundary (image or text)
        var padding = 0;
        if (candidate.is("img")) {
          padding = (viewPortHeight - candidate.height()) / 2;
        } else if (candidate.is("hr")) {
          padding = viewPortHeight;
        } else {
          padding = Math.round(10 * self.zoomFactorInPercent() / 100);
        }
        newOffsetTop = candidate.offset().top - padding;
      } else {
        return defaultPreviousPosition;
      }
      if (newOffsetTop >= scrollPosition + viewPortHeight ||
        (newOffsetTop < scrollPosition - viewPortHeight && !candidate.is("hr"))) {
        // Default scrolling, in case there's no candidate or newOffset isn't valid (pictures too big etc).
        return defaultPreviousPosition;
      }
      if (newOffsetTop < 0) {
        newOffsetTop = 0;
      }

      return newOffsetTop;
    },

    getNextBoundary: function() {
      var self = this;

      var scrollPosition = self.scrollPosition();
      var viewPortHeight = self.viewPort().height;
      var bottomOfViewPort = scrollPosition + viewPortHeight;
      var newOffsetTop = 0;

      if (!self.isNoteSource()) {
        newOffsetTop = bottomOfViewPort - self.DEFAULT_SCROLL_OVERLAP;
        if (newOffsetTop < 0) {
          newOffsetTop = 0;
        }
        return newOffsetTop;
      }

      this.scrollStack.push(Math.max(scrollPosition, 0));

      var centerOfViewPort = bottomOfViewPort - Math.round(viewPortHeight / 2);
      // Find images that need to scrolled into view
      if (self.visibleImageElements === undefined) {
        self.visibleImageElements = $("body").find("img:visible");
      }
      var imageCandidates = $();
      if (self.visibleImageElements.length > 0) {
        imageCandidates = self.visibleImageElements.filter(function() {
          var $element = $(this);
          var elementTop = $element.offset().top;
          return elementTop > centerOfViewPort
            || (elementTop + $element.height() > bottomOfViewPort && elementTop > scrollPosition);
        });
      }

      // Find text elements that are invisible
      var $visiblePageBreakCandidates = $();
      var invisibleTextCandidates = $();
      if (self.snappableElements.length > 0) {
        invisibleTextCandidates = self.snappableElements.filter(function() {
          var $element = $(this);
          return $element.isBelowViewPort(scrollPosition);
        });
        $visiblePageBreakCandidates = self.snappableElements.filter("hr").filter(function() {
          var $element = $(this);
          return $element.offset().top > scrollPosition + 1 && $element.offset().top <= bottomOfViewPort + 1;
        });
      }

      // Find all elements that are on screen
      var lowerViewPortElements = $();
      if (self.visibleTextElements.length > 0) {
        lowerViewPortElements = self.visibleTextElements.filter(function() {
          var element = $(this);
          return element.isOnScreen(scrollPosition) // element is visible
            && element.offset().top > centerOfViewPort; // element is below middle of view port
        });
      }

      var candidate = $();
      if ($visiblePageBreakCandidates.length) {
        candidate = $visiblePageBreakCandidates;
      } else  if (imageCandidates.length > 0 &&
        (invisibleTextCandidates.length == 0 || imageCandidates.offset().top < invisibleTextCandidates.offset().top)) {
        // If there's no text element between the image and the current position, take the image
        candidate = imageCandidates;
      } else if (lowerViewPortElements.length > 0) {
        // Scroll the last visible element in viewport to the top of the viewport
        candidate = lowerViewPortElements.last();
      } else if (invisibleTextCandidates.length > 0) {
        // Scroll to the next invisible text
        candidate = invisibleTextCandidates.first();
      }

      if (candidate.length > 0) {
        // Scroll to next boundary (image or text)
        var padding = 0;
        if (candidate.is("img")) {
          padding = Math.round((viewPortHeight - candidate.height()) / 2);
        } else if (candidate.is("hr")) {
          padding = 0;
        } else {
          padding = Math.round(10 * self.zoomFactorInPercent() / 100);
        }
        newOffsetTop = candidate.offset().top - padding;
      } else {
        // Scroll to end of document
        newOffsetTop = $(document).height();
      }
      newOffsetTop = Math.round(newOffsetTop);
      if (newOffsetTop <= scrollPosition ||
        (newOffsetTop > bottomOfViewPort && !candidate.is("hr"))) {
        // Default scrolling, in case there's no candidate or newOffset isn't valid (pictures too big etc).
        newOffsetTop = bottomOfViewPort - self.DEFAULT_SCROLL_OVERLAP;
      }
      if (newOffsetTop < 0) {
        newOffsetTop = 0;
      }

      return newOffsetTop;
    },

    scrollToPreviousBoundary: function() {
      var self = this;

      // Check if displayed contents are the top of the note, switch to previous note if needed.
      if (self.scrollPosition() <= 0) {
        // Do we need to call any platform function here?
        return true;
      }

      var newOffsetTop = self.getPreviousBoundary();
      self.scrollToTopOffset(newOffsetTop);
      return false;
    },

    scrollToNextBoundary: function() {
      var self = this;

      // Check if displayed contents are reached the end of the note, switch to next note if needed.
      if (self.didReachEndOfNote(true)) {
        return true;
      }

      var newOffsetTop = self.getNextBoundary();
      self.scrollToTopOffset(newOffsetTop);
      return false;
    },

    focusNextOrCurrentNoteLink: function(currentNoteLink) {
      var self = this;
      var noteLinks = self.validNoteLinks();
      var index = noteLinks.index($(".encp-note-link[href='" + currentNoteLink + "']"));
      if (index >= 0) {
        if (index < noteLinks.length - 1) {
          index += 1;
        }
        $(noteLinks.get(index)).focus();
      } else {
        self.log("[WARN] didn't find note link!");
      }
    },

    scrollPosition: function() {
      return (this.targetScrollPosition < 0) ? $(window).scrollTop() : this.targetScrollPosition;
    },

    scrollToTopOffset: function(topOffset) {
      var $window;
      $window = $(window);
      topOffset = Math.max(topOffset, 0);
      topOffset = Math.min(topOffset, $(document).height() - $(window).height());
      topOffset = Math.round(topOffset);
      this.targetScrollPosition = topOffset;
      this.lastScrollPosition = $window.scrollTop();
      this.idleScrollCount = 0;
      this.isScrolling = true;
      $window.scrollTop(topOffset);
      this.monitorScrollPosition();
    },

    monitorScrollPosition: function () {
      var $window;
      var currentPosition;

      if (this.targetScrollPosition == -1) return;
      $window = $(window);
      currentPosition = $window.scrollTop();

      if (currentPosition == this.lastScrollPosition) this.idleScrollCount++;

      if (currentPosition == this.targetScrollPosition || this.idleScrollCount > 2) {
        this.targetScrollPosition = -1;
        this.lastScrollPosition = -1;
        this.idleScrollCount = 0;
        this.isScrolling = false;
      } else {
        this.lastScrollPosition = currentPosition;
        setTimeout(this.monitorScrollPosition.bind(this), 16);
      }
    },

    scrollToMarkerID: function(markerID) {
      var self = this;
      var markerElement = $('#' + markerID);
      if (!markerElement.length) {
        return;
      }

      var viewPortTop = self.scrollPosition();
      var viewPortHeight = self.viewPort().height;
      var viewPortMarginHeight = 0.2 * viewPortHeight;
      var topCorrection = (markerElement.offset().top) - (viewPortTop + viewPortMarginHeight);
      var bottomCorrection = (markerElement.offset().top + markerElement.height()) - (viewPortTop + viewPortHeight - viewPortMarginHeight);
      var scrollCorrection = (topCorrection < 0) ? topCorrection : ((bottomCorrection > 0) ? bottomCorrection : 0);
      if (scrollCorrection) {
        var zoomString = $("html").css("zoom");
        if (zoomString) {
          scrollCorrection *= parseFloat(zoomString);
        }
        self.scrollToTopOffset(viewPortTop + scrollCorrection);
      }
    },

    isEndOfNote: function() {
      var self = this;
      return (self.targetScrollPosition < 0 && self.didReachEndOfNoteCount > 0);
    },

    isBeginningOfNote: function() {
      var self = this;
      return (self.targetScrollPosition < 0 && $(window).scrollTop() == 0);
    },

    parseMediaAndNotify: function() {
      window.encpPlatform.didParseMediaItems(this.getMediaItems());
    },

    deselectLinkIfNeeded: function() {
      var self = this;
      if (self.focused) {
        self.focused.blur();
        self.focused = null;
      }
    },

    switchStyle: function(nameOfStyle) {
      var self = this;
      $.each(self.styleSheets, function() {
        var style = this;
        style.disabled = ($(style).attr('title') != nameOfStyle);
        $(style).attr('rel',(style.disabled ? 'alternate stylesheet' : 'stylesheet'));
      });
    },

    enableStyles: function(stylesToBeEnabled) {
      var self = this;
      $.each(self.styleSheets, function() {
        var style = this;
        var disabled = (stylesToBeEnabled.indexOf($(style).attr('title')) == -1);
        if (style.disabled == disabled) {
          return;
        }
        style.disabled = disabled;
        $(style).attr('rel',(disabled ? 'alternate stylesheet' : 'stylesheet'));
      });
    },

    availableStyles: function() {
      var self = this;
      var results = [];
      $.each(self.styleSheets, function() {
        results.push($(this).attr('title'));
      });
      return JSON.stringify(results);
    },

    applyStyle: function(style) {
      var self = this;
      var $styleElement = $('#encp-presentation-style');
      if ($styleElement.length == 0) {
        $styleElement = $('<style id="encp-presentation-style" type="text/css"></style>');
        $('head').append($styleElement);
      }
      var $body = $('body');
      style += "\nbody { display: block!important; }";
      if (style != $styleElement.text()) {
        ENCPDeferred
          .doNow(function() {
            // Applying style - step 1 : hide a body to reduce occurrence of reflow/repaint.
            $body.css('display','none');
          })
          .then(function() {
            // Applying style - step 2 : apply actual style.
            $styleElement.text(style);
          })
          .waitUntil(function() {
            // Applying style - step 3 : making sure body visible again.
            return ($body.css('display') != 'none');
          })
          .then(function() {
            // Applying style - step 4 : notify host completion.
            window.encpPlatform.didFinishStyleUpdate();
          })
          .done();
      }
    },

    computeImageSize: function(image, screen) {
      if (!image.naturalWidth || !image.naturalHeight) {
        return;
      }
      var $image = $(image);
      var $container = $image.closest('.encp-attachment-previewing');
      // Style for image may affect its parent size.
      // So we need to reset beforehand.
      $image.css('width','');
      $image.css('height','');
      if ($container) {
        $container.css('width','');
      }
      var width = screen.w;
      var height = image.naturalHeight;
      if ($image.parent().length > 0) {
        var parentWidth = $image.parent().width();
        if (parentWidth > 0) {
          width = parentWidth;
        }
      }
      if (width > image.naturalWidth) {
        // Image width is smaller than screen width, so we can show
        // the image with the original size.
        width = image.naturalWidth;
      } else {
        // Image may shrink to fit the screen, so we should keep
        // the aspect ratio of the image.
        height = image.naturalHeight * width / image.naturalWidth;
      }
      // Make sure image fits the screen.
      if (height > screen.h) {
        width *= screen.h / height;
        height = screen.h;
      }
      $image.css('width',width + 'px');
      $image.css('height',height + 'px');
      if ($container) {
        $container.css('width',width + 'px');
      }
    },

    maximumImageBounds: function() {
      var self = this;
      var screenWidth = self.viewPort().width;
      var screenHeight = self.viewPort().height * 0.9;
      var zoomLevel = parseFloat($('html').css("zoom"));
      if (zoomLevel > 0) {
        // Screen size with considering zoom level.
        screenWidth /= zoomLevel;
        screenHeight /= zoomLevel;
      }
      return {w: screenWidth, h: screenHeight};
    },

    adjustImageSize: function() {
      var self = this;
      var images = window.encpPlatform.imagesToBeAdjusted(self.sourceType());
      if (!images || images.length == 0) {
        return;
      }
      var imageBounds = self.maximumImageBounds();
      ENCPDeferred.
        doNow(function() {
          images.each(function() {
            self.computeImageSize(this,imageBounds);
          });
        }).
        then(function() {
          self.recoverScrollPosition();
        }).
        done();
    },

    getFunctionName: function (funcObj) {
      var self = this;
      var functionName = undefined;
      $.each(Object.getOwnPropertyNames(ENCPPresenter.prototype), function() {
        var propertyName = this.toString();
        if (ENCPPresenter.prototype[propertyName] == funcObj) {
          functionName = 'ENCPPresenter.' + propertyName;
          return false;
        }
        return true;
      });
      return functionName;
    },

    log: function(logText) {
      var self = this;
      var caller = self.log.caller.name;
      if (caller == '') {
        caller = self.getFunctionName(self.log.caller);
      }
      console.log('[' + caller + '] ' + logText);
      window.encpPlatform.showLog(logText,'[' + caller + ']');
    },

    viewPort: function() {
      return window.encpPlatform.viewPort();
    },

    destroy: function() {
        var self = this;
        window.clearInterval(self.updateHeightInterval);
        this.scrollMarker = null;
    }

  };

  /**
   * Global exports below
   * setupNotePresenter - Creates the window.encpPresenter object and configures event listeners
   */
  window.setupNotePresenter = function() {
    window.encpPresenter = new ENCPPresenter();
    window.encpPresenter.setup();
    return window.encpPresenter;
  }

})(window.$encpJquery);
