var __slice = [].slice;
var __indexOf = [].indexOf || function (item) {
    for (var i = 0, l = this.length; i < l; i++) {
      if (i in this && this[i] === item) return i;
    }
    return -1;
  };

Poltergeist.WebPage = (function () {
  var command, delegate, commandFunctionBind, delegateFunctionBind, i, j, commandsLength, delegatesRefLength, commandsRef, delegatesRef,
    _this = this;

  //Native or not webpage callbacks
  WebPage.CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized', 'onLoadStarted', 'onResourceRequested',
    'onResourceReceived', 'onError', 'onNavigationRequested', 'onUrlChanged', 'onPageCreated', 'onClosing'];

  // Delegates the execution to the phantomjs page native functions but directly available in the WebPage object
  WebPage.DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render', 'renderBase64', 'goBack', 'goForward', 'reload'];

  //Commands to execute on behalf of the browser but on the current page
  WebPage.COMMANDS = ['currentUrl', 'find', 'nodeCall', 'documentSize', 'beforeUpload', 'afterUpload', 'clearLocalStorage'];

  WebPage.EXTENSIONS = [];

  function WebPage(nativeWebPage, browser) {
    var callback, i, callBacksLength, callBacksRef;

    //Lets create the native phantomjs webpage
    if (nativeWebPage === null || typeof nativeWebPage == "undefined") {
      this._native = require('webpage').create();
    } else {
      this._native = nativeWebPage;
    }

    this.browser = browser;
    if (this.browser) {
      this.browser.registerPage(this);  // Make the browser aware of opened windows/frames
    }

    this.id = 0;
    this.source = null;
    this.closed = false;
    this.state = 'default';
    this.urlBlacklist = [];
    this.frames = [];
    this.errors = [];
    this._networkTraffic = {};
    this._tempHeaders = {};
    this._blockedUrls = [];

    callBacksRef = WebPage.CALLBACKS;
    for (i = 0, callBacksLength = callBacksRef.length; i < callBacksLength; i++) {
      callback = callBacksRef[i];
      this.bindCallback(callback);
    }
  }

  //Bind the commands we can run from the browser to the current page
  commandsRef = WebPage.COMMANDS;
  commandFunctionBind = function (command) {
    return WebPage.prototype[command] = function () {
      var args;
      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      return this.runCommand(command, args);
    };
  };
  for (i = 0, commandsLength = commandsRef.length; i < commandsLength; i++) {
    command = commandsRef[i];
    commandFunctionBind(command);
  }

  //Delegates bind applications
  delegatesRef = WebPage.DELEGATES;
  delegateFunctionBind = function (delegate) {
    return WebPage.prototype[delegate] = function () {
      return this._native[delegate].apply(this._native, arguments);
    };
  };
  for (j = 0, delegatesRefLength = delegatesRef.length; j < delegatesRefLength; j++) {
    delegate = delegatesRef[j];
    delegateFunctionBind(delegate);
  }

  /**
   * This callback is invoked after the web page is created but before a URL is loaded.
   * The callback may be used to change global objects.
   * @return {*}
   */
  WebPage.prototype.onInitializedNative = function () {
    this.id += 1;
    this.source = null;
    this.injectAgent();
    this.removeTempHeaders();
    return this.setScrollPosition({
      left: 0,
      top: 0
    });
  };

  /**
   * This callback is invoked when the WebPage object is being closed,
   * either via page.close in the PhantomJS outer space or via window.close in the page's client-side.
   * @return {boolean}
   */
  WebPage.prototype.onClosingNative = function () {
    this.closed = true;
    if (this.browser) {
      this.browser.unregisterPage(this);  // Make the browser aware of closed windows/frames
    }
    return true;
  };

  /**
   * This callback is invoked when there is a JavaScript console message on the web page.
   * The callback may accept up to three arguments: the string for the message, the line number, and the source identifier.
   * @param message
   * @param line
   * @param sourceId
   * @return {boolean}
   */
  WebPage.prototype.onConsoleMessageNative = function (message, line, sourceId) {
    if (message === '__DOMContentLoaded') {
      this.source = this._native.content;
      return false;
    }
    console.log(message);
    return true;
  };

  /**
   * This callback is invoked when the page starts the loading. There is no argument passed to the callback.
   * @return {number}
   */
  WebPage.prototype.onLoadStartedNative = function () {
    this.state = 'loading';
    return this.requestId = this.lastRequestId;
  };

  /**
   * This callback is invoked when the page finishes the loading.
   * It may accept a single argument indicating the page's status: 'success' if no network errors occurred, otherwise 'fail'.
   * @param status
   * @return {string}
   */
  WebPage.prototype.onLoadFinishedNative = function (status) {
    this.status = status;
    this.state = 'default';

    if (this.source === null || typeof this.source == "undefined") {
      this.source = this._native.content;
    } else {
      this.source = this._native.content;
    }

    return this.source;
  };

  /**
   * Make the browser aware of new windows/frames
   *
   * @param {webPage} newPage
   */
  WebPage.prototype.onPageCreatedNative = function (newPage) {
    new WebPage(newPage, this.browser);
  };

  /**
   * This callback is invoked when there is a JavaScript execution error.
   * It is a good way to catch problems when evaluating a script in the web page context.
   * The arguments passed to the callback are the error message and the stack trace [as an Array].
   * @param message
   * @param stack
   * @return {Number}
   */
  WebPage.prototype.onErrorNative = function (message, stack) {
    var stackString;

    stackString = message;
    stack.forEach(function (frame) {
      stackString += "\n";
      stackString += "    at " + frame.file + ":" + frame.line;
      if (frame["function"] && frame["function"] !== '') {
        return stackString += " in " + frame["function"];
      }
    });

    return this.errors.push({
      message: message,
      stack: stackString
    });
  };

  /**
   * This callback is invoked when the page requests a resource.
   * The first argument to the callback is the requestData metadata object.
   * The second argument is the networkRequest object itself.
   * @param requestData
   * @param networkRequest
   * @return {*}
   */
  WebPage.prototype.onResourceRequestedNative = function (requestData, networkRequest) {
    var abort;

    abort = this.urlBlacklist.some(function (blacklistedUrl) {
      return requestData.url.indexOf(blacklistedUrl) !== -1;
    });

    if (abort) {
      if (this._blockedUrls.indexOf(requestData.url) === -1) {
        this._blockedUrls.push(requestData.url);
      }
      //TODO: check this, as it raises onResourceError
      return networkRequest.abort();
    }

    this.lastRequestId = requestData.id;
    if (requestData.url === this.redirectURL) {
      this.redirectURL = null;
      this.requestId = requestData.id;
    }

    return this._networkTraffic[requestData.id] = {
      request: requestData,
      responseParts: []
    };
  };

  /**
   * This callback is invoked when a resource requested by the page is received.
   * The only argument to the callback is the response metadata object.
   * @param response
   * @return {*}
   */
  WebPage.prototype.onResourceReceivedNative = function (response) {
    var networkTrafficElement;

    if ((networkTrafficElement = this._networkTraffic[response.id]) != null) {
      networkTrafficElement.responseParts.push(response);
    }

    if (this.requestId === response.id) {
      if (response.redirectURL) {
        return this.redirectURL = response.redirectURL;
      }

      this.statusCode = response.status;
      return this._responseHeaders = response.headers;
    }
  };

  /**
   * Inject the poltergeist agent into the webpage
   * @return {Array}
   */
  WebPage.prototype.injectAgent = function () {
    var extension, isAgentInjected, i, extensionsRefLength, extensionsRef, injectionResults;

    isAgentInjected = this["native"]().evaluate(function () {
      return typeof window.__poltergeist;
    });

    if (isAgentInjected === "undefined") {
      this["native"]().injectJs("" + phantom.libraryPath + "/agent.js");
      extensionsRef = WebPage.EXTENSIONS;
      injectionResults = [];
      for (i = 0, extensionsRefLength = extensionsRef.length; i < extensionsRefLength; i++) {
        extension = extensionsRef[i];
        injectionResults.push(this["native"]().injectJs(extension));
      }
      return injectionResults;
    }
  };

  /**
   * Injects a Javascript file extension into the
   * @param file
   * @return {*}
   */
  WebPage.prototype.injectExtension = function (file) {
    //TODO: add error control, for example, check if file already in the extensions array, check if the file exists, etc.
    WebPage.EXTENSIONS.push(file);
    return this["native"]().injectJs(file);
  };

  /**
   * Returns the native phantomjs webpage object
   * @return {*}
   */
  WebPage.prototype["native"] = function () {
    if (this.closed) {
      throw new Poltergeist.NoSuchWindowError;
    }

    return this._native;
  };

  /**
   * Returns the current page window name
   * @return {*}
   */
  WebPage.prototype.windowName = function () {
    return this["native"]().windowName;
  };

  /**
   * Returns the keyCode of a given key as set in the phantomjs values
   * @param name
   * @return {number}
   */
  WebPage.prototype.keyCode = function (name) {
    return this["native"]().event.key[name];
  };

  /**
   * Waits for the page to reach a certain state
   * @param state
   * @param callback
   * @return {*}
   */
  WebPage.prototype.waitState = function (state, callback) {
    var self = this;
    if (this.state === state) {
      return callback.call();
    } else {
      return setTimeout((function () {
        return self.waitState(state, callback);
      }), 100);
    }
  };

  /**
   * Sets the browser header related to basic authentication protocol
   * @param user
   * @param password
   * @return {boolean}
   */
  WebPage.prototype.setHttpAuth = function (user, password) {
    var allHeaders = this.getCustomHeaders();

    if (user === false || password === false) {
      if (allHeaders.hasOwnProperty("Authorization")) {
        delete allHeaders["Authorization"];
      }
      this.setCustomHeaders(allHeaders);
      return true;
    }

    var userName = user || "";
    var userPassword = password || "";

    allHeaders["Authorization"] = "Basic " + btoa(userName + ":" + userPassword);
    this.setCustomHeaders(allHeaders);
    return true;
  };

  /**
   * Returns all the network traffic associated to the rendering of this page
   * @return {{}}
   */
  WebPage.prototype.networkTraffic = function () {
    return this._networkTraffic;
  };

  /**
   * Clears all the recorded network traffic related to the current page
   * @return {{}}
   */
  WebPage.prototype.clearNetworkTraffic = function () {
    return this._networkTraffic = {};
  };

  /**
   * Returns the blocked urls that the page will not load
   * @return {Array}
   */
  WebPage.prototype.blockedUrls = function () {
    return this._blockedUrls;
  };

  /**
   * Clean all the urls that should not be loaded
   * @return {Array}
   */
  WebPage.prototype.clearBlockedUrls = function () {
    return this._blockedUrls = [];
  };

  /**
   * This property stores the content of the web page's currently active frame
   * (which may or may not be the main frame), enclosed in an HTML/XML element.
   * @return {string}
   */
  WebPage.prototype.content = function () {
    return this["native"]().frameContent;
  };

  /**
   * Returns the current active frame title
   * @return {string}
   */
  WebPage.prototype.title = function () {
    return this["native"]().frameTitle;
  };

  /**
   * Returns if possible the frame url of the frame given by name
   * @param frameName
   * @return {string}
   */
  WebPage.prototype.frameUrl = function (frameName) {
    var query;

    query = function (frameName) {
      var iframeReference;
      if ((iframeReference = document.querySelector("iframe[name='" + frameName + "']")) != null) {
        return iframeReference.src;
      }
      return void 0;
    };

    return this.evaluate(query, frameName);
  };

  /**
   * Remove the errors caught on the page
   * @return {Array}
   */
  WebPage.prototype.clearErrors = function () {
    return this.errors = [];
  };

  /**
   * Returns the response headers associated to this page
   * @return {{}}
   */
  WebPage.prototype.responseHeaders = function () {
    var headers;
    headers = {};
    this._responseHeaders.forEach(function (item) {
      return headers[item.name] = item.value;
    });
    return headers;
  };

  /**
   * Get Cookies visible to the current URL (though, for setting, use of page.addCookie is preferred).
   * This array will be pre-populated by any existing Cookie data visible to this URL that is stored in the CookieJar, if any.
   * @return {*}
   */
  WebPage.prototype.cookies = function () {
    return this["native"]().cookies;
  };

  /**
   * Delete any Cookies visible to the current URL with a 'name' property matching cookieName.
   * Returns true if successfully deleted, otherwise false.
   * @param name
   * @return {*}
   */
  WebPage.prototype.deleteCookie = function (name) {
    return this["native"]().deleteCookie(name);
  };

  /**
   * This property gets the size of the viewport for the layout process.
   * @return {*}
   */
  WebPage.prototype.viewportSize = function () {
    return this["native"]().viewportSize;
  };

  /**
   * This property sets the size of the viewport for the layout process.
   * @param size
   * @return {*}
   */
  WebPage.prototype.setViewportSize = function (size) {
    return this["native"]().viewportSize = size;
  };

  /**
   * This property specifies the scaling factor for the page.render and page.renderBase64 functions.
   * @param zoomFactor
   * @return {*}
   */
  WebPage.prototype.setZoomFactor = function (zoomFactor) {
    return this["native"]().zoomFactor = zoomFactor;
  };

  /**
   * This property defines the size of the web page when rendered as a PDF.
   * See: http://phantomjs.org/api/webpage/property/paper-size.html
   * @param size
   * @return {*}
   */
  WebPage.prototype.setPaperSize = function (size) {
    return this["native"]().paperSize = size;
  };

  /**
   * This property gets the scroll position of the web page.
   * @return {*}
   */
  WebPage.prototype.scrollPosition = function () {
    return this["native"]().scrollPosition;
  };

  /**
   * This property defines the scroll position of the web page.
   * @param pos
   * @return {*}
   */
  WebPage.prototype.setScrollPosition = function (pos) {
    return this["native"]().scrollPosition = pos;
  };


  /**
   * This property defines the rectangular area of the web page to be rasterized when page.render is invoked.
   * If no clipping rectangle is set, page.render will process the entire web page.
   * @return {*}
   */
  WebPage.prototype.clipRect = function () {
    return this["native"]().clipRect;
  };

  /**
   * This property defines the rectangular area of the web page to be rasterized when page.render is invoked.
   * If no clipping rectangle is set, page.render will process the entire web page.
   * @param rect
   * @return {*}
   */
  WebPage.prototype.setClipRect = function (rect) {
    return this["native"]().clipRect = rect;
  };

  /**
   * Returns the size of an element given by a selector and its position relative to the viewport.
   * @param selector
   * @return {Object}
   */
  WebPage.prototype.elementBounds = function (selector) {
    return this["native"]().evaluate(function (selector) {
      return document.querySelector(selector).getBoundingClientRect();
    }, selector);
  };

  /**
   * Defines the user agent sent to server when the web page requests resources.
   * @param userAgent
   * @return {*}
   */
  WebPage.prototype.setUserAgent = function (userAgent) {
    return this["native"]().settings.userAgent = userAgent;
  };

  /**
   * Returns the additional HTTP request headers that will be sent to the server for EVERY request.
   * @return {{}}
   */
  WebPage.prototype.getCustomHeaders = function () {
    return this["native"]().customHeaders;
  };

  /**
   * Gets the additional HTTP request headers that will be sent to the server for EVERY request.
   * @param headers
   * @return {*}
   */
  WebPage.prototype.setCustomHeaders = function (headers) {
    return this["native"]().customHeaders = headers;
  };

  /**
   * Adds a one time only request header, after being used it will be deleted
   * @param header
   * @return {Array}
   */
  WebPage.prototype.addTempHeader = function (header) {
    var name, value, tempHeaderResult;
    tempHeaderResult = [];
    for (name in header) {
      if (header.hasOwnProperty(name)) {
        value = header[name];
        tempHeaderResult.push(this._tempHeaders[name] = value);
      }
    }
    return tempHeaderResult;
  };

  /**
   * Remove the temporary headers we have set via addTempHeader
   * @return {*}
   */
  WebPage.prototype.removeTempHeaders = function () {
    var allHeaders, name, value, tempHeadersRef;
    allHeaders = this.getCustomHeaders();
    tempHeadersRef = this._tempHeaders;
    for (name in tempHeadersRef) {
      if (tempHeadersRef.hasOwnProperty(name)) {
        value = tempHeadersRef[name];
        delete allHeaders[name];
      }
    }

    return this.setCustomHeaders(allHeaders);
  };

  /**
   * If possible switch to the frame given by name
   * @param name
   * @return {boolean}
   */
  WebPage.prototype.pushFrame = function (name) {
    if (this["native"]().switchToFrame(name)) {
      this.frames.push(name);
      return true;
    }
    return false;
  };

  /**
   * Switch to parent frame, use with caution:
   * popFrame assumes you are in frame, pop frame not being in a frame
   * leaves unexpected behaviour
   * @return {*}
   */
  WebPage.prototype.popFrame = function () {
    //TODO: add some error control here, some way to check we are in a frame or not
    this.frames.pop();
    return this["native"]().switchToParentFrame();
  };

  /**
   * Returns the webpage dimensions
   * @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}}
   */
  WebPage.prototype.dimensions = function () {
    var scroll, viewport;
    scroll = this.scrollPosition();
    viewport = this.viewportSize();
    return {
      top: scroll.top,
      bottom: scroll.top + viewport.height,
      left: scroll.left,
      right: scroll.left + viewport.width,
      viewport: viewport,
      document: this.documentSize()
    };
  };

  /**
   * Returns webpage dimensions that are valid
   * @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}}
   */
  WebPage.prototype.validatedDimensions = function () {
    var dimensions, documentDimensions;

    dimensions = this.dimensions();
    documentDimensions = dimensions.document;

    if (dimensions.right > documentDimensions.width) {
      dimensions.left = Math.max(0, dimensions.left - (dimensions.right - documentDimensions.width));
      dimensions.right = documentDimensions.width;
    }

    if (dimensions.bottom > documentDimensions.height) {
      dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - documentDimensions.height));
      dimensions.bottom = documentDimensions.height;
    }

    this.setScrollPosition({
      left: dimensions.left,
      top: dimensions.top
    });

    return dimensions;
  };

  /**
   * Returns a Poltergeist.Node given by an id
   * @param id
   * @return {Poltergeist.Node}
   */
  WebPage.prototype.get = function (id) {
    return new Poltergeist.Node(this, id);
  };

  /**
   * Executes a phantomjs mouse event, for more info check: http://phantomjs.org/api/webpage/method/send-event.html
   * @param name
   * @param x
   * @param y
   * @param button
   * @return {*}
   */
  WebPage.prototype.mouseEvent = function (name, x, y, button) {
    if (button == null) {
      button = 'left';
    }
    this.sendEvent('mousemove', x, y);
    return this.sendEvent(name, x, y, button);
  };

  /**
   * Evaluates a javascript and returns the evaluation of such script
   * @return {*}
   */
  WebPage.prototype.evaluate = function () {
    var args, fn;
    fn = arguments[0];
    args = [];

    if (2 <= arguments.length) {
      args = __slice.call(arguments, 1);
    }

    this.injectAgent();
    return JSON.parse(this.sanitize(this["native"]().evaluate("function() { return PoltergeistAgent.stringify(" + (this.stringifyCall(fn, args)) + ") }")));
  };

  /**
   * Does some string sanitation prior parsing
   * @param potentialString
   * @return {*}
   */
  WebPage.prototype.sanitize = function (potentialString) {
    if (typeof potentialString === "string") {
      return potentialString.replace("\n", "\\n").replace("\r", "\\r");
    }

    return potentialString;
  };

  /**
   * Executes a script into the current page scope
   * @param script
   * @return {*}
   */
  WebPage.prototype.executeScript = function (script) {
    return this["native"]().evaluateJavaScript(script);
  };

  /**
   * Executes a script via phantomjs evaluation
   * @return {*}
   */
  WebPage.prototype.execute = function () {
    var args, fn;

    fn = arguments[0];
    args = [];

    if (2 <= arguments.length) {
      args = __slice.call(arguments, 1);
    }

    return this["native"]().evaluate("function() { " + (this.stringifyCall(fn, args)) + " }");
  };

  /**
   * Helper methods to do script evaluation and execution
   * @param fn
   * @param args
   * @return {string}
   */
  WebPage.prototype.stringifyCall = function (fn, args) {
    if (args.length === 0) {
      return "(" + (fn.toString()) + ")()";
    }

    return "(" + (fn.toString()) + ").apply(this, JSON.parse(" + (JSON.stringify(JSON.stringify(args))) + "))";
  };

  /**
   * Binds callbacks to their respective Native implementations
   * @param name
   * @return {Function}
   */
  WebPage.prototype.bindCallback = function (name) {
    var self;
    self = this;

    return this["native"]()[name] = function () {
      var result;
      if (self[name + 'Native'] != null) {
        result = self[name + 'Native'].apply(self, arguments);
      }
      if (result !== false && (self[name] != null)) {
        return self[name].apply(self, arguments);
      }
    };
  };

  /**
   * Runs a command delegating to the PoltergeistAgent
   * @param name
   * @param args
   * @return {*}
   */
  WebPage.prototype.runCommand = function (name, args) {
    var method, result, selector;

    result = this.evaluate(function (name, args) {
      return window.__poltergeist.externalCall(name, args);
    }, name, args);

    if (result !== null) {
      if (result.error != null) {
        switch (result.error.message) {
          case 'PoltergeistAgent.ObsoleteNode':
            throw new Poltergeist.ObsoleteNode;
            break;
          case 'PoltergeistAgent.InvalidSelector':
            method = args[0];
            selector = args[1];
            throw new Poltergeist.InvalidSelector(method, selector);
            break;
          default:
            throw new Poltergeist.BrowserError(result.error.message, result.error.stack);
        }
      } else {
        return result.value;
      }
    }
  };

  /**
   * Tells if we can go back or not
   * @return {boolean}
   */
  WebPage.prototype.canGoBack = function () {
    return this["native"]().canGoBack;
  };

  /**
   * Tells if we can go forward or not in the browser history
   * @return {boolean}
   */
  WebPage.prototype.canGoForward = function () {
    return this["native"]().canGoForward;
  };

  return WebPage;

}).call(this);
