🏆 [#1 Chess Cheat] A.C.A.S (Advanced Chess Assistance System)

Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system

// ==UserScript==
// @name        🏆 [#1 Chess Cheat] A.C.A.S (Advanced Chess Assistance System)
// @name:en     🏆 [#1 Chess Cheat] A.C.A.S (Advanced Chess Assistance System)
// @name:fi     🏆 [#1 Chess Cheat] A.C.A.S (Edistynyt shakkiavustusjärjestelmä)
// @name:sw     🏆 [#1 Chess Cheat] A.C.A.S (Advanserad Schack Assitant System)
// @name:zh-CN  🏆 [#1 Chess Cheat] A.C.A.S(高级国际象棋辅助系统)
// @name:es     🏆 [#1 Chess Cheat] A.C.A.S (Sistema Avanzado de Asistencia al Ajedrez)
// @name:hi     🏆 [#1 Chess Cheat] A.C.A.S (उन्नत शतरंज सहायता प्रणाली)
// @name:ar     🏆 [#1 Chess Cheat] A.C.A.S (نظام المساعدة المتقدم في الشطرنج)
// @name:pt     🏆 [#1 Chess Cheat] A.C.A.S (Sistema Avançado de Assistência ao Xadrez)
// @name:ja     🏆 [#1 Chess Cheat] A.C.A.S(先進的なチェス支援システム)
// @name:de     🏆 [#1 Chess Cheat] A.C.A.S (Fortgeschrittenes Schach-Hilfesystem)
// @name:fr     🏆 [#1 Chess Cheat] A.C.A.S (Système Avancé d'Assistance aux Échecs)
// @name:it     🏆 [#1 Chess Cheat] A.C.A.S (Sistema Avanzato di Assistenza agli Scacchi)
// @name:ko     🏆 [#1 Chess Cheat] A.C.A.S (고급 체스 보조 시스템)
// @name:nl     🏆 [#1 Chess Cheat] A.C.A.S (Geavanceerd Schaakondersteuningssysteem)
// @name:pl     🏆 [#1 Chess Cheat] A.C.A.S (Zaawansowany System Pomocy Szachowej)
// @name:tr     🏆 [#1 Chess Cheat] A.C.A.S (Gelişmiş Satranç Yardım Sistemi)
// @name:vi     🏆 [#1 Chess Cheat] A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao)
// @name:uk     🏆 [#1 Chess Cheat] A.C.A.S (Система передової допомоги в шахах)
// @name:ru     🏆 [#1 Chess Cheat] A.C.A.S (Система расширенной помощи в шахматах)
// @description        Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
// @description:en     Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
// @description:fi     Paranna shakkipelisi suorituskykyä huippuluokan reaaliaikaisen siirtoanalyysin ja strategisen avustusjärjestelmän avulla
// @description:sw     Förbättra dina schackprestationer med ett banbrytande rörelseanalys i realtid och strategiassistans
// @description:zh-CN  利用尖端实时走法分析和策略辅助系统,提升您的国际象棋水平
// @description:es     Mejora tu rendimiento en ajedrez con un sistema de análisis de movimientos en tiempo real y asistencia estratégica de vanguardia
// @description:hi     अपने शतरंज प्रदर्शन को उन्नत करें, एक कटिंग-एज रियल-टाइम मूव विश्लेषण और रणनीति सहायता प्रणाली के साथ
// @description:ar     قم بتحسين أداءك في الشطرنج مع تحليل حركات اللعب في الوقت الحقيقي ونظام مساعدة استراتيجية حديث
// @description:pt     Melhore seu desempenho no xadrez com uma análise de movimentos em tempo real e um sistema avançado de assistência estratégica
// @description:ja     最新のリアルタイムのムーブ分析と戦略支援システムでチェスのパフォーマンスを向上させましょう
// @description:de     Verbessern Sie Ihre Schachleistung mit einer hochmodernen Echtzeitzug-Analyse- und Strategiehilfe-System
// @description:fr     Améliorez vos performances aux échecs avec une analyse de mouvement en temps réel de pointe et un système d'assistance stratégique
// @description:it     Migliora le tue prestazioni agli scacchi con un sistema all'avanguardia di analisi dei movimenti in tempo reale e assistenza strategica
// @description:ko     최첨단 실시간 움직임 분석 및 전략 지원 시스템으로 체스 성과 향상
// @description:nl     Verbeter je schaakprestaties met een geavanceerd systeem voor realtime zetanalyse en strategische ondersteuning
// @description:pl     Popraw swoje osiągnięcia w szachach dzięki zaawansowanemu systemowi analizy ruchów w czasie rzeczywistym i wsparciu strategicznemu
// @description:tr     Keskinleşmiş gerçek zamanlı hareket analizi ve strateji yardım sistemiyle satranç performansınızı artırın
// @description:vi     Nâng cao hiệu suất cờ vua của bạn với hệ thống phân tích nước đi và hỗ trợ chiến thuật hiện đại
// @description:uk     Покращуйте свою шахову гру з використанням передової системи аналізу ходів в режимі реального часу та стратегічної підтримки
// @description:ru     Слава Украине
// @homepageURL https://psyyke.github.io/A.C.A.S
// @supportURL  https://github.com/Psyyke/A.C.A.S/tree/main#why-doesnt-it-work
// @match       https://psyyke.github.io/A.C.A.S/*
// @match       http://localhost/*
// @match       https://www.chess.com/*
// @match       https://lichess.org/*
// @match       https://playstrategy.org/*
// @match       https://www.pychess.org/*
// @match       https://chess.org/*
// @match       https://papergames.io/*
// @match       https://vole.wtf/kilobytes-gambit/
// @match       https://chess.coolmath-games.com/*
// @match       https://www.coolmathgames.com/0-chess/*
// @match       https://immortal.game/*
// @match       https://chessarena.com/*
// @match       http://chess.net/*
// @match       https://www.freechess.club/*
// @match       https://*chessclub.com/*
// @match       https://gameknot.com/*
// @match       https://chesstempo.com/*
// @match       https://www.redhotpawn.com/*
// @match       https://www.chessanytime.com/*
// @match       https://www.simplechess.com/*
// @match       https://chessworld.net/*
// @match       https://app.edchess.io/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_registerMenuCommand
// @grant       GM_openInTab
// @grant       GM_addStyle
// @grant       GM_setClipboard
// @grant       GM_notification
// @grant       unsafeWindow
// @run-at      document-start
// @require     https://greasyfork.org/scripts/470418-commlink-js/code/CommLinkjs.js
// @require     https://greasyfork.org/scripts/470417-universalboarddrawer-js/code/UniversalBoardDrawerjs.js
// @icon        https://raw.githubusercontent.com/Psyyke/A.C.A.S/main/assets/images/grey-logo.png
// @version     2.2.2
// @namespace   HKR
// @author      HKR
// @license     GPL-3.0
// ==/UserScript==

/*
     e            e88~-_            e           ,d88~~\
    d8b          d888   \          d8b          8888
   /Y88b         8888             /Y88b         `Y88b
  /  Y88b        8888            /  Y88b         `Y88b,
 /____Y88b  d88b Y888   / d88b  /____Y88b  d88b    8888
/      Y88b Y88P  "88_-~  Y88P /      Y88b Y88P \__88P'

Advanced Chess Assistance System (A.C.A.S) v2 | Q3 2023

[WARNING]
- Please be advised that the use of A.C.A.S may violate the rules and lead to disqualification or banning from tournaments and online platforms.
- The developers of A.C.A.S and related systems will NOT be held accountable for any consequences resulting from its use.
- We strongly advise to use A.C.A.S only in a controlled environment ethically.

[ADDITIONAL]
- Big fonts created with: https://www.patorjk.com/software/taag/ (Cyberlarge)

JOIN THE DISCUSSION ABOUT USERSCRIPTS IN GENERAL @ https://hakorr.github.io/Userscripts/community/invite ("Userscript Hub")

DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*\
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
//////////////////////////////////////////////////////////////////
DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*/




/*
  ______         _____  ______  _______
 |  ____ |      |     | |_____] |_____| |
 |_____| |_____ |_____| |_____] |     | |_____


Code below this point runs on any site, including the GUI.
*/

// KEEP THESE AS FALSE ON PRODUCTION
const debugModeActivated = false;
const onlyUseDevelopmentBackend = false;

const domain = window.location.hostname.replace('www.', '');
const greasyforkURL = 'https://greasyfork.org/en/scripts/459137';

const backendConfig = {
    'hosts': { 'prod': 'psyyke.github.io', 'dev': 'localhost' },
    'path': '/A.C.A.S/'
};

const currentBackendUrlKey = 'currentBackendURL';

const isBackendUrlUpToDate = Object.values(backendConfig.hosts).some(x => GM_getValue(currentBackendUrlKey)?.includes(x));

function constructBackendURL(host) {
    const protocol = window.location.protocol + '//';
    const hosts = backendConfig.hosts;

    return protocol + (host || (hosts?.prod || hosts?.path)) + backendConfig.path;
}

function isRunningOnBackend() {
    const hostsArr = Object.values(backendConfig.hosts);

    const foundHost = hostsArr.find(host => host === window?.location?.host);
    const isCorrectPath = window?.location?.pathname?.includes(backendConfig.path);

    const isBackend = typeof foundHost === 'string' && isCorrectPath;

    if(isBackend) {
        GM_setValue(currentBackendUrlKey, constructBackendURL(foundHost));

        return true;
    }

    return false;
}

function prependProtocolWhenNeeded(url) {
    if(!url.startsWith('http://') && !url.startsWith('https://')) {
        return 'http://' + url;
    }

    return url;
}

function getCurrentBackendURL(skipGmStorage) {
    if(onlyUseDevelopmentBackend) {
        return constructBackendURL(backendConfig.hosts?.dev);
    }

    const gmStorageUrl = GM_getValue(currentBackendUrlKey);

    if(skipGmStorage || !gmStorageUrl) {
        return constructBackendURL();
    }

    return prependProtocolWhenNeeded(gmStorageUrl);
}

if(!isBackendUrlUpToDate) {
    GM_setValue(currentBackendUrlKey, getCurrentBackendURL(true));
}

function createInstanceVariable(dbValue) {
    return {
        set: (instanceID, value) => GM_setValue(dbValues[dbValue](instanceID), { value, 'date': Date.now() }),
        get: instanceID => {
            const data = GM_getValue(dbValues[dbValue](instanceID));

            if(data?.date) {
                data.date = Date.now();

                GM_setValue(dbValues[dbValue](instanceID), data);
            }

            return data?.value;
        }
    }
}

const tempValueIndicator = '-temp-value-';
const dbValues = {
    AcasConfig: 'AcasConfig',
    playerColor: instanceID => 'playerColor' + tempValueIndicator + instanceID,
    turn: instanceID => 'turn' + tempValueIndicator + instanceID,
    fen: instanceID => 'fen' + tempValueIndicator + instanceID
};

const instanceVars = {
    playerColor: createInstanceVariable('playerColor'),
    fen: createInstanceVariable('fen')
};

if(isRunningOnBackend()) {
    // expose variables and functions
    unsafeWindow.USERSCRIPT = {
        'GM_info': GM_info,
        'GM_getValue': val => GM_getValue(val),
        'GM_setValue': (val, data) => GM_setValue(val, data),
        'GM_deleteValue': val => GM_deleteValue(val),
        'GM_listValues': val => GM_listValues(val),
        'tempValueIndicator': tempValueIndicator,
        'dbValues': dbValues,
        'instanceVars': instanceVars,
        'CommLinkHandler': CommLinkHandler,
    };

    return;
}






/*
 _______ _     _ _______ _______ _______      _______ _____ _______ _______ _______
 |       |_____| |______ |______ |______      |______   |      |    |______ |______
 |_____  |     | |______ ______| ______|      ______| __|__    |    |______ ______|


Code below this point only runs on chess sites, not on the GUI itself.
*/

function getUniqueID() {
    return ([1e7]+-1e3+4e3+-8e3+-1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
    )
}

const commLinkInstanceID = getUniqueID();

const blacklistedURLs = [
    constructBackendURL(backendConfig?.hosts?.prod),
    constructBackendURL(backendConfig?.hosts?.dev),
    'https://www.chess.com/play',
    'https://lichess.org/',
    'https://chess.org/',
    'https://papergames.io/en/chess',
    'https://playstrategy.org/',
    'https://www.pychess.org/',
    'https://www.coolmathgames.com/0-chess',
    'https://chess.net/'
];

const configKeys = {
    'engineElo': 'engineElo',
    'moveSuggestionAmount': 'moveSuggestionAmount',
    'arrowOpacity': 'arrowOpacity',
    'displayMovesOnExternalSite': 'displayMovesOnExternalSite',
    'showMoveGhost': 'showMoveGhost',
    'showOpponentMoveGuess': 'showOpponentMoveGuess',
    'showOpponentMoveGuessConstantly': 'showOpponentMoveGuessConstantly',
    'onlyShowTopMoves': 'onlyShowTopMoves',
    'maxMovetime': 'maxMovetime',
    'chessVariant': 'chessVariant',
    'chessEngine': 'chessEngine',
    'lc0Weight': 'lc0Weight',
    'engineNodes': 'engineNodes',
    'chessFont': 'chessFont',
    'useChess960': 'useChess960',
    'onlyCalculateOwnTurn': 'onlyCalculateOwnTurn',
    'ttsVoiceEnabled': 'ttsVoiceEnabled',
    'ttsVoiceName': 'ttsVoiceName',
    'ttsVoiceSpeed': 'ttsVoiceSpeed',
    'chessEngineProfile': 'chessEngineProfile',
    'primaryArrowColorHex': 'primaryArrowColorHex',
    'secondaryArrowColorHex': 'secondaryArrowColorHex',
    'opponentArrowColorHex': 'opponentArrowColorHex',
    'reverseSide': 'reverseSide'
};

const config = {};

Object.values(configKeys).forEach(key => {
    config[key] = {
        get:  profile => getGmConfigValue(key, commLinkInstanceID, profile),
        set: null
    };
});

let BoardDrawer = null;
let chessBoardElem = null;
let chesscomVariantPlayerColorsTable = null;
let activeGuiMoveMarkings = [];

let lastBoardRanks = null;
let lastBoardFiles = null;

let lastBoardSize = null;
let lastPieceSize = null;

let lastBoardOrientation = null;

let isUserMouseDown = false;

const supportedSites = {};

const pieceNameToFen = {
    'pawn': 'p',
    'knight': 'n',
    'bishop': 'b',
    'rook': 'r',
    'queen': 'q',
    'king': 'k'
};

function getArrowStyle(type, fill, opacity) {
    const baseStyleArr = [
        'stroke: rgb(0 0 0 / 50%);',
        'stroke-width: 2px;',
        'stroke-linejoin: round;'
    ];

    switch(type) {
        case 'best': 
            return [
                `fill: ${fill || 'limegreen'};`,
                `opacity: ${opacity || 0.9};`,
                ...baseStyleArr
            ].join('\n');
        case 'secondary': 
            return [
                ...baseStyleArr,
                `fill: ${fill ? fill : 'dodgerblue'};`,
                `opacity: ${opacity || 0.7};`,
            ].join('\n');
        case 'opponent':
            return [
                ...baseStyleArr,
                `fill: ${fill ? fill : 'crimson'};`,
                `opacity: ${opacity || 0.3};`,
            ].join('\n');
    }
};

const CommLink = new CommLinkHandler(`frontend_${commLinkInstanceID}`, {
    'singlePacketResponseWaitTime': 1500,
    'maxSendAttempts': 3,
    'statusCheckInterval': 1,
    'silentMode': true
});

// manually register a command so that the variables are dynamic
CommLink.commands['createInstance'] = async () => {
    return await CommLink.send('mum', 'createInstance', {
        'domain': domain,
        'instanceID': commLinkInstanceID,
        'chessVariant': getChessVariant(),
        'playerColor': getPlayerColorVariable()
    });
}

CommLink.registerSendCommand('ping', { commlinkID: 'mum', data: 'ping' });
CommLink.registerSendCommand('pingInstance', { data: 'ping' });
CommLink.registerSendCommand('log');
CommLink.registerSendCommand('updateBoardOrientation');
CommLink.registerSendCommand('updateBoardFen');
CommLink.registerSendCommand('calculateBestMoves');

CommLink.registerListener(`backend_${commLinkInstanceID}`, packet => {
    try {
        switch(packet.command) {
            case 'ping':
                return `pong (took ${Date.now() - packet.date}ms)`;
            case 'getFen':
                return getFen();
            case 'removeSiteMoveMarkings':
                boardUtils.removeMarkings();
                return true;
            case 'markMoveToSite':
                boardUtils.markMoves(packet.data);
                return true;
        }
    } catch(e) {
        return null;
    }
});

const boardUtils = {
    markMoves: moveObjArr => {
        const maxScale = 1;
        const minScale = 0.5;
        const totalRanks = moveObjArr.length;

        moveObjArr.forEach((markingObj, idx) => {
            const profile = markingObj.profile;

            if(idx === 0)
                boardUtils.removeMarkings(profile);

            const [from, to] = markingObj.player;
            const [oppFrom, oppTo] = markingObj.opponent;
            const oppMovesExist = oppFrom && oppTo;
            const rank = idx + 1;

            const showOpponentMoveGuess = getConfigValue(configKeys.showOpponentMoveGuess, profile);
            const showOpponentMoveGuessConstantly = getConfigValue(configKeys.showOpponentMoveGuessConstantly, profile);

            const arrowOpacity = getConfigValue(configKeys.arrowOpacity, profile) / 100;
            const primaryArrowColorHex = getConfigValue(configKeys.primaryArrowColorHex, profile);
            const secondaryArrowColorHex = getConfigValue(configKeys.secondaryArrowColorHex, profile);
            const opponentArrowColorHex = getConfigValue(configKeys.opponentArrowColorHex, profile);

            let playerArrowElem = null;
            let oppArrowElem = null;
            let arrowStyle = getArrowStyle('best', primaryArrowColorHex, arrowOpacity);
            let lineWidth = 30;
            let arrowheadWidth = 80;
            let arrowheadHeight = 60;
            let startOffset = 30;

            if(idx !== 0) {
                arrowStyle = getArrowStyle('secondary', secondaryArrowColorHex, arrowOpacity);

                const arrowScale = totalRanks === 2
                    ? 0.75
                    : maxScale - (maxScale - minScale) * ((rank - 1) / (totalRanks - 1));

                lineWidth = lineWidth * arrowScale;
                arrowheadWidth = arrowheadWidth * arrowScale;
                arrowheadHeight = arrowheadHeight * arrowScale;
                startOffset = startOffset;
            }

            playerArrowElem = BoardDrawer.createShape('arrow', [from, to],
                {
                    style: arrowStyle,
                    lineWidth, arrowheadWidth, arrowheadHeight, startOffset
                }
            );

            if(oppMovesExist && showOpponentMoveGuess) {
                oppArrowElem = BoardDrawer.createShape('arrow', [oppFrom, oppTo],
                    {
                        style: getArrowStyle('opponent', opponentArrowColorHex, arrowOpacity),
                        lineWidth, arrowheadWidth, arrowheadHeight, startOffset
                    }
                );

                if(showOpponentMoveGuessConstantly) {
                    oppArrowElem.style.display = 'block';
                } else {
                    const squareListener = BoardDrawer.addSquareListener(from, type => {
                        if(!oppArrowElem) {
                            squareListener.remove();
                        }

                        switch(type) {
                            case 'enter':
                                oppArrowElem.style.display = 'inherit';
                                break;
                            case 'leave':
                                oppArrowElem.style.display = 'none';
                                break;
                        }
                    });
                }
            }

            if(idx === 0 && playerArrowElem) {
                const parentElem = playerArrowElem.parentElement;

                // move best arrow element on top (multiple same moves can hide the best move)
                parentElem.appendChild(playerArrowElem);

                if(oppArrowElem) {
                    parentElem.appendChild(oppArrowElem);
                }
            }

            activeGuiMoveMarkings.push({ ...markingObj, playerArrowElem, oppArrowElem, profile });
        });
    },
    removeMarkings: profile => {
        let removalArr = activeGuiMoveMarkings;

        if(profile) {
            removalArr = removalArr.filter(obj => obj.profile === profile);

            activeGuiMoveMarkings =  activeGuiMoveMarkings.filter(obj => obj.profile !== profile);
        } else {
            activeGuiMoveMarkings = [];
        }

        removalArr.forEach(markingObj => {
            markingObj.oppArrowElem?.remove();
            markingObj.playerArrowElem?.remove();
        });
    },
    setBoardOrientation: orientation => {
        if(BoardDrawer) {
            if(debugModeActivated) console.warn('setBoardOrientation', orientation);

            BoardDrawer.setOrientation(orientation);
        }
    },
    setBoardDimensions: dimensionArr => {
        if(BoardDrawer) {
            if(debugModeActivated) console.warn('setBoardDimensions', dimensionArr);

            BoardDrawer.setBoardDimensions(dimensionArr);
        }
    }
};

function displayImportantNotification(title, text) {
    if(typeof GM_notification === 'function') {
        GM_notification({ title: title, text: text });
    } else {
        alert(`[${title}]` + '\n\n' + text);
    }
}

function filterInvisibleElems(elementArr, inverse) {
    return [...elementArr].filter(elem => {
        const style = getComputedStyle(elem);
        const bounds = elem.getBoundingClientRect();

        const isHidden =
            style.visibility === 'hidden' ||
            style.display === 'none' ||
            style.opacity === '0' ||
            bounds.width == 0 ||
            bounds.height == 0;

        return inverse ? isHidden : !isHidden;
    });
}

function getElementSize(elem) {
    const rect = elem.getBoundingClientRect();

    if(rect.width !== 0 && rect.height !== 0) {
        return { width: rect.width, height: rect.height };
    }

    const computedStyle = window.getComputedStyle(elem);
    const width = parseFloat(computedStyle.width);
    const height = parseFloat(computedStyle.height);

    return { width, height };
}

function extractElemTransformData(elem) {
    const computedStyle = window.getComputedStyle(elem);
    const transformMatrix = new DOMMatrix(computedStyle.transform);

    const x = transformMatrix.e;
    const y = transformMatrix.f;

    return [x, y];
}

function getElemCoordinatesFromTransform(elem, config) {
    const onlyFlipX = config?.onlyFlipX;
    const onlyFlipY = config?.onlyFlipY;

    lastBoardSize = getElementSize(chessBoardElem);

    const [files, ranks] = getBoardDimensions();

    lastBoardRanks = ranks;
    lastBoardFiles = files;

    const boardOrientation = getPlayerColorVariable();

    let [x, y] = extractElemTransformData(elem);

    const boardDimensions = lastBoardSize;
    let squareDimensions = boardDimensions.width / lastBoardRanks;

    const normalizedX = Math.round(x / squareDimensions);
    const normalizedY = Math.round(y / squareDimensions);

    if(onlyFlipY || boardOrientation === 'w') {
        const flippedY = lastBoardFiles - normalizedY - 1;

        return [normalizedX, flippedY];
    } else {
        const flippedX = lastBoardRanks - normalizedX - 1;

        return [flippedX, normalizedY];
    }
}

function getElemCoordinatesFromLeftBottomPercentages(elem) {
    if(!lastBoardRanks || !lastBoardFiles) {
        const [files, ranks] = getBoardDimensions();

        lastBoardRanks = ranks;
        lastBoardFiles = files;
    }

    const boardOrientation = getPlayerColorVariable();

    const leftPercentage = parseFloat(elem.style.left?.replace('%', ''));
    const bottomPercentage = parseFloat(elem.style.bottom?.replace('%', ''));

    const x = Math.max(Math.round(leftPercentage / (100 / lastBoardRanks)), 0);
    const y = Math.max(Math.round(bottomPercentage / (100 / lastBoardFiles)), 0);

    if (boardOrientation === 'w') {
        return [x, y];
    } else {
        const flippedX = lastBoardRanks - (x + 1);
        const flippedY = lastBoardFiles - (y + 1);

        return [flippedX, flippedY];
    }
}

function getElemCoordinatesFromLeftTopPixels(elem) {
    const pieceSize = getElementSize(elem);

    const leftPixels = parseFloat(elem.style.left?.replace('px', ''));
    const topPixels = parseFloat(elem.style.top?.replace('px', ''));

    const x = Math.max(Math.round(leftPixels / pieceSize.width), 0);
    const y = Math.max(Math.round(topPixels / pieceSize.width), 0);

    const boardOrientation = getPlayerColorVariable();

    if (boardOrientation === 'w') {
        const flippedY = lastBoardFiles - (y + 1);

        return [x, flippedY];
    } else {
        const flippedX = lastBoardRanks - (x + 1);

        return [flippedX, y];
    }
}

function updateChesscomVariantPlayerColorsTable() {
    let colors = [];

    document.querySelectorAll('*[data-color]').forEach(pieceElem => {
        const colorCode = Number(pieceElem?.dataset?.color);

        if(!colors?.includes(colorCode)) {
            colors.push(colorCode);
        }
    });

    if(colors?.length > 1) {
        colors = colors.sort((a, b) => a - b);

        chesscomVariantPlayerColorsTable = { [colors[0]]: 'w', [colors[1]]: 'b' };
    }
}

function getBoardDimensionsFromSize() {
    const boardDimensions = getElementSize(chessBoardElem);

    lastBoardSize = getElementSize(chessBoardElem);

    const boardWidth = boardDimensions?.width;
    const boardHeight = boardDimensions.height;

    const boardPiece = getPieceElem();

    if(boardPiece) {
        const pieceDimensions = getElementSize(boardPiece);

        lastPieceSize = getElementSize(boardPiece);

        const boardPieceWidth = pieceDimensions?.width;
        const boardPieceHeight = pieceDimensions?.height;

        const boardRanks = Math.floor(boardWidth / boardPieceWidth);
        const boardFiles = Math.floor(boardHeight / boardPieceHeight);

        const ranksInAllowedRange = 0 < boardRanks && boardRanks <= 69;
        const filesInAllowedRange = 0 < boardFiles && boardFiles <= 69;

        if(ranksInAllowedRange && filesInAllowedRange) {
            return [boardRanks, boardFiles];
        }
    }
}

function chessCoordinatesToIndex(coord) {
    const x = coord.charCodeAt(0) - 97;
    let y = null;

    const lastHalf = coord.slice(1);

    if(lastHalf === ':') {
        y = 9;
    } else {
        y = Number(coord.slice(1)) - 1;
    }

    return [x, y];
}

function getGmConfigValue(key, instanceID, profileID) {
    if(typeof profileID === 'object') {
        profileID = profileID.name;
    }

    const config = GM_getValue(dbValues.AcasConfig);

    const instanceValue = config?.instance?.[instanceID]?.[key];
    const globalValue = config?.global?.[key];

    if(instanceValue !== undefined) {
        return instanceValue;
    }

    if(globalValue !== undefined) {
        return globalValue;
    }

    if(profileID) {
        const globalProfileValue = config?.global?.['profiles']?.[profileID]?.[key];
        const instanceProfileValue = config?.instance?.[instanceID]?.['profiles']?.[profileID]?.[key];

        if(instanceProfileValue !== undefined) {
            return instanceProfileValue;
        }

        if(globalProfileValue !== undefined) {
            return globalProfileValue;
        }
    }

    return null;
}

function isBoardDrawerNeeded() {
    const config = GM_getValue(dbValues.AcasConfig);

    const gP = config?.global?.['profiles'];
    const iP = config?.instance?.[commLinkInstanceID]?.['profiles'];

    if(gP) {
        const globalProfiles = Object.keys(gP);

        for(profileName of globalProfiles) {
            if(gP[profileName][configKeys.displayMovesOnExternalSite]) {
                return true;
            }
        }
    }

    if(iP) {
        const instanceProfiles = Object.keys(iP);

        for(profileName of instanceProfiles) {
            if(iP[profileName][configKeys.displayMovesOnExternalSite]) {
                return true;
            }
        }
    }

    return false;
}

function getConfigValue(key, profile) {
    return config[key]?.get(profile);
}

function setConfigValue(key, val) {
    return config[key]?.set(val);
}

function squeezeEmptySquares(fenStr) {
    return fenStr.replace(/1+/g, match => match.length);
}

function getPlayerColorVariable() {
    return instanceVars.playerColor.get(commLinkInstanceID);
}

function getFenPieceColor(pieceFenStr) {
    return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b';
}

function getFenPieceOppositeColor(pieceFenStr) {
    return getFenPieceColor(pieceFenStr) == 'w' ? 'b' : 'w';
}

function convertPieceStrToFen(str) {
    if(!str || str.length !== 2) {
        return null;
    }

    const firstChar = str[0].toLowerCase();
    const secondChar = str[1];

    if(firstChar === 'w') {
        return secondChar.toUpperCase();
    } else if (firstChar === 'b') {
        return secondChar.toLowerCase();
    }

    return null;
}

function getCanvasPixelColor(canvas, [xPercentage, yPercentage], debug) {
    const ctx = canvas.getContext('2d');

    const x = xPercentage * canvas.width;
    const y = yPercentage * canvas.height;

    const imageData = ctx.getImageData(x, y, 1, 1);
    const pixel = imageData.data;
    const brightness = (pixel[0] + pixel[1] + pixel[2]) / 3;

    if(debug) {
        const clonedCanvas = document.createElement('canvas');
              clonedCanvas.width = canvas.width;
              clonedCanvas.height = canvas.height;

        const clonedCtx = clonedCanvas.getContext('2d');
              clonedCtx.drawImage(canvas, 0, 0);

        clonedCtx.fillStyle = 'red';
        clonedCtx.beginPath();
        clonedCtx.arc(x, y, 1, 0, Math.PI * 2);
        clonedCtx.fill();

        const dataURL = clonedCanvas.toDataURL();

        console.log(canvas, pixel, dataURL);
    }

    return brightness < 128 ? 'b' : 'w';
}

function canvasHasPixelAt(canvas, [xPercentage, yPercentage], debug) {
    xPercentage = Math.min(Math.max(xPercentage, 0), 100);
    yPercentage = Math.min(Math.max(yPercentage, 0), 100);

    const ctx = canvas.getContext('2d');
    const x = xPercentage * canvas.width;
    const y = yPercentage * canvas.height;

    const imageData = ctx.getImageData(x, y, 1, 1);
    const pixel = imageData.data;

    if(debug) {
        const clonedCanvas = document.createElement('canvas');
              clonedCanvas.width = canvas.width;
              clonedCanvas.height = canvas.height;

        const clonedCtx = clonedCanvas.getContext('2d');
              clonedCtx.drawImage(canvas, 0, 0);

        clonedCtx.fillStyle = 'red';
        clonedCtx.beginPath();
        clonedCtx.arc(x, y, 1, 0, Math.PI * 2);
        clonedCtx.fill();

        const dataURL = clonedCanvas.toDataURL();

        console.log(canvas, pixel, dataURL);
    }

    return pixel[3] !== 0;
}

function getSiteData(dataType, obj) {
    const pathname = window.location.pathname;

    let dataObj = { pathname };

    if(obj && typeof obj === 'object') {
        dataObj = { ...dataObj, ...obj };
    }

    const dataHandlerFunction = supportedSites[domain]?.[dataType];

    if(typeof dataHandlerFunction !== 'function') {
        return null;
    }

    const result = dataHandlerFunction(dataObj);

    //if(debugModeActivated) console.warn('GET_SITE_DATA', '| DATA_TYPE:', dataType, '| INPUT_OBJ:', obj, '| DATA_OBJ:', dataObj, '| RESULT:', result);

    return result;
}

function addSupportedChessSite(domain, typeHandlerObj) {
    supportedSites[domain] = typeHandlerObj;
}

function getBoardElem() {
    const boardElem = getSiteData('boardElem');

    return boardElem || null;
}

function getPieceElem(getAll) {
    const boardElem = getBoardElem();

    const boardQuerySelector = (getAll ? query => [...boardElem?.querySelectorAll(query)] : boardElem?.querySelector?.bind(boardElem));

    if(typeof boardQuerySelector !== 'function')
        return null;

    const pieceElem = getSiteData('pieceElem', { boardQuerySelector, getAll });

    return pieceElem || null;
}

function getSquareElems(element) {
    const squareElems = getSiteData('squareElems', { element });

    return squareElems || null;
}

function getChessVariant() {
    const chessVariant = getSiteData('chessVariant');

    return chessVariant || null;
}

function getBoardOrientation() {
    const boardOrientation = getSiteData('boardOrientation');

    return boardOrientation || null;
}

function getPieceElemFen(pieceElem) {
    const pieceFen = getSiteData('pieceElemFen', { pieceElem });

    return pieceFen || null;
}

// this function gets called a lot, needs to be optimized
function getPieceElemCoords(pieceElem) {
    const pieceCoords = getSiteData('pieceElemCoords', { pieceElem });

    return pieceCoords || null;
}

function getBoardDimensions() {
    const boardDimensionArr = getSiteData('boardDimensions');

    if(boardDimensionArr) {
        lastBoardRanks = boardDimensionArr[0];
        lastBoardFiles = boardDimensionArr[1];

        return boardDimensionArr;
    } else {
        lastBoardRanks = 8;
        lastBoardFiles = 8;

        return [8, 8];
    }
}

function isMutationNewMove(mutationArr) {
    const isNewMove = getSiteData('isMutationNewMove', { mutationArr });

    return isNewMove || false;
}

function getFen(onlyBasic) {
    const [boardRanks, boardFiles] = getBoardDimensions();

    if(debugModeActivated) console.warn('getFen()', 'onlyBasic:', onlyBasic, 'Ranks:', boardRanks, 'Files:', boardFiles);

    const board = Array.from({ length: boardFiles }, () => Array(boardRanks).fill(1));

    function getBasicFen() {
        const pieceElems = getPieceElem(true);
        const isValidPieceElemsArray = Array.isArray(pieceElems) || pieceElems instanceof NodeList;

        if(isValidPieceElemsArray) {
            pieceElems.forEach(pieceElem => {
                const pieceFenCode = getPieceElemFen(pieceElem);
                const pieceCoordsArr = getPieceElemCoords(pieceElem);

                if(debugModeActivated) console.warn('pieceElem', pieceElem, 'pieceFenCode', pieceFenCode, 'pieceCoordsArr', pieceCoordsArr);

                try {
                    const [xIdx, yIdx] = pieceCoordsArr;

                    board[boardFiles - (yIdx + 1)][xIdx] = pieceFenCode;
                } catch(e) {
                    if(debugModeActivated) console.error(e);
                }
            });
        }

        return squeezeEmptySquares(board.map(x => x.join('')).join('/'));
    }

    function getBoardPiece(fenCoord) {
        const indexArr = chessCoordinatesToIndex(fenCoord);

        return board?.[boardFiles - (indexArr[1] + 1)]?.[indexArr[0]];
    }

    // Works on 8x8 boards only
    function getRights() {
        let rights = '';

        // check for white
        const e1 = getBoardPiece('e1'),
              h1 = getBoardPiece('h1'),
              a1 = getBoardPiece('a1');

        if(e1 == 'K' && h1 == 'R') rights += 'K';
        if(e1 == 'K' && a1 == 'R') rights += 'Q';

        //check for black
        const e8 = getBoardPiece('e8'),
              h8 = getBoardPiece('h8'),
              a8 = getBoardPiece('a8');

        if(e8 == 'k' && h8 == 'r') rights += 'k';
        if(e8 == 'k' && a8 == 'r') rights += 'q';

        return rights ? rights : '-';
    }

    const basicFen = getBasicFen();

    if(debugModeActivated) console.warn('basicFen', basicFen);

    if(onlyBasic) {
        return basicFen;
    }

    // FEN structure: [fen] [player color] [castling rights] [en passant targets] [halfmove clock] [fullmove clock]
    const fullFen = `${basicFen} ${getPlayerColorVariable()} ${getRights()} - 0 1`;

    return fullFen;
}

function resetCachedValues() {
    chesscomVariantPlayerColorsTable = null;
}

function fenToArray(fen) {
    const rows = fen.split('/');
    const board = [];

    for (let row of rows) {
        const boardRow = [];
        for (let char of row) {
            if (isNaN(char)) {
                boardRow.push(char);
            } else {
                boardRow.push(...Array(parseInt(char)).fill(''));
            }
        }
        board.push(boardRow);
    }

    return board;
}

function onNewMove(mutationArr, bypassFenChangeDetection) {
    const currentFullFen = getFen();
    const lastFullFen = instanceVars.fen.get(commLinkInstanceID);

    const fenChanged = currentFullFen !== lastFullFen;

    if((fenChanged || bypassFenChangeDetection)) {
        if(debugModeActivated) console.warn('NEW MOVE DETECTED!');

        resetCachedValues();

        boardUtils.setBoardDimensions(getBoardDimensions());

        const lastPlayerColor = getPlayerColorVariable();

        updatePlayerColor();

        const playerColor = getPlayerColorVariable();
        const orientationChanged = playerColor != lastPlayerColor;

        if(orientationChanged) {
            CommLink.commands.log(`Player color (e.g. board orientation) changed from ${lastPlayerColor} to ${playerColor}!`);

            resetCachedValues();

            CommLink.commands.log(`Turn updated to ${playerColor}!`);
        }

        boardUtils.removeMarkings();

        CommLink.commands.updateBoardFen(currentFullFen);

        if(orientationChanged) {
            CommLink.commands.calculateBestMoves(currentFullFen);
        }
    }
}

function observeNewMoves() {
    let lastProcessedFen = null;

    const boardObserver = new MutationObserver(mutationArr => {
        if(debugModeActivated) console.log(mutationArr);

        if(isMutationNewMove(mutationArr))
        {
            if(debugModeActivated) console.warn('Mutation is a new move:', mutationArr);

            if(domain === 'chess.org')
            {
                setTimeout(() => onNewMove(mutationArr), 250);
            }
            else
            {
                onNewMove(mutationArr);
            }
        }
    });

    boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true });
}

async function updatePlayerColor() {
    const boardOrientation = getBoardOrientation();

    const boardOrientationChanged = lastBoardOrientation !== boardOrientation;
    const boardOrientationDiffers = BoardDrawer && BoardDrawer?.orientation !== boardOrientation;

    if(boardOrientationChanged || boardOrientationDiffers) {
        lastBoardOrientation = boardOrientation;

        instanceVars.playerColor.set(commLinkInstanceID, boardOrientation);

        boardUtils.setBoardOrientation(boardOrientation);

        await CommLink.commands.updateBoardOrientation(boardOrientation);
    }
}






/*
 _______ _____ _______ _______      _______  _____  _______ _______ _____ _______ _____ _______
 |______   |      |    |______      |______ |_____] |______ |         |   |______   |   |
 ______| __|__    |    |______      ______| |       |______ |_____  __|__ |       __|__ |_____

Code below this point handles chess site specific things. (e.g. which element is the board or the pieces)
*/

addSupportedChessSite('chess.com', {
    'boardElem': obj => {
        const pathname = obj.pathname;

        if(pathname?.includes('/variants')) {
            return document.querySelector('.TheBoard-layers');
        }

        return document.querySelector('#board-layout-chessboard > .board');
    },

    'pieceElem': obj => {
        const pathname = obj.pathname;
        const getAll = obj.getAll;

        if(pathname?.includes('/variants')) {
            const filteredPieceElems = filterInvisibleElems(document.querySelectorAll('.TheBoard-layers *[data-piece]'))
                .filter(elem => elem?.dataset?.piece?.toLowerCase() !== 'x');

            return getAll ? filteredPieceElems : filteredPieceElems[0];
        }

        return obj.boardQuerySelector('.piece');
    },

    'squareElems': obj => {
        const pathname = obj.pathname;
        const element = obj.element;

        if(pathname?.includes('/variants')) {
            return [...element.querySelectorAll('.square')];
        }
    },

    'chessVariant': obj => {
        const pathname = obj.pathname;

        if(pathname?.includes('/variants')) {
            const variant = pathname.match(/variants\/([^\/]*)/)?.[1]
                .replaceAll('-chess', '')
                .replaceAll('-', '');

            const replacementTable = {
                'doubles-bughouse': 'bughouse',
                'paradigm-chess30': 'paradigm'
            };

            return replacementTable[variant] || variant;
        }
    },

    'boardOrientation': obj => {
        const pathname = obj.pathname;

        if(pathname?.includes('/variants')) {
            const playerNumberStr = document.querySelector('.playerbox-bottom [data-player]')?.dataset?.player;

            if(!playerNumberStr)
                return 'w';

            return playerNumberStr === '0' ? 'w' : 'b';
        }

        const boardElem = getBoardElem();

        return boardElem?.classList.contains('flipped') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pathname = obj.pathname;
        const pieceElem = obj.pieceElem;

        let pieceColor = null;
        let pieceName = null;

        if(pathname?.includes('/variants')) {
            if(!chesscomVariantPlayerColorsTable) {
                updateChesscomVariantPlayerColorsTable();
            }

            const pieceFenStr = pieceElem?.dataset?.piece;

            pieceColor = chesscomVariantPlayerColorsTable?.[pieceElem?.dataset?.color];
            pieceName = pieceElem?.dataset?.piece;

            if(pieceName?.length > 1) {
                pieceName = pieceName[0];
            }
        } else {
            const pieceStr = [...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/));

            [pieceColor, pieceName] = pieceStr.split('');
        }

        return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
    },

    'pieceElemCoords': obj => {
        const pathname = obj.pathname;
        const pieceElem = obj.pieceElem;

        if(pathname?.includes('/variants')) {
            const coords = getElemCoordinatesFromTransform(pieceElem);

            return coords;
        }

        return pieceElem.classList.toString()
            ?.match(/square-(\d)(\d)/)
            ?.slice(1)
            ?.map(x => Number(x) - 1);
    },

    'boardDimensions': obj => {
        const pathname = obj.pathname;

        if(pathname?.includes('/variants')) {
            const squaresContainerElem = document.querySelector('.TheBoard-squares');

            let ranks = 0;
            let files = 0;

            [...squaresContainerElem.childNodes].forEach((x, i) => {
                const visibleChildElems = filterInvisibleElems([...x.childNodes]);

                if(visibleChildElems?.length > 0) {
                    ranks = ranks + 1;

                    if(visibleChildElems.length > files) {
                        files = visibleChildElems.length;
                    }
                }
            });

            return [ranks, files];
        } else {
            return [8, 8];
        }
    },

    'isMutationNewMove': obj => {
        const pathname = obj.pathname;
        const mutationArr = obj.mutationArr;

        if(pathname?.includes('/variants')) {
            return mutationArr.find(m => m.type === 'childList') ? true : false;
        }

        if(mutationArr.length == 1)
            return false;

        const modifiedHoverSquare = mutationArr.find(m => m?.target?.classList?.contains('hover-square')) ? true : false;
        const modifiedHighlight = mutationArr.find(m => m?.target?.classList?.contains('highlight')) ? true : false;
        const modifiedElemPool = mutationArr.find(m => m?.target?.classList?.contains('element-pool')) ? true : false;

        const isPremove = mutationArr.filter(m => m?.target?.classList?.contains('highlight'))
            .map(x => x?.target?.style?.['background-color'])
            .find(x => x === 'rgb(244, 42, 50)') ? true : false;

        return (
            (mutationArr.length >= 4 && !modifiedHoverSquare)
            || mutationArr.length >= 7
            || modifiedHighlight
            || modifiedElemPool
        ) && !isPremove;
    }
});

addSupportedChessSite('lichess.org', {
    'boardElem': obj => {
        return document.querySelector('cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece:not(.ghost)');
    },

    'chessVariant': obj => {
        const variantLinkElem = document.querySelector('.variant-link');

        if(variantLinkElem) {
            let variant = variantLinkElem?.innerText?.toLowerCase()?.replaceAll(' ', '-');

            const replacementTable = {
                'correspondence': 'chess',
                'koth': 'kingofthehill',
                'three-check': '3check'
            };

            return replacementTable[variant] || variant;
        }
    },

    'boardOrientation': obj => {
        const filesElem = document.querySelector('coords.files');

        return filesElem?.classList?.contains('black') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
        const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));

        if(pieceColor && elemPieceName) {
            const pieceName = pieceNameToFen[elemPieceName];

            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        const key = pieceElem?.cgKey;

        if(key) {
            return chessCoordinatesToIndex(key);
        }
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('playstrategy.org', {
    'boardElem': obj => {
        return document.querySelector('cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece[class*="-piece"]:not(.ghost)');
    },

    'chessVariant': obj => {
        const variantLinkElem = document.querySelector('.variant-link');

        if(variantLinkElem) {
            let variant = variantLinkElem?.innerText
                ?.toLowerCase()
                ?.replaceAll(' ', '-');

            const replacementTable = {
                'correspondence': 'chess',
                'koth': 'kingofthehill',
                'three-check': '3check',
                'five-check': '5check',
                'no-castling': 'nocastle'
            };

            return replacementTable[variant] || variant;
        }
    },

    'boardOrientation': obj => {
        const cgWrapElem = document.querySelector('.cg-wrap');

        return cgWrapElem.classList?.contains('orientation-p1') ? 'w' : 'b';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const playerColor = getPlayerColorVariable();
        const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w');

        let pieceName = null;

        [...pieceElem?.classList]?.forEach(className => {
            if(className?.includes('-piece')) {
                const elemPieceName = className?.split('-piece')?.[0];

                if(elemPieceName && elemPieceName?.length === 1) {
                    pieceName = elemPieceName;
                }
            }
        });

        if(pieceColor && pieceName) {
            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        const key = pieceElem?.cgKey;

        if(key) {
            return chessCoordinatesToIndex(key);
        }
    },

    'boardDimensions': obj => {
        return getBoardDimensionsFromSize();
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('pychess.org', {
    'boardElem': obj => {
        return document.querySelector('cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece[class*="-piece"]:not(.ghost)');
    },

    'chessVariant': obj => {
        const variantLinkElem = document.querySelector('#main-wrap .tc .user-link');

        if(variantLinkElem) {
            let variant = variantLinkElem?.innerText
                ?.toLowerCase()
                ?.replaceAll(' ', '')
                ?.replaceAll('-', '');

            const replacementTable = {
                'correspondence': 'chess',
                'koth': 'kingofthehill',
                'nocastling': 'nocastle',
                'gorogoro+': 'gorogoro',
                'oukchaktrang': 'cambodian'
            };

            return replacementTable[variant] || variant;
        }
    },

    'boardOrientation': obj => {
        const cgWrapElem = document.querySelector('.cg-wrap');

        return cgWrapElem.classList?.contains('orientation-black') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const playerColor = getPlayerColorVariable();
        const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w');

        let pieceName = null;

        [...pieceElem?.classList]?.forEach(className => {
            if(className?.includes('-piece')) {
                const elemPieceName = className?.split('-piece')?.[0];

                if(elemPieceName && elemPieceName?.length === 1) {
                    pieceName = elemPieceName;
                }
            }
        });

        if(pieceColor && pieceName) {
            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        const key = pieceElem?.cgKey;

        if(key) {
            return chessCoordinatesToIndex(key);
        }
    },

    'boardDimensions': obj => {
        return getBoardDimensionsFromSize();
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('chess.org', {
    'boardElem': obj => {
        return document.querySelector('.cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece:not(.ghost)');
    },

    'chessVariant': obj => {
        const variantNum = unsafeWindow?.GameConfig?.instance?.variant;
        const variant = GameConfig?.VARIANT_NAMES?.[variantNum]?.toLowerCase();

        if(variant) {
            const replacementTable = {
                'standard': 'chess'
            };

            return replacementTable[variant] || variant;
        }
    },

    'boardOrientation': obj => {
        const filesElem = document.querySelector('coords.files');

        return filesElem?.classList?.contains('black') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
        const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));

        if(pieceColor && elemPieceName) {
            const pieceName = pieceNameToFen[elemPieceName];

            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return getElemCoordinatesFromTransform(pieceElem);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        if(isUserMouseDown) {
            return false;
        }

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('chess.coolmath-games.com', {
    'boardElem': obj => {
        return document.querySelector('.cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece:not(.ghost)');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        const boardElem = getBoardElem();

        return boardElem.classList?.contains('orientation-black') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
        const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));

        if(pieceColor && elemPieceName) {
            const pieceName = pieceNameToFen[elemPieceName];

            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return getElemCoordinatesFromLeftBottomPercentages(pieceElem?.parentElement);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        if(isUserMouseDown) {
            return false;
        }

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('papergames.io', {
    'boardElem': obj => {
        return document.querySelector('#chess_board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('*[data-piece][data-square]');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        const boardElem = getBoardElem();

        if(boardElem) {
            const firstRankText = [...boardElem.querySelector('.coordinates').childNodes]?.[0].textContent;

            return firstRankText == 'h' ? 'b' : 'w';
        }
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        return convertPieceStrToFen(pieceElem?.dataset?.piece);
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        const key = pieceElem?.dataset?.square;

        if(key) {
            return chessCoordinatesToIndex(key);
        }
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 12;
    }
});

addSupportedChessSite('vole.wtf', {
    'boardElem': obj => {
        return document.querySelector('#board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('*[data-t][data-l][data-p]:not([data-p="0"]');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        return 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceNum = Number(pieceElem?.dataset?.p);
        const pieceFenStr = 'pknbrq';

        if(pieceNum > 8) {
            return pieceFenStr[pieceNum - 9].toUpperCase();
        } else {
            return pieceFenStr[pieceNum - 1];
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return [Number(pieceElem?.dataset?.l), 7 - Number(pieceElem?.dataset?.t)];
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 12;
    }
});

addSupportedChessSite('immortal.game', {
    'boardElem': obj => {
        return document.querySelector('div.pawn.relative, div.knight.relative, div.bishop.relative, div.rook.relative, div.queen.relative, div.king.relative')?.parentElement?.parentElement;
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('div.pawn.relative, div.knight.relative, div.bishop.relative, div.rook.relative, div.queen.relative, div.king.relative');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        const coordA = [...document.querySelectorAll('svg text[x]')]
            .find(elem => elem?.textContent == 'a');

        const coordAX = Number(coordA?.getAttribute('x')) || 10;

        return coordAX < 15 ? 'w' : 'b';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
        const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));

        if(pieceColor && elemPieceName) {
            const pieceName = pieceNameToFen[elemPieceName];

            return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return getElemCoordinatesFromTransform(pieceElem?.parentElement);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        if(isUserMouseDown) {
            return false;
        }

        return mutationArr.length >= 5;
    }
});

addSupportedChessSite('chessarena.com', {
    'boardElem': obj => {
        return document.querySelector('*[data-component="GameLayoutBoard"] cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('cg-piece:not(*[style*="visibility: hidden;"])');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        const titlesElem = document.querySelector('cg-titles');

        return titlesElem?.classList?.contains('rotated') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.className?.[0];
        const elemPieceName = pieceElem?.className?.[1];

        if(pieceColor && elemPieceName) {
            const pieceName = elemPieceName; // pieceNameToFen[elemPieceName]

            return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return getElemCoordinatesFromTransform(pieceElem, { 'onlyFlipY': true });
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        if(isUserMouseDown) {
            return false;
        }

        return mutationArr.find(m => m?.attributeName === 'style') ? true : false;
    }
});


addSupportedChessSite('chess.net', {
    'boardElem': obj => {
        return document.querySelector('cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece:not(.ghost)');
    },

    'chessVariant': obj => {
        const variantLinkElem = document.querySelector('.variant-link');

        if(variantLinkElem) {
            let variant = variantLinkElem?.innerText?.toLowerCase()?.replaceAll(' ', '-');

            const replacementTable = {
                'correspondence': 'chess',
                'koth': 'kingofthehill',
                'three-check': '3check'
            };

            return replacementTable[variant] || variant;
        }
    },

    'boardOrientation': obj => {
        const filesElem = document.querySelector('coords.files');

        return filesElem?.classList?.contains('black') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
        const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));

        if(pieceColor && elemPieceName) {
            const pieceName = pieceNameToFen[elemPieceName];

            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        const key = pieceElem?.cgKey;

        if(key) {
            return chessCoordinatesToIndex(key);
        }
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('freechess.club', {
    'boardElem': obj => {
        return document.querySelector('cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece:not(.ghost)');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        const filesElem = document.querySelector('coords.files');

        return filesElem?.classList?.contains('black') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
        const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));

        if(pieceColor && elemPieceName) {
            const pieceName = pieceNameToFen[elemPieceName];

            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        const key = pieceElem?.cgKey;

        if(key) {
            return chessCoordinatesToIndex(key);
        }
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('play.chessclub.com', {
    'boardElem': obj => {
        return document.querySelector('cg-board');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('piece:not(.ghost)');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        const filesElem = document.querySelector('coords.files');

        return filesElem?.classList?.contains('black') ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
        const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));

        if(pieceColor && elemPieceName) {
            const pieceName = pieceNameToFen[elemPieceName];

            return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        const key = pieceElem?.cgKey;

        if(key) {
            return chessCoordinatesToIndex(key);
        }
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 4
            || mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('gameknot.com', {
    'boardElem': obj => {
        return document.querySelector('#chess-board-acboard');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('*[class*="chess-board-piece"] > img[src*="chess56."][style*="visible"]');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        return document.querySelector('#chess-board-my-side-color .player_white') ? 'w' : 'b';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const left = Number(pieceElem.style.left.replace('px', ''));
        const top = Number(pieceElem.style.top.replace('px', ''));

        const pieceColor = left >= 0 ? 'w' : 'b';
        const pieceName = 'kqrnbp'[(top * -1) / 60];

        return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return getElemCoordinatesFromLeftTopPixels(pieceElem.parentElement);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.find(m => m.type === 'childList') ? true : false
            || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
    }
});

addSupportedChessSite('chesstempo.com', {
    'boardElem': obj => {
        return document.querySelector('.ct-board-squares');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('*[class*="ct-pieceClass"][class*="ct-piece-"]');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        return document.querySelector('.ct-coord-column').innerText === 'a' ? 'w' : 'b';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceNameClass = [...pieceElem.classList].find(x => x?.includes('ct-piece-'));
        const colorNameCombo = pieceNameClass?.split('ct-piece-')?.pop();

        const elemPieceColor = colorNameCombo.startsWith('white') ? 'w' : 'b';
        const elemPieceName = colorNameCombo.substring(5);

        const pieceName = pieceNameToFen[elemPieceName];

        return elemPieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return [pieceElem?.ct?.piece?.piece?.column, pieceElem?.ct?.piece?.piece?.row];
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 4;
    }
});

addSupportedChessSite('redhotpawn.com', {
    'boardElem': obj => {
        return document.querySelector('#board-0_1');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('li.piece[id*="-pc-"]');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        const aCoordLeftStyleNum = Number([...document.querySelectorAll('.boardCoordinate')]
            .find(elem => elem?.innerText === 'a')
            ?.style?.left?.replace('px', ''));

        return aCoordLeftStyleNum < 200 ? 'w' : 'b';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        return (pieceElem?.id?.match(/-pc-(.*?)-/) || [])[1];
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return getElemCoordinatesFromLeftTopPixels(pieceElem);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        if(isUserMouseDown) {
            return false;
        }

        return mutationArr.length >= 4;
    }
});

addSupportedChessSite('simplechess.com', {
    'boardElem': obj => {
        return document.querySelector('#chessboard');
    },

    'pieceElem': obj => {
        const getAll = obj.getAll;

        const pieceElems = [...document.querySelectorAll('canvas.canvas_piece')].filter(elem => canvasHasPixelAt(elem, [0.5, 0.5]));

        return getAll ? pieceElems : pieceElems[0];
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        return document.querySelector('#chessboard_coordy0')?.innerText === '8' ? 'w' : 'b';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const pieceTypeCoordPercentages = [
            { 'name' : 'k', 'coords': [52/60, 26/60] },
            { 'name' : 'q', 'coords': [8/60, 16/60] },
            { 'name' : 'n', 'coords': [51/60, 42/60] },
            { 'name' : 'b', 'coords': [9/60, 50/60] },
            { 'name' : 'r', 'coords': [45/60, 15/60] },
            { 'name' : 'p', 'coords': [0.5, 0.5] }
        ];

        const pieceColorCoordPercentages = {
            'k': [42/60, 27/60],
            'q': [30/60, 50/60],
            'n': [38/60, 41/60],
            'b': [30/60, 20/60]
        };

        let pieceName = null;

        for(obj of pieceTypeCoordPercentages) {
            const isThisPiece = canvasHasPixelAt(pieceElem, obj.coords);

            if(isThisPiece) {
                pieceName = obj.name;

                break;
            }
        }

        if(pieceName) {
            const colorCoords = pieceColorCoordPercentages[pieceName] || [0.5, 0.5];

            const pieceColor = getCanvasPixelColor(pieceElem, colorCoords);

            return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
        }
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return getElemCoordinatesFromLeftTopPixels(pieceElem);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        if(isUserMouseDown) {
            return false;
        }

        return mutationArr.length >= 7;
    }
});

addSupportedChessSite('chessworld.net', {
    'boardElem': obj => {
        return document.querySelector('#ChessWorldChessBoard');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('img[src*="merida"');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        return document.querySelector('div[style*="boardb.jpg"]') ? 'w' : 'b';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;

        const [elemPieceColor, elemPieceName] = pieceElem
            ?.src
            ?.split('/')
            ?.pop()
            ?.replace('.png', '')
            ?.split('_');

        const pieceColor = elemPieceColor === 'white' ? 'w' : 'b';
        const pieceName = pieceNameToFen[elemPieceName];

        return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return chessCoordinatesToIndex(pieceElem?.id);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 2;
    }
});

addSupportedChessSite('app.edchess.io', {
    'boardElem': obj => {
        return document.querySelector('*[data-boardid="chessboard"]');
    },

    'pieceElem': obj => {
        return obj.boardQuerySelector('*[data-piece]');
    },

    'chessVariant': obj => {
        return 'chess';
    },

    'boardOrientation': obj => {
        return document.querySelector('*[data-square]')?.dataset?.square == 'h1' ? 'b' : 'w';
    },

    'pieceElemFen': obj => {
        const pieceElem = obj.pieceElem;
        const [pieceColor, pieceName] = pieceElem?.dataset?.piece?.split('');

        return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
    },

    'pieceElemCoords': obj => {
        const pieceElem = obj.pieceElem;

        return chessCoordinatesToIndex(pieceElem?.parentElement?.parentElement?.dataset?.square);
    },

    'boardDimensions': obj => {
        return [8, 8];
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;

        return mutationArr.length >= 2;
    }
});

/*
 _____ __   _ _____ _______ _____ _______        _____ ______ _______ _______ _____  _____  __   _
   |   | \  |   |      |      |   |_____| |        |    ____/ |_____|    |      |   |     | | \  |
 __|__ |  \_| __|__    |    __|__ |     | |_____ __|__ /_____ |     |    |    __|__ |_____| |  \_|

Code below this point is related to initialization. (e.g. wait for chess board and create the instance)
*/

async function isAcasBackendReady() {
    const res = await CommLink.commands.ping();

    return res ? true : false;
}

async function start() {
    await CommLink.commands.createInstance(commLinkInstanceID);

    const pathname = window.location.pathname;
    const adjustSizeByDimensions = domain === 'chess.com' && pathname?.includes('/variants');
    const ignoreBodyRectLeft = domain === 'app.edchess.io';

    const boardOrientation = getBoardOrientation();

    instanceVars.playerColor.set(commLinkInstanceID, boardOrientation);
    instanceVars.fen.set(commLinkInstanceID, getFen());

    if(isBoardDrawerNeeded()) {
        BoardDrawer = new UniversalBoardDrawer(chessBoardElem, {
            'window': window,
            'boardDimensions': getBoardDimensions(),
            'playerColor': getPlayerColorVariable(),
            'zIndex': domain === 'chessarena.com' ? 9999 : 500,
            'prepend': true,
            'debugMode': debugModeActivated,
            'adjustSizeByDimensions': adjustSizeByDimensions ? true : false,
            'adjustSizeConfig': {
                'noLeftAdjustment': true
            },
            'ignoreBodyRectLeft': ignoreBodyRectLeft
        });
    }

    await updatePlayerColor();

    observeNewMoves();

    CommLink.setIntervalAsync(async () => {
        await CommLink.commands.createInstance(commLinkInstanceID);
    }, 1000);
}

function startWhenBackendReady() {
    let timesUrlForceOpened = 0;

    const interval = CommLink.setIntervalAsync(async () => {
        if(await isAcasBackendReady()) {
            start();

            interval.stop();
        } else if(timesUrlForceOpened < 1) {
            timesUrlForceOpened++;

            GM_openInTab(getCurrentBackendURL(), true);
        }
    }, 1000);
}

function initializeIfSiteReady() {
    const boardElem = getBoardElem();
    const firstPieceElem = getPieceElem();

    const bothElemsExist = boardElem && firstPieceElem;
    const boardElemChanged = chessBoardElem != boardElem;

    if(bothElemsExist && boardElemChanged) {
        chessBoardElem = boardElem;

        chessBoardElem.addEventListener('mousedown', () => { isUserMouseDown = true; });
        chessBoardElem.addEventListener('mouseup', () => { isUserMouseDown = false; });
        chessBoardElem.addEventListener('touchstart', () => { isUserMouseDown = true; });
        chessBoardElem.addEventListener('touchend', () => { isUserMouseDown = false; });


        if(!blacklistedURLs.includes(window.location.href)) {
            startWhenBackendReady();
        }
    }
}

if(typeof GM_registerMenuCommand === 'function') {
    GM_registerMenuCommand('[u] Open GreasyFork Page', e => {
        GM_openInTab(greasyforkURL, true);
    }, 'u');

    GM_registerMenuCommand('[o] Open GUI Manually', e => {
        GM_openInTab(getCurrentBackendURL(), true);
    }, 'o');

    GM_registerMenuCommand('[s] Start Manually', e => {
        if(chessBoardElem) {
            start();
        } else {
            displayImportantNotification('Failed to start manually', 'No chessboard element found!');
        }
    }, 's');

    GM_registerMenuCommand('[g] Get Moves Manually', e => {
        if(chessBoardElem) {
            onNewMove(null, true);
        } else {
            displayImportantNotification('Failed to get moves', 'No chessboard element found!');
        }
    }, 'g');

    GM_registerMenuCommand('[r] Render BoardDrawer Manually', e => {
        if(typeof BoardDrawer?.updateDimensions === 'function') {
            BoardDrawer.updateDimensions();
        } else {
            displayImportantNotification('Failed to render BoardDrawer', 'BoardDrawer not initialized or something else went wrong!');
        }
    }, 'r');

    if(typeof GM_setClipboard === 'function') {
        GM_registerMenuCommand('[c] Copy FEN to Clipboard', e => {
            if(chessBoardElem) {
                GM_setClipboard(getFen());
            } else {
                displayImportantNotification('Failed to get FEN', 'No chessboard element found!');
            }
        }, 'c');
    }
}

setInterval(initializeIfSiteReady, 1000);