/*
This file is part of the GhostDriver by Ivan De Marino <http://ivandemarino.me>.

Copyright (c) 2014, Ivan De Marino <http://ivandemarino.me>
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

var ghostdriver = ghostdriver || {};

ghostdriver.WebElementLocator = function(session) {
    // private:
    const
    _supportedStrategies = [
        "class name", "className",              //< Returns an element whose class name contains the search value; compound class names are not permitted.
        "css", "css selector",                  //< Returns an element matching a CSS selector.
        "id",                                   //< Returns an element whose ID attribute matches the search value.
        "name",                                 //< Returns an element whose NAME attribute matches the search value.
        "link text", "linkText",                //< Returns an anchor element whose visible text matches the search value.
        "partial link text", "partialLinkText", //< Returns an anchor element whose visible text partially matches the search value.
        "tag name", "tagName",                  //< Returns an element whose tag name matches the search value.
        "xpath"                                 //< Returns an element matching an XPath expression.
    ];

    var
    _session = session,
    _errors = require("./errors.js"),
    _log = ghostdriver.logger.create("WebElementLocator"),

    _find = function(what, locator, rootElement) {
        var currWindow = _session.getCurrentWindow(),
            findRes,
            findAtom = require("./webdriver_atoms.js").get(
                "find_" +
                (what.indexOf("element") >= 0 ? what : "element")), //< normalize
            errorMsg;

        if (currWindow !== null &&
            locator && typeof(locator) === "object" &&
            locator.using && locator.value &&         //< if well-formed input
            _supportedStrategies.indexOf(locator.using) >= 0) {  //< and if strategy is recognized

            _log.debug("_find.locator", JSON.stringify(locator));

            // Ensure "rootElement" is valid, otherwise undefine-it
            if (!rootElement || typeof(rootElement) !== "object" || !rootElement["ELEMENT"]) {
                rootElement = undefined;
            }

            // Use Atom "find_result" to search for element in the page
            findRes = currWindow.evaluate(
                findAtom,
                locator.using,
                locator.value,
                rootElement);

            // De-serialise the result of the Atom execution
            try {
                return JSON.parse(findRes);
            } catch (e) {
                errorMsg = JSON.stringify(locator);
                _log.error("_find.locator.error", errorMsg);
                return {
                    "status"    : _errors.FAILED_CMD_STATUS_CODES[_errors.FAILED_CMD_STATUS.UNKNOWN_COMMAND],
                    "value"     : errorMsg
                };
            }
        }

        // Window was not found
        return {
            "status"    : _errors.FAILED_CMD_STATUS_CODES[_errors.FAILED_CMD_STATUS.NO_SUCH_WINDOW],
            "value"     : "No such window"
        };
    },

    _locateElement = function(locator, rootElement) {
        var findElementRes = _find("element", locator, rootElement);

        _log.debug("_locateElement.locator", JSON.stringify(locator));
        _log.debug("_locateElement.findElementResult", JSON.stringify(findElementRes));

        // To check if element was found, the following must happen:
        // 1. "findElementRes" result object must be valid
        // 2. property "status" is found and is {Number}
        if (findElementRes !== null && typeof(findElementRes) === "object" &&
            findElementRes.hasOwnProperty("status") && typeof(findElementRes.status) === "number") {
            // If the atom succeeds, but returns a null value, the element was not found.
            if (findElementRes.status === 0 && findElementRes.value === null) {
                findElementRes.status = _errors.FAILED_CMD_STATUS_CODES[
                    _errors.FAILED_CMD_STATUS.NO_SUCH_ELEMENT
                ];
                findElementRes.value = {
                    "message": "Unable to find element with " +
                        locator.using + " '" +
                        locator.value + "'"
                };
            }
            return findElementRes;
        }

        // Not found
        return {
            "status"    : _errors.FAILED_CMD_STATUS_CODES[_errors.FAILED_CMD_STATUS.NO_SUCH_ELEMENT],
            "value"     : "No Such Element found"
        };
    },

    _locateElements = function(locator, rootElement) {
        var findElementsRes = _find("elements", locator, rootElement),
            elements = [],
            i, ilen;

        _log.debug("_locateElements.locator", JSON.stringify(locator));
        _log.debug("_locateElements.findElementsResult", JSON.stringify(findElementsRes));

        // To check if something was found, the following must happen:
        // 1. "findElementsRes" result object must be valid
        // 2. property "status" is found and is {Number}
        // 3. property "value" is found and is and {Object}
        if (findElementsRes !== null && typeof(findElementsRes) === "object" &&
            findElementsRes.hasOwnProperty("status") && typeof(findElementsRes.status) === "number" &&
            findElementsRes.hasOwnProperty("value") && findElementsRes.value !== null && typeof(findElementsRes.value) === "object") {
            return findElementsRes;
        }

        // Not found
        return {
            "status"    : _errors.FAILED_CMD_STATUS_CODES[_errors.FAILED_CMD_STATUS.NO_SUCH_ELEMENT],
            "value"     : "No Such Elements found"
        };
    },

    _locateActiveElement = function() {
        var currWindow = _session.getCurrentWindow(),
            activeElementRes;

        if (currWindow !== null) {
            activeElementRes = currWindow.evaluate(
                    require("./webdriver_atoms.js").get("active_element"));

            // De-serialise the result of the Atom execution
            try {
                activeElementRes = JSON.parse(activeElementRes);
            } catch (e) {
                return {
                    "status"    : _errors.FAILED_CMD_STATUS_CODES[_errors.FAILED_CMD_STATUS.NO_SUCH_ELEMENT],
                    "value"     : "No Active Element found"
                };
            }

            // If found
            if (typeof(activeElementRes.status) !== "undefined") {
                return activeElementRes;
            }
        }

        return {
            "status"    : _errors.FAILED_CMD_STATUS_CODES[_errors.FAILED_CMD_STATUS.NO_SUCH_WINDOW],
            "value"     : "No such window"
        };
    },

    _handleLocateCommand = function(req, res, locatorMethod, rootElement, startTime) {
        // Search for a WebElement on the Page
        var elementOrElements,
            searchStartTime = startTime || new Date().getTime(),
            stopSearchByTime,
            request = {};

        _log.debug("_handleLocateCommand", "Element(s) Search Start Time: " + searchStartTime);

        // If a "locatorMethod" was not provided, default to "locateElement"
        if(typeof(locatorMethod) !== "function") {
            locatorMethod = this.locateElement;
        }

        // Some language bindings can send a null instead of an empty
        // JSON object for the getActiveElement command.
        if (req.post && typeof req.post === "string") {
            request = JSON.parse(req.post);
        }

        // Try to find the element
        elementOrElements = locatorMethod(request, rootElement);

        _log.debug("_handleLocateCommand.elements", JSON.stringify(elementOrElements));
        _log.debug("_handleLocateCommand.rootElement", (typeof(rootElement) !== "undefined" ? JSON.stringify(rootElement) : "BODY"));

        if (elementOrElements &&
            elementOrElements.hasOwnProperty("status") &&
            elementOrElements.status === 0 &&
            elementOrElements.hasOwnProperty("value")) {

            // return if elements found OR we passed the "stopSearchByTime"
            stopSearchByTime = searchStartTime + _session.getImplicitTimeout();
            if (elementOrElements.value.length !== 0 || new Date().getTime() > stopSearchByTime) {

                _log.debug("_handleLocateCommand", "Element(s) Found. Search Stop Time: " + stopSearchByTime);

                res.success(_session.getId(), elementOrElements.value);
                return;
            }
        }

        // retry if we haven't passed "stopSearchByTime"
        stopSearchByTime = searchStartTime + _session.getImplicitTimeout();
        if (stopSearchByTime >= new Date().getTime()) {

            _log.debug("_handleLocateCommand", "Element(s) NOT Found: RETRY. Search Stop Time: " + stopSearchByTime);

            // Recursive call in 50ms
            setTimeout(function(){
                _handleLocateCommand(req, res, locatorMethod, rootElement, searchStartTime);
            }, 50);
            return;
        }

        // Error handler. We got a valid response, but it was an error response.
        if (elementOrElements) {

            _log.error("_handleLocateCommand", "Element(s) NOT Found: GAVE UP. Search Stop Time: " + stopSearchByTime);

            _errors.handleFailedCommandEH(
                _errors.FAILED_CMD_STATUS_CODES_NAMES[elementOrElements.status],
                elementOrElements.value.message,
                req,
                res,
                _session,
                "SessionReqHand");
            return;
        }

        throw _errors.createInvalidReqVariableResourceNotFoundEH(req);
    };

    // public:
    return {
        locateElement : _locateElement,
        locateElements : _locateElements,
        locateActiveElement : _locateActiveElement,
        handleLocateCommand : _handleLocateCommand
    };
};
