// chrome.js
// Namhyeon Go <abuse@catswords.net>
// https://github.com/gnh1201/welsonjs

var STD = require("lib/std");
var RAND = require("lib/rand");
var SHELL = require("lib/shell");
var SYS = require("lib/system");
var FILE = require("lib/file");
var HTTP = require("lib/http");
var Websocket = require("lib/websocket");
var AutoIt = require("lib/autoit");
var Toolkit = require("lib/toolkit");
var ExtraMath = require("lib/extramath");

// for remote debugging
var pageEventId = 0;

var ChromeObject = function(interfaces) {
    STD.EventTarget.apply(this, arguments);  // Set event-attachable object

    this.interfaces = (typeof interfaces !== "undefined" ? interfaces : []);
    this.vendor = "Chrome";
    this.workingDirectory = SYS.getEnvString("PROGRAMFILES") + "\\Google\\Chrome\\Application";
    this.binPath = SYS.getEnvString("PROGRAMFILES") + "\\Google\\:installedDir\\Application\\chrome.exe";

    //this.processID = 0;
    this.installedDir = "Chrome";
    this.profileName = "Default";
    this.userDataDir = null;

    // proxy
    this.isPreventProxy = false;
    this.proxy = {
        "protocol": "socks5",
        "host": "127.0.0.1",
        "port": 1080,
        "ua": null
    };
    this.inPrivate = false;

    // user agent
    this.userAgent = null;
    this.userAgents = [];

    // dependencies
    this.oAutoIt = null;

    // for remote debugging
    this.debuggingPort = 0;
    this.pageId = "";
    this.ws = Websocket.create();
    this.isAttached = false;
    this.pageList = [];
    this.title = "";
    this.frameIndex = -1;

    this.isPreventEvaluate = false;
    this.isAppMode = false;
    this.baseScreenX = 0;
    this.baseScreenY = 0;

    this.create = function() {
        this.oAutoIt = AutoIt.create();
        this.baseScreenX = 1;
        this.baseScreenY = (!this.isAppMode ? 84 : 32);
        return this;
    };

    this.setIsPreventEvaluate = function(flag) {
        this.isPreventEvaluate = flag;
    };

    this.setBinPath = function(path) {
        this.binPath = path;
        return this;
    };

    this.setProfile = function(profileName, installedDir) {
        this.profileName = (profileName == "Default" ? "Chrome" : profileName);
        this.setInstalledDir(installedDir);
        this.workingDirectory = this.workingDirectory.replace(":installedDir", this.installedDir);
        this.binPath = this.binPath.replace(":installedDir", this.installedDir);
        //this.binPath = this.binPath.replace(":installedDir", "Chrome");
        return this;
    };

    this.clearProfile = function() {
        var FN = SHELL.getPathOfMyDocuments() + "\\UserData_Chrome_" + this.profileName + "\\" + this.profileName;
        while (FILE.folderExists(FN)) {
            try {
                return FILE.deleteFolder(FN);
            } catch (e) {
                console.warn("Can not clear the session! Resaon: " + e.message);
                console.warn("Retrying clear the profile...");
            }
        }
    };

    this.clear = function() {
        return this.clearProfile();
    };

    this.setUserDataDir = function(dirname) {
        if (dirname != null) {
            this.userDataDir = dirname;
        } else {
            this.userDataDir = SHELL.getPathOfMyDocuments() + "\\UserData_Chrome_" + this.profileName;
        }
        return this;
    };

    this.setInstalledDir = function(dirname) {
        if (dirname != null) {
            this.installedDir = dirname;
        }
        return this;
    };
    
    this.setProxy = function(obj) {
        this.proxy = obj;
        return this;
    };

    this.setProxyProtocol = function(s) {
        this.proxy.protocol = s;
        return this;
    };

    this.setProxyHost = function(s) {
        this.proxy.host = s;
        return this;
    };

    this.setProxyPort = function(port) {
        this.proxy.port = port;
        return this;
    };

    this.setIsPreventProxy = function(flag) {
        this.isPreventProxy = flag;
        return this;
    };

    this.setDebuggingPort = function(port) {
        this.debuggingPort = port;
        console.log("Enabled debugging port:", port);
        return this;
    };

    this.setFrameIndex = function(idx) {
        this.frameIndex = idx;
    };

    this.setPageId = function(s) {
        if (s == null) {
            var pageList = this.getPageList();
            if (pageList instanceof Array && pageList.length > 0) {
                this.pageId = pageList[0].id;
            }
        } else {
            this.pageId = s;
        }
    };

    this.createShoutcut = function(url) {
        if (!this.userDataDir) {
            this.userDataDir = SHELL.getPathOfMyDocuments() + "\\UserData_Chrome_" + this.profileName;
        }

        var cmd = [
            "cscript",
            "app.js",
            "shoutcut.legacy",
            "chrome",
            this.installedDir,
        ];

        // disable default browser check
        cmd.push("--no-default-browser-check");

        // disable popop blocking
        cmd.push("--disable-popup-blocking");

        // disable 3d
        cmd.push("--disable-3d-apis");

        // block non-proxyed webrtc traffic
        cmd.push("--force-webrtc-ip-handling-policy=disable-non-proxied-udp");

        // check incognito mode
        if (this.inPrivate) {
            cmd.push("--incognito");
        }

        // check debugging port
        if (this.debuggingPort > 0) {
            cmd.push("--remote-debugging-port=" + this.debuggingPort);
        }

        cmd.push("--profile-directory=\"" + this.profileName + "\"");
        cmd.push("--user-data-dir=\"" + this.userDataDir + "\"");
        cmd.push("\"" + url + "\"");

        SHELL.createShoutcut("Chrome Prototype (" + this.installedDir + ")", cmd.join(' '), SYS.getCurrentScriptDirectory());
    };

    this.setInPrivate = function(flag) {
        this.inPrivate = flag;
        return this;
    };

    this.setUserAgent = function(ua) {
        this.userAgent = ua;
        return this;
    };

    this.addUserAgent = function(ua) {
        this.userAgents.push(ua);
        return this;
    };

    this.addUserAgentsFromFile = function(filename) {
        var text = FILE.readFile(filename, FILE.CdoCharset.CdoUTF_8);
        var lines = text.split(/\r?\n/);
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            if (line !== "") {
                this.userAgents.push(line);
            }
        }
        return this;
    };

    this.open = function(url) {
        this.setProfile(this.profileName, this.installedDir);
        
        // if the file does not exists, Check the 32bit installation folder again
        if (!FILE.fileExists(this.binPath)) {
            this.workingDirectory = SYS.getEnvString("PROGRAMFILES(X86)") + "\\Google\\Chrome\\Application";
            this.binPath = SYS.getEnvString("PROGRAMFILES(X86)") + "\\Google\\:installedDir\\Application\\chrome.exe";
            this.setProfile(this.profileName, this.installedDir);
        }

        // find profile
        if (!FILE.fileExists(this.binPath)) {
            console.error("ChromeObject.open() -> '" + this.profileName + "' profile does not exists. You have to create it.");
            return this;
        }

        // create shoutcut to desktop
        if (this.debuggingPort == 0) {
            this.createShoutcut();
        }

        /*
        var process;
        while (this.processID == 0) {
            try {
                if (!this.userDataDir) {
                    this.userDataDir = SHELL.getPathOfMyDocuments() + "\\UserData_Chrome_" + this.profileName;
                }
                var shell = SHELL.create().setWorkingDirectory(this.workingDirectory);
                var process = shell.createProcess([
                    "\"" + this.binPath + "\"",
                    "--profile-directory=\"" + this.profileName + "\"",
                    "--proxy-server=\"socks5://127.0.0.1:" + this.proxyPort + "\"",
                    "--user-data-dir=\"" + this.userDataDir + "\"",
                    "\"" + url + "\""
                ].join(" "));
                sleep(1500);
                this.processID = process.ProcessID;
                sleep(1500);
                shell.release();
            } catch (e) {
                console.error("ChromeObject.open() ->", e.message);
                sleep(1500);
            }
        }
        */

        try {
            // get user data directory
            if (!this.userDataDir) {
                this.userDataDir = SHELL.getPathOfMyDocuments() + "\\UserData_Chrome_" + this.profileName;
            }

            // connect to shell
            var shell = SHELL.create();
            shell.setWorkingDirectory(this.workingDirectory);

            // initialize
            var cmd = [];

            // enable inPrivate (incognito) mode
            if (this.inPrivate) {
                cmd.push("--incognito");
            }

            // enable debugging port
            if (this.debuggingPort > 0) {
                cmd.push("--remote-debugging-port=" + this.debuggingPort);
            }

            // disable default browser check
            cmd.push("--no-default-browser-check");

            // disable popop blocking
            cmd.push("--disable-popup-blocking");

            // disable 3D
            cmd.push("--disable-3d-apis");

            // block non-proxyed webrtc traffic
            cmd.push("--force-webrtc-ip-handling-policy=disable-non-proxied-udp");
            
            // disable session crashed bubble (
            //cmd.push("--disable-session-crashed-bubble");

            // enable restore the last session
            //cmd.push("--restore-last-session");

            // set profile directory
            cmd.push("--profile-directory=\"" + this.profileName + "\"");

            // set proxy configuration
            if (this.proxy != null && this.isPreventProxy != true) {
                console.log("Enabled proxy server:", this.proxy.protocol + "://" + this.proxy.host + ":" + this.proxy.port);
                cmd.push("--proxy-server=\"" + this.proxy.protocol + "://" + this.proxy.host + ":" + this.proxy.port + "\"");
                if (this.proxy.ua != null) {
                    this.setUserAgent(this.proxy.ua);
                }
            }

            // set user data directory
            cmd.push("--user-data-dir=\"" + this.userDataDir + "\"");

            // choice user agent
            if (this.userAgent == null && this.userAgents.length > 0) {
                this.setUserAgent(RAND.one(this.userAgents));
            }

            // set user agent
            if (this.userAgent != null) {
                cmd.push("--user-agent=\"" + this.userAgent + "\"");
            }

            // set the URL
            if (!this.isAppMode) {
                cmd.push("\"" + url + "\"");
            } else {
                cmd.push("--app=\"" + url + "\"");
            }

            // build the command line
            console.log(cmd.join(" "));

            // run
            shell.runAs(this.binPath, cmd);
            sleep(300);

            // release shell
            shell.release();
        } catch (e) {
            console.error("ChromeObject.open() -> ", e.message);
            sleep(300);
        }

        return this;
    };
    
    this.getPageList = function() {
        var pageList = [];

        if (this.debuggingPort > 0) {
            try {
                //var responseText = HTTP.get("http://127.0.0.1:" + this.debuggingPort + "/json");
                //console.log(responseText);
                //pageList = JSON.parse(responseText);

                var pageList = HTTP.create("CURL")
                    .setDataType("json")
                    .open("GET", "http://127.0.0.1:" + this.debuggingPort + "/json")
                    .send()
                    .responseBody
                ;

                this.pageList = pageList;
                return pageList;
            } catch (e) {
                console.error("ChromeObject.getPageList() ->", e.message);
                //return this.getPageList();    // 무한 루프 문제로 주석처리
                return pageList;   // 바로 넘김
            }
        } else {
            console.error("Remote debugging unavailable");
            return pageList;
        }
    };

    this.getPageById = function(id) {
        return this.getPageList().find(function(x) {
            return (x.id == id);
        });
    };

    this.getPagesByUrl = function(url) {
        return this.getPageList().filter(function(x) {
            return (x.url.indexOf(url) == 0);
        });
    };

    this.getPagesByUrls = function(urls) {
        return this.getPageList().reduce(function(acc, x) {
            for (var i = 0; i < urls.length; i++) {
                if (x.url.indexOf(urls[i]) == 0) {
                    acc.push(x);
                }
            }
            return acc;
        }, []);
    };

    this.getPagesByTitle = function(title) {
        return this.getPageList().filter(function(x) {
            return (x.title.indexOf(title) == 0);
        });
    };

    this.getPagesByTitles = function(titles) {
        return this.getPageList().reduce(function(acc, x) {
            for (var i = 0; i < titles.length; i++) {
                if (x.title.indexOf(titles[i]) == 0) {
                    acc.push(x);
                }
            }
            return acc;
        }, []);
    };

    this.getParameterValue = function(paramName, defaultValue) {
        var paramValue = defaultValue;
        var page = this.getPageById(this.pageId);
        var params = page.url.split('&');

        for (var i = 0; i < params.length; i++) {
            if (params[i].indexOf(paramName + '=') == 0) {
                paramValue = params[i].substring((paramName + '=').length);
                //break;  // Do not break because will be get the last value
            }
        }

        return paramValue;
    };

    this.sendPageRPC = function(method, params) {
        var result = null;

        try {
            if (this.pageId != "") {
                result = this.ws.send("ws://127.0.0.1:" + this.debuggingPort + "/devtools/page/" + this.pageId, JSON.stringify({
                    "id": pageEventId,
                    "method": method,
                    "params": params
                }));
                pageEventId++;
                console.log("ChromeObject().sendPageRPC() -> Sent");
            } else {
                this.setPageId(null);
                if (this.pageId != "") {
                    result = this.sendPageRPC(method, params);
                } else {
                    console.error("Page not found");
                }
            }
        } catch (e) {
            console.log("ChromeObject.sendPageRPC() ->", e.message);
        }

        return result;
    };

    this.navigate = function(url) {
        return this.sendPageRPC("Page.navigate", {
            "url": url
        });
    };
    
    this.navigateEvaluately = function(url) {
        return this.evaluate('location.href = "' + url + '"');
    };

    this.evaluate = function(expression, frameIndex) {
        if (this.isPreventEvaluate) return;

        var frameIndex = (typeof frameIndex !== "undefined" ? frameIndex : this.frameIndex);

        if (frameIndex > -1) {
            expression = 'var __getFrame=function(e){return document.getElementsByTagName("frame")[e]},__getDocument=function(){return __getFrame(' + frameIndex + ').contentDocument||__getFrame(' + frameIndex + ').contentWindow.document},__getWindow=function(){return __getFrame(' + frameIndex + ').contentWindow};' + expression;
        } else {
            expression = 'var __getDocument=function(){return document},__getWindow=function(){return window};' + expression;
        }

        try {
            return this.sendPageRPC("Runtime.evaluate", {
                "expression": expression
            });
        } catch (e) {
            console.error("ChromeObject.evaluate() ->", e.message);
        }
    };

    this.getEvaluatedValue = function(expression) {
        try {
            var responseText = this.evaluate(expression);
            console.log(responseText);

            var result = JSON.parse(responseText).result.result.value;
            if (typeof(result) !== "undefined" && result != null) {
                return result;
            } else {
                return "";
            }
        } catch (e) {
            console.error("ChromeObject.getEvaluatedValue() ->", e.message);
            return "";
        }
    };

    this.exit = function() {
        return this.sendPageRPC("Browser.close", {});
    };
    
    this.close = function() {
        var response = this.sendPageRPC("Page.close", {});
        this.setPageId(null);
        return response;
    };

    this.terminate = function() {
        try {
            this.oAutoIt.callFunction("WinKill", [this.getTitle()]);
        } catch (e) {
            console.error("ChromeObject.terminate() ->", e.message);
        }
    };

    this.focus = function() {
        var title = "";

        if (this.debuggingPort > 0) {
            try {
                // set page id
                if (this.pageId == "") {
                    this.setPageId(null);
                }

                // calling _focus()
                title = this._focus();

                // find window by title
                var pageList = this.getPageList();
                if (pageList.length > 0) {
                    this.oAutoIt.callFunction("WinActivate", [title]);
                }
            } catch (e) {
                console.error("ChromeObject.focus() ->", e.message);
            }
        }

        // calling `onfocus` event
        this.dispatchEvent(new STD.Event("focus"));

        return title;
    };

    this._focus = function() {
        var title = "";
        
        try {
            // get current title
            var _title = this.getTitle();

            // if not focused
            if (_title.indexOf(this.pageId.substring(0, 6)) < 0) {
                // save previous title
                this.title = _title;

                // will be change title
                title = this.title + " " + this.pageId.substring(0, 6);

                // change webpage title for focusing window
                this.setTitle(title);
            } else {
                title = _title;   /// when it is already catch
            }
        } catch (e) {
            console.error("ChromeObject._focus() ->", e.message);
        }

        return title;
    };

    this.blur = function() {
        return this.setTitle(this.title);
    };

    this.getScrollHeight = function(selector) {
        return parseInt(this.getEvaluatedValue('__getDocument().querySelector("' + selector + '").scrollHeight'));
    };

    this.focusWithoutActivate = function() {
        if (this.debuggingPort > 0) {
            try {
                // if page ID is empty
                if (this.pageId == "") {
                    var pageList = this.getPageList();
                    if (pageList instanceof Array && pageList.length > 0) {
                        this.pageId = pageList[0].id;
                    }
                }

                // change webpage title for focusing window
                this.setTitle(this.pageId);
            } catch (e) {
                console.error("ChromeObject.focusWithoutActivate() ->", e.message);
            }
        }
    };

    this.autoAdjustByScreen = function(sX, sY, divX, divY) {
        // focus
        var title = this.focus();
        sleep(300);

        // adjust window position and size
        var bX = Math.floor(sX / divX);
        var bY = Math.floor(sY / divY);
        var x = this.getRandomInt(0, bX);
        var y = this.getRandomInt(0, bY);
        var w = this.getRandomInt(bX * 3, sX - bX);
        var h = this.getRandomInt(bY, sY - bY);
        this.oAutoIt.callFunction("WinMove", [title, "", x, y, w, h]);
        
        // blur
        this.blur();
    };

    this.autoAdjustByWindow = function(sX, sY, w1, w2, h1, h2) {
        // catch focus
        var title = this.focus();
        sleep(300);

        // adjust window position and size
        var w = this.getRandomInt(w1, w2);
        var h = this.getRandomInt(h1, h2);
        var x = this.getRandomInt(0, (sX - w < 0 ? parseInt(sX * 0.2) : (sX - w)));
        var y = this.getRandomInt(0, (sY - h < 0 ? parseInt(sY * 0.2) : (sY - h)));
        this.oAutoIt.callFunction("WinMove", [title, "", x, y, w, h]);

        // blur
        this.blur();
    };
    
    this.downMouseWheel = function(times) {
        if (this.debuggingPort > 0) {
            try {
                var pos = this.getScreenPosition();
                this.oAutoIt.callFunction("MouseMove" [pos.x + 100, pos.y + 100]);
                this.oAutoIt.callFunction("MouseWheel", ["down", times]);
            } catch (e) {
                console.error("ChromeObject.downMouseWheel() ->", e.message);
            }
        }
    };

    this.upMouseWheel = function(times) {
        if (this.debuggingPort > 0) {
            try {
                var pos = this.getScreenPosition();
                this.oAutoIt.callFunction("MouseMove", [pos.x + 100, pos.y + 100]);
                this.oAutoIt.callFunction("MouseWheel", ["up", times]);
            } catch (e) {
                console.error("ChromeObject.upMouseWheel() ->", e.message);
            }
        }
    };
    
    this.setTitle = function(title) {
        var i = 0, repeat = 2;

        if (this.debuggingPort > 0) {
            while (i < repeat) {
                this.evaluate('document.title = ' + this.__escape(title));
                i++;
            }
        }
    };

    this.getTitle = function() {
        if (this.debuggingPort > 0) {
            return this.getEvaluatedValue('document.title');
        }
    };

    this.getScreenPosition = function() {
        var result = this.getEvaluatedValue('(function() { return [__getWindow().screenX, __getWindow().screenY].join(","); })();');
        var pos = result.split(',');
        return {
            "x": parseInt(pos[0]),
            "y": parseInt(pos[1])
        };
    };

    this.getElementPosition = function(selector, startIndex) {
        var startIndex = (typeof startIndex !== 'undefined' ? startIndex : 0);
        var result;
        var pos = -1;

        if (startIndex > 0) {
            result = this.getEvaluatedValue('(function(k) { var rect = __getDocument().querySelectorAll("' + selector + '")[k].getBoundingClientRect(); return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height)].join(","); })(' + startIndex + ');');
        } else {
            result = this.getEvaluatedValue('(function() { var rect = __getDocument().querySelector("' + selector + '").getBoundingClientRect(); return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height)].join(","); })();');
        }

        pos = result.split(',');
        if (pos.length == 6) {
            return {
                "x": parseInt(pos[0]),
                "y": parseInt(pos[1]),
                "a": parseInt(pos[2]),
                "b": parseInt(pos[3]),
                "w": parseInt(pos[4]),
                "h": parseInt(pos[5])
            };
        } else {
            return {
                "x": -1,
                "y": -1,
                "a": -1,
                "b": -1,
                "g": -1,
                "d": -1
            };
        }
    };

    this.getElementPositionByText = function(selector, searchText) {
        var result;
        var pos = -1;
        var s = '(function() {'
            + '    var element = Object.values(__getDocument().querySelectorAll("' + selector + '")).find(function(x) {'
            + '        return (x.innerText.indexOf(' + this.__escape() + ') > -1);'
            + '    });'
            + '    if (element) {'
            + '        var rect = element.getBoundingClientRect();'
            + '        return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height)].join(",");'
            + '    }'
            + '})()'
        ;

        result = this.getEvaluatedValue(s);
        pos = result.split(',');
        if (pos.length == 6) {
            return {
                "x": parseInt(pos[0]),
                "y": parseInt(pos[1]),
                "a": parseInt(pos[2]),
                "b": parseInt(pos[3]),
                "w": parseInt(pos[4]),
                "h": parseInt(pos[5])
            };
        } else {
            return {
                "x": -1,
                "y": -1,
                "a": -1,
                "b": -1,
                "g": -1,
                "d": -1
            };
        }
    };

    this.getNestedElementPosition = function(selector, subSelector, searchText, startIndex) {
        var s = '';
        var startIndex = (typeof startIndex !== 'undefined' ? startIndex : 0);

        if (searchText.indexOf(':tokenize(') == 0) {
            searchText = searchText.substring(searchText.indexOf('(') + 1, searchText.lastIndexOf(')'));
            s += '(function() {'
                + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '")).filter(function(x) {'
                + '        var el = ' + (subSelector == ':self' ? 'x;' : 'x.querySelector("' + subSelector + '");')
                + '        var keywords = ' + this.__escape(searchText) + '.trim().split(" ");'
                + '        var text = el instanceof HTMLElement ? [el.innerText, el.getAttribute("aria-label"), el.getAttribute("class")].join(" ") : "";'
                + '        return (text.split(" ").filter(function(w) { return keywords.indexOf(w) > -1; }).length >= keywords.length);'
                + '    ' + (startIndex > 0 ? '}).slice(' + startIndex + ');' : '});')
                + '    if (elements.length > 0) {'
                + '        var element = elements[0];'
                + '        var rect = element.getBoundingClientRect();'
                + '        var elClassName = "welsonjs_" + parseInt(Math.random() * 1000000000);'
                + '        element.setAttribute("class", element.getAttribute("class") + " " + elClassName);'
                + '        return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height), "." + elClassName].join(",");' 
                + '    } else {'
                + '        return "";'
                + '    }'
                + '})()'
            ;
        }

        else if (searchText.indexOf(':text(') == 0) {
            this.evaluate(ExtraMath.export_measureSimilarity());

            searchText = searchText.substring(searchText.indexOf('(') + 1, searchText.lastIndexOf(')'));
            s += '(function() {'
                + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '")).filter(function(x) {'
                + '        var el = ' + (subSelector == ':self' ? 'x;' : 'x.querySelector("' + subSelector + '");')
                + '        var searchText = ' + this.__escape(searchText) + '.trim();'
                + '        var text = el instanceof HTMLElement ? el.innerText : "";'
                + '        return ExtraMath.measureSimilarity(text, searchText) >= 0.9;'
                + '    ' + (startIndex > 0 ? '}).slice(' + startIndex + ');' : '});')
                + '    if (elements.length > 0) {'
                + '        var element = elements[0];'
                + '        var rect = element.getBoundingClientRect();'
                + '        var elClassName = "welsonjs_" + parseInt(Math.random() * 1000000000);'
                + '        element.setAttribute("class", element.getAttribute("class") + " " + elClassName);'
                + '        return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height), "." + elClassName].join(",");' 
                + '    } else {'
                + '        return "";'
                + '    }'
                + '})()'
            ;
        }

        else if (searchText.indexOf(':p(') == 0) {
            var p = parseFloat(searchText.substring(searchText.indexOf('(') + 1, searchText.lastIndexOf(')')));
            if (p > 0) {
                s += '(function() {'
                    + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '")).filter(function(x) {'
                    + '        return (Math.random() < ' + p + ');'
                    + '    ' + (startIndex > 0 ? '}).slice(' + startIndex + ');' : '});')
                    + '    if (elements.length > 0) {'
                    + '        var element = elements[0];'
                    + '        var rect = element.getBoundingClientRect();'
                    + '        var elClassName = "welsonjs_" + parseInt(Math.random() * 1000000000);'
                    + '        element.setAttribute("class", element.getAttribute("class") + " " + elClassName);'
                    + '        return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height), "." + elClassName].join(",");' 
                    + '    } else {'
                    + '        return "";'
                    + '    }'
                    + '})()'
                ;
            } else {
                s += '(function() {'
                    + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '"));'
                    + '    ' + (startIndex > 0 ? 'elements = elements.slice(' + startIndex + ');' : '')
                    + '    if (elements.length > 0) {'
                    + '        var k = Math.floor(Math.random() * elements.length);'
                    + '        var element = elements[k];'
                    + '        var rect = element.getBoundingClientRect();'
                    + '        var elClassName = "welsonjs_" + parseInt(Math.random() * 1000000000);'
                    + '        element.setAttribute("class", element.getAttribute("class") + " " + elClassName);'
                    + '        return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height), "." + elClassName].join(",");' 
                    + '    } else {'
                    + '        return "";'
                    + '    }'
                    + '})()'
                ;
            }
        }

        else {
            s += '(function() {'
                + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '")).filter(function(x) {'
                + '        var el = ' + (subSelector == ':self' ? 'x;' : 'x.querySelector("' + subSelector + '");')
                + '        var searchText = ' + this.__escape(searchText) + '.trim();'
                + '        var text = el instanceof HTMLElement ? [el.innerText, el.getAttribute("aria-label"), el.getAttribute("class")].join(" ") : "";'
                + '        return (text.indexOf(searchText) > -1);'
                + '    ' + (startIndex > 0 ? '}).slice(' + startIndex + ');' : '});')
                + '    if (elements.length > 0) {'
                + '        var element = elements[0];'
                + '        var rect = element.getBoundingClientRect();'
                + '        var elClassName = "welsonjs_" + parseInt(Math.random() * 1000000000);'
                + '        element.setAttribute("class", element.getAttribute("class") + " " + elClassName);'
                + '        return [parseInt(rect.left), parseInt(rect.top), parseInt(__getWindow().pageXOffset + rect.left), parseInt(__getWindow().pageYOffset + rect.top), parseInt(rect.width), parseInt(rect.height), "." + elClassName].join(",");' 
                + '    } else {'
                + '        return "";'
                + '    }'
                + '})()'
            ;
        }

        var result = this.getEvaluatedValue(s);
        var pos = result.split(',');
        if (pos.length == 7) {
            return {
                "x": parseInt(pos[0]),
                "y": parseInt(pos[1]),
                "a": parseInt(pos[2]),
                "b": parseInt(pos[3]),
                "w": parseInt(pos[4]),
                "h": parseInt(pos[5]),
                "s": pos[6]
            };
        } else {
            return {
                "x": -1,
                "y": -1,
                "a": -1,
                "b": -1,
                "w": -1,
                "h": -1,
                "s": ""
            };
        }
    };

    this.triggerEventOnNestedElement = function(eventName, selector, subSelector, searchText, startIndex) {
        var s = '';
        var startIndex = (typeof startIndex !== 'undefined' ? startIndex : 0);

        if (searchText.indexOf(':tokenize(') == 0) {
            searchText = searchText.substring(searchText.indexOf('(') + 1, searchText.lastIndexOf(')'));
            s += '(function() {'
                + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '")).filter(function(x) {'
                + '        var el = ' + (subSelector == ':self' ? 'x;' : 'x.querySelector("' + subSelector + '");')
                + '        var keywords = ' + this.__escape(searchText) + '.trim().split(" ");'
                + '        var text = el instanceof HTMLElement ? [el.innerText, el.getAttribute("aria-label"), el.getAttribute("class")].join(" ") : "";'
                + '        return (text.split(" ").filter(function(w) { return keywords.indexOf(w) > -1; }).length >= keywords.length);'
                + '    ' + (startIndex > 0 ? '}).slice(' + startIndex + ');' : '});')
                + '    if (elements.length > 0) {'
                + '        elements[0].' + (eventName == 'click' ? 'click()' : 'dispatchEvent(new Event("' + eventName + '"))') + ';'
                + '    }'
                + '})()'
            ;
        } else if (searchText.indexOf(':p(') == 0) {
            var p = parseFloat(searchText.substring(searchText.indexOf('(') + 1, searchText.lastIndexOf(')')));
            if (p > 0) {
                s += '(function() {'
                    + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '")).filter(function(x) {'
                    + '        return (Math.random() < ' + p + ');'
                    + '    ' + (startIndex > 0 ? '}).slice(' + startIndex + ');' : '});')
                    + '    if (elements.length > 0) {'
                    + '        elements[0].' + (eventName == 'click' ? 'click()' : 'dispatchEvent(new Event("' + eventName + '"))') + ';'
                    + '    }'
                    + '})()'
                ;
            } else {
                s += '(function() {'
                    + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '"));'
                    + '    ' + (startIndex > 0 ? 'elements = elements.slice(' + startIndex + ');' : '')
                    + '    if (elements.length > 0) {'
                    + '        var k = Math.floor(Math.random() * elements.length);'
                    + '        elements[k].' + (eventName == 'click' ? 'click()' : 'dispatchEvent(new Event("' + eventName + '"))') + ';'
                    + '    }'
                    + '})()'
                ;
            }
        } else {
            s += '(function() {'
                + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '")).filter(function(x) {'
                + '        var el = ' + (subSelector == ':self' ? 'x;' : 'x.querySelector("' + subSelector + '");')
                + '        var searchText = ' + this.__escape(searchText) + '.trim();'
                + '        var text = el instanceof HTMLElement ? [el.innerText, el.getAttribute("aria-label"), el.getAttribute("class")].join(" ") : "";'
                + '        return (text.indexOf(searchText) > -1);'
                + '    ' + (startIndex > 0 ? '}).slice(' + startIndex + ');' : '});')
                + '    if (elements.length > 0) {'
                + '        elements[0].' + (eventName == 'click' ? 'click()' : 'dispatchEvent(new Event("' + eventName + '"))') + ';'
                + '    }'
                + '})()'
            ;
        }

        return this.evaluate(s);
    };

    this.getNestedElementIndex = function(selector, subSelector, searchText) {
        var s = '';

        s += '(function() {'
            + '    var elements = Object.values(__getDocument().querySelectorAll("' + selector + '"));'
            + '    var result = -1;'
            + '    for (var i = 0; i < elements.length; i++) {'
            + '        if (x.querySelector("' + subSelector + '").innerText.indexOf(' + this.__escape(searchText) + ') > -1) {'
            + '            result = i;'
            + '            break;'
            + '        }'
            + '    }'
            + '    return result;'
            + '})()'
        ;

        return parseInt(this.getEvaluatedValue(s));
    };

    this.getElementCount = function(selector) {
        return this.getEvaluatedValue('document.querySelectorAll("' + selector + '").length');
    };

    this.getPageHeight = function() {
        var height = 0;

        if (this.debuggingPort > 0) {
            var result = this.getEvaluatedValue('(function(obj) { return Math.max(obj.scrollHeight, obj.clientHeight); })(__getDocument().querySelector("html"))');
            height = parseInt(result);
        }

        return height;
    };
    
    this.setIsAttached = function(isAttached) {
        this.isAttached = isAttached;
        return this;
    };

    this.getRandomInt = function(min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min + 1)) + min;
    };
    
    this.checkDebuggingPort = function() {
        var isChecked = false;
        var isDone = false;

        while (!isDone) {
            try {
                if (this.debuggingPort > 0) {
                    var result = SHELL.exec("netstat -ano | findstr :" + this.debuggingPort);
                    if (result.indexOf(":" + this.debuggingPort) > -1) {
                        isChecked = true;
                    }
                }

                isDone = true;
            } catch(e) {
                sleep(1);
                console.error("ChromeObject.checkDebuggingPort() ->", e.message);
            }
        }

        return isChecked;
    };

    this.getCurrentUrl = function() {
        var page = this.getPageById(this.pageId);
        return page.url;
    };

    this.getCurrentDomain = function() {
        return this.getEvaluatedValue('__getDocument().domain') || '';
    };

    this.triggerEvent = function(eventName, selector) {
        if (selector.indexOf(':p(') < 0) {
            if (eventName == 'click') {
                return this.evaluate('__getDocument().querySelector("' + selector + '").click()');
            } else {
                return this.evaluate('__getDocument().querySelector("' + selector + '").dispatchEvent(new Event("' + eventName + '"))');
            }
        } else {
            var p = parseFloat(selector.substring(selector.indexOf('(') + 1, selector.indexOf(')')));
            var _selector = selector.substring(0, selector.indexOf(':'));
            if (p > 0) {
                return this.evaluate('(function(obj, p) { var element = Object.values(obj).find(function() { return (Math.random() < p); }); if(element) element.click(); })(__getDocument().querySelectorAll("' + _selector + '"), ' + p + ')');
            } else {
                return this.evaluate('(function(obj) { var elements = Object.values(obj); var element = elements[Math.floor(Math.random() * elements.length)]; if(element) element.click(); })(__getDocument().querySelectorAll("' + _selector + '"))');
            }
        }
    };

    this.triggerEventByFind = function(eventName, selector, searchText) {
        var s = '(function() {'
            + '    var element = Object.values(__getDocument().querySelectorAll("' + selector + '")).find(function(x) {'
            + '        return (x.innerText.indexOf(' + this.__escape(searchText) + ') > -1);'
            + '    });'
            + '    if (element) {'
            + '        element.' + (eventName == 'click' ? 'click()' : 'dispatchEvent(new Event("' + eventName + '"))') + ';'
            + '    }'
            + '})()'
        ;
        return this.evaluate(s);
    };

    this.triggerEventOnNestedFind = function(eventName, selector, subSelector, searchText) {
        var s = '(function() {'
            + '    var element = Object.values(__getDocument().querySelectorAll("' + selector + '")).find(function(x) {'
            + '        return (x.querySelector("' + subSelector + '").innerText.indexOf(' + this.__escape(searchText) + ') > -1);'
            + '    });'
            + '    if (element) {'
            + '        element.' + (eventName == 'click' ? 'click()' : 'dispatchEvent(new Event("' + eventName + '"))') + ';'
            + '    }'
            + '})()'
        ;
        return this.evaluate(s);
    };

    this.scrollTo = function(x, y) {
        return this.evaluate('__getWindow().scrollTo(parseInt(' + x + '), parseInt(' + y + '))');
    };

    this.scrollBy = function(dx, dy) {
        return this.evaluate('__getWindow().scrollBy(parseInt(' + dx + '), parseInt(' + dy + '))');
    };

    this.scrollToElement = function(selector, dx, dy) {
        return this.evaluate('(function(rect, dx, dy) { __getWindow().scrollTo(rect.x + dx, rect.y + dy); })(__getDocument().querySelector("' + selector + '").getBoundingClientRect(), parseInt("' + dx + '"), parseInt("' + dy + '"))');
    };

    this.reload = function() {
        //return this.sendPageRPC("Page.reload", {});
        return this.evaluate("__getWindow().reload()");
    };

    this.hasClass = function(seletctor, className) {
        try {
            var result = this.getEvaluatedValue('__getDocument().querySelector("' + seletctor + '").getAttribute("class")');
            if (typeof(result) === "string") {
                return (result.split(' ').indexOf(className) > -1);
            } else {
                return false;
            }
        } catch (e) {
            console.error("ChromeObject.hasClass() ->", e.message);
        }
    };

    this.getAttribute = function(selector, attributeName) {
        return this.getEvaluatedValue('__getDocument().querySelector("' + selector + '").getAttribute("' + attributeName + '")');
    };

    this.sendKeys = function(s) {
        this.oAutoIt.callFunction("Send", [s]);
    };

    this.sendSpaceKey = function() {
        this.oAutoIt.callFunction("Send", ["{SPACE}"]);
    };

    this.setValue = function(selector, value, repeat, searchIndex) {
        var s = value,
            i = 0,
            searchIndex = (typeof searchIndex !== "undefined" ? searchIndex : 0),
            repeat = (typeof repeat !== "undefined" ? repeat : 1)
        ;

        while (i < repeat) {
            if (searchIndex > 0) {
                this.evaluate('Object.values(__getDocument().querySelectorAll("' + selector + '"))[' + searchIndex + '].value = ' + this.__escape(s));
            } else {
                this.evaluate('__getDocument().querySelector("' + selector + '").value = ' + this.__escape(s));
            }
            i++;
        }
    };

    this.getText = function(selector) {
        return this.getEvaluatedValue('__getDocument().querySelector("' + selector + '").innerText');
    };

    this.setHTML = function(selector, value, repeat, searchIndex) {
        var s = value,
            i = 0,
            searchIndex = (typeof searchIndex !== "undefined" ? searchIndex : 0),
            repeat = (typeof repeat !== "undefined" ? repeat : 1)
        ;

        while (i < repeat) {
            if (searchIndex > 0) {
                this.evaluate('Object.values(__getDocument().querySelectorAll("' + selector + '"))[' + searchIndex + '].value = ' + this.__escape(s));
            } else {
                this.evaluate('__getDocument().querySelector("' + selector + '").innerHTML = ' + this.__escape(s));
            }
            i++;
        }
    };

    this.getHTML = function(selector) {
        return this.getEvaluatedValue('__getDocument().querySelector("' + selector + '").innerHTML');
    };

    this.traceMouseClick = function() {
        return this.evaluate('__getWindow().addEventListener("click",function(e){var t=e.clientX,n=e.clientY,l=__getDocument().createElement("div");l.style.position="absolute",l.style.width="20px",l.style.height="20px",l.style.backgroundColor="#00ff00",l.style.zIndex=99999,l.style.top=__getWindow().pageYOffset+n-10+"px",l.style.left=__getWindow().pageXOffset+t-10+"px",__getDocument().body.appendChild(l)});');
    };

    // Added in 2023-12-27
    this.markPosition = function(x, y) {
        return this.evaluate('(function(x, y){var t=x,n=y,l=__getDocument().createElement("div");l.style.position="absolute",l.style.width="20px",l.style.height="20px",l.style.backgroundColor="#ff0000",l.style.zIndex=99999,l.style.top=__getWindow().pageYOffset+n-10+"px",l.style.left=__getWindow().pageXOffset+t-10+"px",__getDocument().body.appendChild(l)})(' + parseInt(x) + ', ' + parseInt(y) + ');');
    };

    // Added in 2023-12-27
    this.getTextsBySelectorAll = function(selector) {
        return JSON.parse(this.getEvaluatedValue('JSON.stringify(Object.values(__getDocument().querySelectorAll("' + selector + '")).reduce(function(a, x) { a.push(x.innerText); return a; }, []))'));
    };

    this.getWindowInnerHeight = function() {
        return parseInt(this.getEvaluatedValue('__getWindow().innerHeight'));
    };

    this.getWindowPageYOffset = function() {
        return this.getEvaluatedValue('__getWindow().pageYOffset');
    };
    
    this.getDocumentBodyOffsetHeight = function() {
        return this.getEvaluatedValue('__getDocument().body.offsetHeight');
    };

    this.getDocumentScrollTop = function() {
        return parseInt(this.getEvaluatedValue('__getDocument().documentElement.scrollTop'));
    };

    // formula: y > 0 and y + h < ih
    this.isVisibleElementInViewport = function(elementPosition) {
        return (elementPosition.y > 0 && (elementPosition.y + elementPosition.h < this.getWindowInnerHeight()));
    };

    this.isPageScrollEnded = function() {
        return (this.getWindowInnerHeight() + this.getWindowPageYOffset()) >= this.getDocumentBodyOffsetHeight;
    };

    this.__escape = function(value) {
        var pos = value.indexOf("__escaped:");
        if (pos === 0)
            return 'decodeURIComponent("' + value.substring(10) + '")';
        else
            return 'decodeURIComponent("' + this.encodeURIComponent(value)[0] + '")';
    };

    // Added in 2024-01-08
    this.encodeURI = function(s) {
        return [encodeURI(s), s];
    };
    
    // Added in 2024-01-08
    this.decodeURI = function(s) {
        if (s instanceof Array && s.length == 2) {
            return s[1];
        } else {
            return decodeURI(s);
        }
    };

    // Added in 2024-01-08
    this.encodeURIComponent = function(s) {
        return [encodeURIComponent(s), s];
    };

    // Added in 2024-01-08
    this.decodeURIComponent = function(s) {
        if (s instanceof Array && s.length == 2) {
            return s[1];
        } else {
            return decodeURIComponent(s);
        }
    };

    this.prompt = function(s) {
        return this.getEvaluatedValue('prompt("' + s + '")');
    };
    
    this.confirm = function(s) {
        return this.getEvaluatedValue('(confirm("' + s + '") ? "true" : "false")');
    };

    this.setVendor = function(vendor) {
        var vendor = vendor.toLowerCase();

        switch (vendor) {
            case "msedge":
                this.workingDirectory = SYS.getEnvString("PROGRAMFILES(X86)") + "\\Microsoft\\Edge\\Application";
                this.binPath = SYS.getEnvString("PROGRAMFILES(X86)") + "\\Microsoft\\Edge\\Application\\msedge.exe";
                break;

            case "chrome":
                this.workingDirectory = SYS.getEnvString("PROGRAMFILES") + "\\Google\\Chrome\\Application";
                this.binPath = SYS.getEnvString("PROGRAMFILES") + "\\Google\\:installedDir\\Application\\chrome.exe";
                break;

            case "chromium":
                this.workingDirectory = SYS.getEnvString("LOCALAPPDATA") + "\\Chromium\\Application";
                this.binPath = SYS.getEnvString("LOCALAPPDATA") + "\\Chromium\\Application\\chrome.exe";
                break;

            case "opera":
                this.workingDirectory = SYS.getEnvString("LOCALAPPDATA") + "\\Programs\\Opera";
                this.binPath = SYS.getEnvString("LOCALAPPDATA") + "\\Programs\\Opera\\opera.exe";
                break;
        }

        return this;
    };

    this.vMouseClick = function(x, y) {
        Toolkit.sendClick(this.pageId.substring(0, 6), this.baseScreenX + x, this.baseScreenY + y, 1);
    };

    this.vSendKeys = function(s) {
        Toolkit.sendKeys(this.pageId.substring(0, 6), s);
    };

    this.mouseClick = function(x, y) {
        var screenPosition = this.getScreenPosition();
        this.oAutoIt.callFunction("MouseMove", [screenPosition.x + this.baseScreenX + x, screenPosition.y + this.baseScreenY + y]);
        this.oAutoIt.callFunction("MouseClick", ["left"]);
    };

    this.mouseWheelUp = function() {
        this.oAutoIt.callFunction("MouseWheel", ["up"]);
    };

    this.mouseWheelDown = function() {
        this.oAutoIt.callFunction("MouseWheel", ["down"]);
    };

    this.getReadyState = function() {
        return this.getEvaluatedValue("document.readyState");
    };
    
    this.getCookie = function() {
        return this.getEvaluatedValue("document.cookie");
    };

    this.getNumberOfSelectorAll = function(selector) {
        return parseInt(this.getEvaluatedValue('document.querySelectorAll("' + selector + '").length'));
    };

    this.setValueOfSelectorAll = function(selector, s) {
        this.evaluate('document.querySelectorAll("' + selector + '").forEach(function(x){x.value = "' + s + '";})');
    };

    this.sendEnterKey = function() {
        this.evaluate('var ev=new KeyboardEvent("keydown",{bubbles:!0,cancelable:!0,keyCode:13});document.body.dispatchEvent(ev);');
    };

    this.getShadowRootSelector = function(selectors) {
        var s = "').shadowRoot.querySelector('";
        return ".querySelector('" + selectors.join(s) + "')";
    };

    this.create();
};
ChromeObject.prototype = new STD.EventTarget();
ChromeObject.prototype.constructor = ChromeObject;

exports.create = function(profileName) {
    return (new ChromeObject()).setProfile(profileName, null);
};

exports.start = function(url, proxyPort, profileName, userDataDir, installedDir) {
    return (new ChromeObject())
        .setProxyPort(proxyPort)
        .setProfile(profileName, installedDir)
        .setUserDataDir(userDataDir)
        .open(url)
    ;
};

exports.startWithDebugging = function(url, proxy, profileName, debuggingPort) {
    return (new ChromeObject())
        .setProxy(proxy)
        .setProfile(profileName, null)
        .setUserDataDir(null)
        .setDebuggingPort(debuggingPort)
        .open(url)
    ;
};

exports.startWithDebuggingUA = function(url, proxy, profileName, debuggingPort) {
    return (new ChromeObject())
        .setProxy(proxy)
        .setProfile(profileName, null)
        .setUserDataDir(null)
        .setDebuggingPort(debuggingPort)
        .addUserAgentsFromFile("data\\UserAgents.txt")
        .open(url)
    ;
};

exports.startDebug = function(url, proxy, profileName, debuggingPort, isPreventProxy) {
    return (new ChromeObject())
        .setProxy(proxy)
        .setProfile(profileName, null)
        .setUserDataDir(null)
        .setDebuggingPort(debuggingPort)
        .setIsPreventProxy(isPreventProxy)
        //.addUserAgentsFromFile("data\\Chrome.txt")
        //.addUserAgentsFromFile("data\\Edge.txt")
        //.addUserAgentsFromFile("data\\Safari.txt")
        .open(url)
    ;
};

exports.startDebugInPrivate = function(url, proxy, profileName, debuggingPort, isPreventProxy) {
    return (new ChromeObject())
        .setProxy(proxy)
        .setProfile(profileName, null)
        .setUserDataDir(null)
        .setDebuggingPort(debuggingPort)
        .setIsPreventProxy(isPreventProxy)
        .setInPrivate(true)
        //.addUserAgentsFromFile("data\\Chrome.txt")
        //.addUserAgentsFromFile("data\\Edge.txt")
        //.addUserAgentsFromFile("data\\Safari.txt")
        .open(url)
    ;
};

exports.VERSIONINFO = "Chrome Web Browser Debugging Interface (chrome.js) version 0.4.6";
exports.AUTHOR = "abuse@catswords.net";
exports.global = global;
exports.require = global.require;