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

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        🏆 [#1 Chess Assistant] A.C.A.S (Advanced Chess Assistance System)
// @name:en     🏆 [#1 Chess Assistant] A.C.A.S (Advanced Chess Assistance System)
// @name:fi     🏆 [#1 Chess Assistant] A.C.A.S (Edistynyt shakkiavustusjärjestelmä)
// @name:sw     🏆 [#1 Chess Assistant] A.C.A.S (Advanserad Schack Assitant System)
// @name:zh-CN  🏆 [#1 Chess Assistant] A.C.A.S(高级国际象棋辅助系统)
// @name:es     🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avanzado de Asistencia al Ajedrez)
// @name:hi     🏆 [#1 Chess Assistant] A.C.A.S (उन्नत शतरंज सहायता प्रणाली)
// @name:ar     🏆 [#1 Chess Assistant] A.C.A.S (نظام المساعدة المتقدم في الشطرنج)
// @name:pt     🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avançado de Assistência ao Xadrez)
// @name:ja     🏆 [#1 Chess Assistant] A.C.A.S(先進的なチェス支援システム)
// @name:de     🏆 [#1 Chess Assistant] A.C.A.S (Fortgeschrittenes Schach-Hilfesystem)
// @name:fr     🏆 [#1 Chess Assistant] A.C.A.S (Système Avancé d'Assistance aux Échecs)
// @name:it     🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avanzato di Assistenza agli Scacchi)
// @name:ko     🏆 [#1 Chess Assistant] A.C.A.S (고급 체스 보조 시스템)
// @name:nl     🏆 [#1 Chess Assistant] A.C.A.S (Geavanceerd Schaakondersteuningssysteem)
// @name:pl     🏆 [#1 Chess Assistant] A.C.A.S (Zaawansowany System Pomocy Szachowej)
// @name:tr     🏆 [#1 Chess Assistant] A.C.A.S (Gelişmiş Satranç Yardım Sistemi)
// @name:vi     🏆 [#1 Chess Assistant] A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao)
// @name:uk     🏆 [#1 Chess Assistant] A.C.A.S (Система передової допомоги в шахах)
// @name:ru     🏆 [#1 Chess Assistant] 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://chess.coolmathgames.com/*
// @match       https://www.coolmathgames.com/0-chess/*
// @match       https://immortal.game/*
// @match       https://worldchess.com/*
// @match       http://chess.net/*
// @match       https://chess.net/*
// @match       https://*.freechess.club/*
// @match       https://*.chessclub.com/*
// @match       https://gameknot.com/*
// @match       https://www.chessanytime.com/*
// @match       https://app.edchess.io/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_openInTab
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.deleteValue
// @grant       GM.listValues
// @grant       GM.openInTab
// @grant       GM_registerMenuCommand
// @grant       GM_setClipboard
// @grant       GM_notification
// @grant       unsafeWindow
// @run-at      document-start
// @require     https://update.greasyfork.org/scripts/534637/LegacyGMjs.js?acasv=2
// @require     https://update.greasyfork.org/scripts/470418/CommLinkjs.js?acasv=2
// @require     https://update.greasyfork.org/scripts/470417/UniversalBoardDrawerjs.js?acasv=2
// @icon        https://raw.githubusercontent.com/Psyyke/A.C.A.S/main/assets/images/logo-192.png
// @version     2.3.9
// @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 Y88   / d88b /____Y88b  d88b    8888
/      Y88b  Y88P "88Y-~   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/ (Tmplr)

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*/

(async () => { try { await LOAD_LEGACY_GM_SUPPORT();
/*
┏┓┓ ┏┓┳┓┏┓┓
┃┓┃ ┃┃┣┫┣┫┃
┗┛┗┛┗┛┻┛┛┗┗┛
============
Code below this point runs on any site, including the GUI.
*/

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

const currentBackendUrlKey = 'currentBackendURL';
const currentBackendUrl = typeof GM_getValue === 'function'
    ? GM_getValue(currentBackendUrlKey)
    : await GM.getValue(currentBackendUrlKey);
const isBackendUrlUpToDate = Object.values(backendConfig.hosts)
    .some(x => currentBackendUrl?.includes(x));
const isDevPage = window?.location?.pathname?.includes('/dev');

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

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

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

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

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

    if(isBackend && !skipGM)
        GM_setValue(currentBackendUrlKey, constructBackendURL(foundHost));

    return isBackend;
}

const runningOnBackend = isRunningOnBackend();
const runningOnDevPage = runningOnBackend && isDevPage;

// 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';

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
};
// Add them to acas-userscript-bridge.js as well if you,
// decide to add more variables here
const instanceVars = {
    playerColor: createInstanceVariable('playerColor'),
    turn: createInstanceVariable('turn'),
    fen: createInstanceVariable('fen')
};

function exposeViaMessages() {
    const handlers = {
        USERSCRIPT_getValue: (args, messageId) => {
            const [key] = args;
            const value = GM_getValue(key);
            window.postMessage({ messageId, value }, '*');
        },
        USERSCRIPT_setValue: (args, messageId) => {
            const [key, value] = args;
            GM_setValue(key, value);
            window.postMessage({ messageId, value: true }, '*');
        },
        USERSCRIPT_deleteValue: (args, messageId) => {
            const [key] = args;
            GM_deleteValue(key);
            window.postMessage({ messageId, value: true }, '*');
        },
        USERSCRIPT_listValues: (args, messageId) => {
            const value = GM_listValues();
            window.postMessage({ messageId, value }, '*');
        },
        USERSCRIPT_getInfo: (args, messageId) => {
            const value = typeof GM_info !== 'undefined' ? JSON.parse(JSON.stringify(GM_info)) : {};
            window.postMessage({ messageId, value }, '*');
        },
        USERSCRIPT_instanceVars: (args, messageId) => {
            const [instanceId, key, value] = args;

            if (!instanceVars.hasOwnProperty(key)) {
                window.postMessage({ messageId, value: false }, '*');
                return;
            }

            const result = (value !== undefined)
                ? instanceVars[key].set(instanceId, value)
                : instanceVars[key].get(instanceId);

            window.postMessage({ messageId, value: result }, '*');
        }
    };

    window.addEventListener('message', (event) => {
        const handler = handlers[event.data?.type];
        if(handler) handler(event.data.args, event.data.messageId);
    });

    const script = document.createElement('script');
    script.innerHTML = 'window.isUserscriptActive = true;';

    document.head.appendChild(script);
}

function exposeViaUnsafe() {
    if(typeof unsafeWindow !== 'object') return;

    unsafeWindow.USERSCRIPT = {
        'getValue': val => GM_getValue(val),
        'setValue': (val, data) => GM_setValue(val, data),
        'deleteValue': val => GM_deleteValue(val),
        'listValues': val => GM_listValues(val),
        'instanceVars': instanceVars,
        'getInfo': () => GM_info
    };

    unsafeWindow.isUserscriptActive = true;
}

if(runningOnBackend && !isDevPage) {
    if(typeof unsafeWindow === 'object')
        exposeViaUnsafe();
    else
        exposeViaMessages();

    return;
}

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

┏┓┓┏┏┓┏┓┏┓  ┏┓┳┏┳┓┏┓┏┓
┃ ┣┫┣ ┗┓┗┓  ┗┓┃ ┃ ┣ ┗┓
┗┛┛┗┗┛┗┛┗┛  ┗┛┻ ┻ ┗┛┗┛
======================
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 = Object.freeze([
    'engineElo', 'moveSuggestionAmount', 'arrowOpacity',
    'displayMovesOnExternalSite', 'showMoveGhost', 'showOpponentMoveGuess',
    'showOpponentMoveGuessConstantly', 'onlyShowTopMoves', 'maxMovetime',
    'chessVariant', 'chessEngine', 'lc0Weight',
    'engineNodes', 'chessFont', 'useChess960',
    'onlyCalculateOwnTurn', 'ttsVoiceEnabled', 'ttsVoiceName',
    'ttsVoiceSpeed', 'chessEngineProfile', 'primaryArrowColorHex',
    'secondaryArrowColorHex', 'opponentArrowColorHex', 'reverseSide',
    'engineEnabled', 'autoMove', 'autoMoveLegit',
    'autoMoveRandom', 'autoMoveAfterUser', 'legitModeType',
    'moveDisplayDelay', 'renderSquarePlayer', 'renderSquareEnemy',
    'renderSquareContested', 'renderSquareSafe', 'renderPiecePlayerCapture',
    'renderPieceEnemyCapture', 'renderOnExternalSite', 'feedbackOnExternalSite',
    'enableMoveRatings', 'enableEnemyFeedback', 'feedbackEngineDepth',
    'enableAdvancedElo', 'advancedElo', 'advancedEloDepth',
    'advancedEloSkill', 'advancedEloMaxError', 'advancedEloProbability',
    'advancedEloHash', 'advancedEloThreads', 'moveAsFilledSquares',
    'movesOnDemand', 'onlySuggestPieces', 'isUserscriptGhost'
].reduce((o, k) => (o[k] = k, o), {}));

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 activeMetricRenders = [];
let activeFeedback = [];

let boardObserver = null;
let dumbBoardObservingInterval = null;
let lastMutationObservationDate = 0;

let lastCalculatedFullFen = null;

let lastBoardRanks = null;
let lastBoardFiles = null;

let lastBoardSize = null;
let lastPieceSize = null;

let lastTurn = null;

let lastBoardMatrix = null;
let lastBoardOrientation = null;

let matchFirstSuggestionGiven = false;

let lastMoveRequestTime = 0;

let lastMutationObsProcessedTurn = null;

let isUserMouseDown = false;
let activeAutomoves = [];

let modListeners = [];
let modDrawerListeners = [];
let modLastEnteredSquare = { 'squareIndex': null, 'squareFen': null, 'pieceFen': null };

let isMovesOnDemandActive = false;

const supportedSites = {};

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

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

function getArrowStyle(type, fill, opacity) {
    const getBaseStyleModification = (f, o) => [
        'stroke: rgb(0 0 0 / 50%);',
        'stroke-width: 2px;',
        'stroke-linejoin: round;',
        `fill: ${fill || f};`,
        `opacity: ${opacity || o};`
    ].join('\n');

    switch(type) {
        case 'best':
            return getBaseStyleModification('limegreen', 0.9);
        case 'secondary':
            return getBaseStyleModification('dodgerblue', 0.7);
        case 'opponent':
            return getBaseStyleModification('crimson', 0.3);
    }
};

const CommLink = new CommLinkHandler(`frontend_${commLinkInstanceID}`, {
    'singlePacketResponseWaitTime': 250,
    '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': getBoardOrientation()
    });
}

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.registerSendCommand('calculateSpecificMoves');

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':
                const profile = packet.data?.[0]?.profile;

                boardUtils.removeMarkings(profile);
                boardUtils.markMoves(packet.data);

                const isAutoMove = getConfigValue(configKeys.autoMove, profile);
                const isAutoMoveAfterUser = getConfigValue(configKeys.autoMoveAfterUser, profile);

                if(isAutoMove && (!isAutoMoveAfterUser || matchFirstSuggestionGiven)) {
                    const existingAutomoves = activeAutomoves.filter(x => x.move.active);

                    // Stop all existing automoves
                    for(const x of existingAutomoves) {
                        x.move.stop();
                    }

                    const isLegit = getConfigValue(configKeys.autoMoveLegit, profile);
                    const isRandom = getConfigValue(configKeys.autoMoveRandom, profile);

                    const move = isRandom
                        ? packet.data[Math.floor(Math.random() * Math.random() * packet.data.length)]?.player
                        : packet.data[0]?.player;

                    makeMove(profile, move, isLegit);
                }

                matchFirstSuggestionGiven = true;

                return true;
            case 'renderMetricsToSite':
                renderMetrics(packet.data);
                return true;
            case 'feedbackToSite':
                displayFeedback(packet.data);
                return true;
        }
    } catch(e) {
        return null;
    }
});

function clearMetricRenders() {
    activeMetricRenders.forEach(elem => {
        if(elem) elem?.remove();
    });
}

function renderMetrics(addedMetrics) {
    if(!BoardDrawer) return;

    clearMetricRenders();

    function processMetric(metric) {
        const data = metric?.data;

        if(!data) return;

        const shapeType = data?.shapeType;
        const shapeSquare = data?.shapeSquare;
        const shapeConfig = data?.shapeConfig;

        if(shapeType && shapeSquare && shapeConfig) {
            const shape = BoardDrawer.createShape(shapeType, shapeSquare, shapeConfig);

            activeMetricRenders.push(shape);
        }
    }

    function findMetricByType(type) {
        return addedMetrics.filter(metric => metric?.data?.shapeType === type) || [];
    }

    findMetricByType('text')
        .forEach(processMetric);

    findMetricByType('rectangle')
        .forEach(processMetric);
}

function clearFeedback() {
    activeFeedback.forEach(elem => {
        if(elem) elem?.remove();
    });
}

function displayFeedback(addedFeedback) {
    if(!BoardDrawer) return;

    clearFeedback();

    function processFeedback(feedback) {
        const data = feedback?.data;

        if(!data) return;

        const shapeType = data?.shapeType;
        const shapeSquare = data?.shapeSquare;
        const shapeConfig = data?.shapeConfig;

        if(shapeType && shapeSquare && shapeConfig) {
            const shape = BoardDrawer.createShape(shapeType, shapeSquare, shapeConfig);

            activeFeedback.push(shape);
        }
    }

    addedFeedback.forEach(processFeedback);
}

function maybeAnnounceMarkingsToPage(moveMarkings) {
    if(!runningOnDevPage || typeof unsafeWindow === 'undefined') return;

    const markings = activeGuiMoveMarkings || [];

    let selectedMarking = null;

    if(markings.length === 1) {
        selectedMarking = markings[0].player;
    } else if (markings.length > 1) {
        const randomIndex = Math.floor(Math.random() * markings.length);
        selectedMarking = markings[randomIndex].player;
    }

    unsafeWindow.postMessage({ name: 'bestMoveArr', value: selectedMarking });
}

const boardUtils = {
    markMoves: moveObjArr => { // needs refactoring but too lazy for now
        if(!BoardDrawer) return;

        const maxScale = 1;
        const minScale = 0.5;
        const totalRanks = moveObjArr.length;

        function fillSquare(square, style) {
            const shapeType = 'rectangle';
            const shapeConfig = { style };

            const rect = BoardDrawer.createShape(shapeType, square, shapeConfig);

            return rect;
        }

        const markedSquares = { 0: [], 1: [] };

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

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

            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);
            const moveAsFilledSquares = getConfigValue(configKeys.moveAsFilledSquares, profile);
            const onlySuggestPieces = getConfigValue(configKeys.onlySuggestPieces, profile);
            const movesOnDemand = getConfigValue(configKeys.movesOnDemand, profile);

            if(onlySuggestPieces && !movesOnDemand) {
                const fillType = idx === 0 ? 1 : 0,
                      fillColor = fillType ? primaryArrowColorHex : secondaryArrowColorHex;

                const fromSquareMarking = fillSquare(from, `opacity: ${arrowOpacity}; stroke-width: 5; stroke: black; rx: 2; ry: 2; fill: ${fillColor};`);
                let markedSquareElems = [fromSquareMarking];

                if(oppFrom) {
                    const oppFromSquareMarking = fillSquare(oppFrom, `opacity: ${arrowOpacity}; stroke-width: 5; stroke: black; rx: 2; ry: 2; display: none; fill: ${opponentArrowColorHex};`);

                    const squareListener = BoardDrawer.addSquareListener(from, type => {
                        if(!oppFromSquareMarking) squareListener.remove();

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

                    markedSquareElems.push(oppFromSquareMarking);
                }

                activeGuiMoveMarkings.push(
                    { 'otherElems': markedSquareElems }, profile
                );

            } else if(moveAsFilledSquares) {
                const fillType = idx === 0 ? 1 : 0,
                      fillColor = fillType ? primaryArrowColorHex : secondaryArrowColorHex,
                      styling = `opacity: ${arrowOpacity}; stroke-width: 5; stroke: black; rx: 2; ry: 2; fill: ${fillColor};`,
                      skipFromSquare = markedSquares[fillType].find(x => x === from) ? 'opacity: 0;' : '',
                      skipToSquare = markedSquares[fillType].find(x => x === to) ? 'opacity: 0;' : '';

                const fromSquareStyle = `${styling} ${skipFromSquare}`;
                const toSquareStyle = `filter: brightness(1.5); stroke-dasharray: 4 4; ${styling} ${skipToSquare}`;

                const fromSquareFill = fillSquare(from, fromSquareStyle);
                const toSquareFill = fillSquare(to, toSquareStyle);

                const markedSquareFens = [from, to];
                const markedSquareElems = [fromSquareFill, toSquareFill];

                if(oppMovesExist && showOpponentMoveGuess) {
                    const oppFromSquareFill = fillSquare(oppFrom, fromSquareStyle + ` fill: ${opponentArrowColorHex};`);
                    const oppToSquareFill = fillSquare(oppTo, toSquareStyle + ` fill: ${opponentArrowColorHex};`);

                    markedSquareElems.push(oppFromSquareFill, oppToSquareFill);

                    if(showOpponentMoveGuessConstantly) {
                        oppFromSquareFill.style.display = 'block';
                        oppToSquareFill.style.display = 'block';
                    } else {
                        oppFromSquareFill.style.display = 'none';
                        oppToSquareFill.style.display = 'none';

                        const squareListener = BoardDrawer.addSquareListener(from, type => {
                            if(!oppFromSquareFill || !oppToSquareFill) {
                                squareListener.remove();
                            }

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

                markedSquares[fillType].push(...markedSquareFens);
                activeGuiMoveMarkings.push(
                    { 'otherElems': markedSquareElems }, profile
                );

            } else {
                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 {
                        oppArrowElem.style.display = 'none';

                        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 }
                );
            }
        });

        maybeAnnounceMarkingsToPage(activeGuiMoveMarkings);
    },
    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();
            markingObj?.otherElems?.forEach(x => x?.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 clearVisuals() {
    clearFeedback();
    clearMetricRenders();
    boardUtils.removeMarkings();
}

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 = getBoardOrientation();

    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 = getBoardOrientation();

    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);

    lastPieceSize = pieceSize;

    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 = getBoardOrientation();

    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 = boardDimensions;

    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];
}

/* Need to make the board matricies more cohesive, right now it's really confusing flipping them
    * differently for each function. I just can't be bothered right now so please don't make fun of it.
    * Thanks, Haka
    * */

function chessCoordinatesToMatrixIndex(coord) {
    const [boardRanks, boardFiles] = getBoardDimensions();
    const indexArr = chessCoordinatesToIndex(coord);

    let x, y;

    y = boardFiles - (indexArr[1] + 1);
    x = indexArr[0];

    return [x, y];
}

function chessCoordinatesToDomIndex(coord) {
    const [boardRanks, boardFiles] = getBoardDimensions();
    const indexArr = chessCoordinatesToIndex(coord);
    const boardOrientation = getBoardOrientation();

    let x, y;

    if(boardOrientation === 'w') {
        x = indexArr[0];
        y = boardFiles - (indexArr[1] + 1);
    } else {
        x = boardRanks - (indexArr[0] + 1);
        y = indexArr[1];
    }

    return [x, y];
}

function indexToChessCoordinates(coord) {
    const [boardRanks, boardFiles] = getBoardDimensions();
    const boardOrientation = getBoardOrientation();

    const [x, y] = coord;
    const file = String.fromCharCode('a'.charCodeAt(0) + x);

    let rank;

    if (boardOrientation === 'w') {
        rank = boardRanks - y;
    } else {
        rank = boardRanks - y;
    }

    return `${file}${rank}`;
}

function isPawnPromotion(bestMove) {
    const [fenCoordFrom, fenCoordTo] = bestMove;
    const piece = getBoardPiece(fenCoordFrom);

    if(typeof piece !== 'string' || piece.toLowerCase() !== 'p')
        return false;

    // Determine the row from the ending coordinate (assumes standard algebraic notation, e.g., 'e8')
    const endingRow = parseInt(fenCoordTo[1], 10);

    // Check if the pawn reaches the promotion row
    if ((piece === 'P' && endingRow === (lastBoardFiles ?? 8)) || (piece === 'p' && endingRow === 1)) {
        return true;
    }

    return false;
}


function fenCoordArrToDomCoord(fenCoordArr) {
    // fenCoordArr e.g. ["e6", "e5"]

    const boardClientRect = chessBoardElem.getBoundingClientRect();

    const pieceElem = getPieceElem();
    const pieceDimensions = getElementSize(pieceElem);
    const pieceWidth = pieceDimensions?.width;
    const pieceHeight = pieceDimensions?.height;

    lastPieceSize = pieceDimensions;

    const [boardRanks, boardFiles] = getBoardDimensions();

    // Array to hold the center coordinates of each square
    const centerCoordinates = fenCoordArr.map(coord => {
        const [x, y] = chessCoordinatesToDomIndex(coord);

        const centerX = boardClientRect.x + (x * pieceWidth) + (pieceWidth / 2);
        const centerY = boardClientRect.y + (y * pieceHeight) + (pieceHeight / 2);

        return [centerX, centerY];
    });

    return centerCoordinates;
}

function coordinatesFromMoves(board, piecePos, moves, isPieceWhite) { // piecePos [x, y], moves [[x, y], ...]
    const result = [];

    for(let i = 0; i < moves.length; i++) {
        const x = piecePos[0] + moves[i][0];
        const y = piecePos[1] + moves[i][1];
        const square = board?.[y]?.[x];

        if(!square) continue;

        if(square === 1) {
            result.push([x, y]);
        } else {
            const squareIsWhite = square === square.toUpperCase();

            if(squareIsWhite !== isPieceWhite)
                result.push([x, y]);
        }
    }

    return result;
}

// Called by addMovesOnDemandListeners()
function getPiecePaths(board, piecePos, pieceFen, isPieceWhite) {
    const [xPos, yPos] = piecePos;

    if(!pieceFen || typeof pieceFen !== 'string') return;

    const pieceType = pieceFen.toUpperCase();

    const isLinearMovingPiece = pieceType === 'R' || pieceType === 'Q';
    const isDiagonalMovingPiece = pieceType === 'B' || pieceType === 'Q';

    const boardHeight = board.length;
    const boardWidth = board[0]?.length || 0;
    const longerBoardSide = Math.max(boardWidth, boardHeight);
    const shorterBoardSide = Math.min(boardWidth, boardHeight);

    function cast(directions, length) {
        const moves = [];

        for(let direction of directions) {
            for(let i = 1; i < length; i++) {
                const x = xPos + direction[0] * i;
                const y = yPos + direction[1] * i;

                const square = board?.[y]?.[x];
                if(!square) break;

                if(square === 1) {
                    moves.push([x, y]);
                } else {
                    const squareIsWhite = square === square.toUpperCase();

                    if(squareIsWhite !== isPieceWhite) {
                        moves.push([x, y]);
                        break;
                    } else
                        break;
                }
            }
        }

        return moves;
    }

    function castDiagonal() {
        return cast(
            [
                [1, -1], [-1, -1], // top right, top left
                [1, 1], [-1, 1]    // bottom right, bottom left
            ],
            shorterBoardSide
        );
    }

    function castStraight() {
        return cast(
            [
                [0, -1], [0, 1], // top, bottom
                [-1, 0], [1, 0]  // left, right
            ],
            longerBoardSide
        );
    }

    if(pieceType === 'P') {
        const direction = isPieceWhite ? [[-1, -1], [1, -1], [0, -1], [0, -2]] : [[-1, 1], [1, 1], [0, 1], [0, 2]];
        return coordinatesFromMoves(board, piecePos, direction, isPieceWhite);
    }

    if(pieceType === 'N') {
        return coordinatesFromMoves(board, piecePos, [
            [-2, -1], [-2, 1], [2, -1], [2, 1],
            [-1, -2], [-1, 2], [1, -2], [1, 2]
        ], isPieceWhite);
    }

    if(pieceType === 'K') {
        return coordinatesFromMoves(board, piecePos, [
            [-1, 0], [-2, 0], [1, 0], [2, 0], [0, -1],
            [0, 1], [-1, -1], [1, 1], [-1, 1], [1, -1]
        ], isPieceWhite);
    }

    if(pieceType === 'B') return castDiagonal();
    if(pieceType === 'R') return castStraight();
    if(pieceType === 'Q') return [...castDiagonal(), ...castStraight()];

    return [0, 0];
}

function addMovesOnDemandListeners() {
    let lastProcessedSquareFen = null;

    if(!BoardDrawer) return;

    function handle() {
        if((lastProcessedSquareFen !== modLastEnteredSquare.squareFen) || !modLastEnteredSquare.squareFen) {
            const lastIdx = modLastEnteredSquare.squareIndex;

            if(!modLastEnteredSquare.squareFen && lastIdx) {
                const lastPieceFen = modLastEnteredSquare.pieceFen;

                modLastEnteredSquare.squareFen = indexToChessCoordinates(lastIdx);
                modLastEnteredSquare.pieceFen = lastBoardMatrix?.[lastIdx?.[1]]?.[lastIdx?.[0]];

                if(lastPieceFen === 1) return;
            }

            lastProcessedSquareFen = modLastEnteredSquare.squareFen;

            const pieceFen = modLastEnteredSquare.pieceFen;
            const isPieceWhite = pieceFen >= 'A' && pieceFen <= 'Z';
            const isPlayerPiece = (lastBoardOrientation === 'w') === isPieceWhite;

            if(!pieceFen) return;

            const legalMovesArr = getPiecePaths(lastBoardMatrix, modLastEnteredSquare.squareIndex, pieceFen, isPieceWhite)
                ?.map(pathArr => lastProcessedSquareFen + indexToChessCoordinates(pathArr));

            if(legalMovesArr?.length > 0)
                CommLink.commands.calculateSpecificMoves({ 'moves': legalMovesArr, 'isOpponent': !isPlayerPiece });
        }
    }

    // Clear existing listeners
    modListeners.forEach(({ type, handler }) => {
        document.removeEventListener(type, handler);
    }); modListeners.length = 0;

    modDrawerListeners.forEach(x => x?.remove());
    modDrawerListeners.length = 0;

    const mouseDownHandler = () => handle(true);
    const touchStartHandler = () => handle(true);

    [
        ['mousedown', mouseDownHandler],
        ['touchstart', touchStartHandler]
    ].forEach(([type, handler]) => {
        document.addEventListener(type, handler);
        modListeners.push({ type, handler });
    });

    for (let y = 0; y < lastBoardMatrix.length; y++)
        for (let x = 0; x < lastBoardMatrix[y].length; x++) {
                const squareFen = indexToChessCoordinates([x, y]);

                const squareListener = BoardDrawer.addSquareListener(squareFen, type => {
                    if(!isMovesOnDemandActive) return;

                    switch(type) {
                        case 'enter':
                            modLastEnteredSquare.pieceFen = lastBoardMatrix[y][x];
                            modLastEnteredSquare.squareFen = squareFen;
                            modLastEnteredSquare.squareIndex = [x, y];

                            break;
                    }
                });

                modDrawerListeners.push(squareListener);
            }
}

function getRandomOwnPieceDomCoord(fenCoord, boardMatrix) {
    let [x, y] = chessCoordinatesToMatrixIndex(fenCoord);

    const pieceAtFenCoord = boardMatrix[y][x];

    if(pieceAtFenCoord === 1) {
        return null;
    }

    const isWhitePiece = pieceAtFenCoord === pieceAtFenCoord.toUpperCase();

    const getDistance = (row1, col1, row2, col2) => {
        return Math.abs(row1 - row2) + Math.abs(col1 - col2);
    };

    let candidatePieces = [];

    // Loop through the board matrix to find all close own pieces
    for(let row = 0; row < boardMatrix.length; row++) {
        for(let col = 0; col < boardMatrix[row].length; col++) {
            const currentPiece = boardMatrix[row][col];

            // Skip if no piece is found or if the piece is of the wrong color
            if(currentPiece === 1 || (isWhitePiece && currentPiece === currentPiece.toLowerCase()) || (!isWhitePiece && currentPiece === currentPiece.toUpperCase())) {
                continue;
            }

            const distance = getDistance(y, x, row, col);

            if(distance < 6) {
                candidatePieces.push({ distance, coord: [col, row], piece: currentPiece });
            }
        }
    }

    if (candidatePieces.length > 0) {
        // Choose a random piece from the candidates
        const randomIndex = Math.floor(Math.random() * candidatePieces.length);
        const chosenPiece = candidatePieces[randomIndex];

        return fenCoordArrToDomCoord([indexToChessCoordinates(chosenPiece.coord)])[0];
    }

    return null;
}

function getPieceAmount() {
    return getPieceElem(true)?.length ?? 0;
}

class AutomaticMove {
    constructor(profile, fenMoveArr, isLegit, callback) {
        this.id = getUniqueID();

        // activeAutomoves is an external variable, not a child of AutomaticMove
        activeAutomoves.push({ 'id': this.id, 'move': this });

        this.profile = profile;
        this.fenMoveArr = fenMoveArr;
        this.isLegit = isLegit;

        this.active = true;
        this.isPromotingPawn = false;

        this.onFinished = function(...args) {
            activeAutomoves.filter(x => x.id !== this.id); // remove the move from the active automove list

            this.active = false;

            callback(...args);
        };

        this.moveDomCoords = fenCoordArrToDomCoord(fenMoveArr);
        this.isPromotion = isPawnPromotion(fenMoveArr);

        if(this.isLegit) {
            const legitModeType = getConfigValue(configKeys.legitModeType, this.profile) ?? 'casual';

            const pieceRanges = [
                { minPieces: 30, maxPieces: Infinity }, // Opening (60+ pieces)
                { minPieces: 23, maxPieces: 29 },       // Early Middlegame (48 to 64 pieces)
                { minPieces: 16, maxPieces: 22 },       // Mid Middlegame (32 to 48 pieces)
                { minPieces: 10, maxPieces: 15 },       // Late Middlegame (16 to 32 pieces)
                { minPieces: 6, maxPieces: 9 },         // Endgame (8 to 16 pieces)
                { minPieces: 3, maxPieces: 5 },         // Very Endgame (2 to 8 pieces)
                { minPieces: 1, maxPieces: 2 },         // Extremely Few Pieces (1 piece)
            ];

            const timeRanges = {
                beginner: [
                    [2000, 4000],
                    [3000, 15000],
                    [5000, 25000],
                    [4000, 30000],
                    [3000, 15000],
                    [2000, 10000],
                    [1000, 4000],
                ],
                casual: [
                    [900, 3000],    // Opening
                    [1000, 15000],  // Early Middlegame
                    [3000, 20000],  // Mid Middlegame
                    [2000, 13000],  // Late Middlegame
                    [1500, 10000],  // Endgame
                    [1000, 9000],   // Very Endgame
                    [500, 3000],    // Extremely Few Pieces
                ],
                intermediate: [
                    [750, 2000],
                    [1000, 10000],
                    [2000, 15000],
                    [1500, 12000],
                    [1000, 8000],
                    [750, 7000],
                    [500, 2000],
                ],
                advanced: [
                    [500, 1500],
                    [1000, 8000],
                    [750, 8000],
                    [750, 12000],
                    [750, 5000],
                    [750, 3000],
                    [500, 1200],
                ],
                master: [
                    [333, 999],
                    [400, 2000],
                    [400, 3000],
                    [400, 2500],
                    [400, 2000],
                    [400, 1500],
                    [333, 750],
                ],
                professional: [
                    [333, 666],
                    [333, 666],
                    [333, 1000],
                    [333, 1500],
                    [333, 1000],
                    [333, 666],
                    [333, 666],
                ],
                god: [
                    [50, 333],
                    [50, 233],
                    [50, 300],
                    [50, 250],
                    [50, 200],
                    [50, 150],
                    [50, 100],
                ]
            };

            this.timeRanges = pieceRanges.map((range, index) => ({
                ...range,
                timeRange: timeRanges[legitModeType][index],
            }));

            this.shouldHesitate = this.isLegit && Math.random() < 0.15;
            this.shouldHesitateTwice = this.isLegit && Math.random() < 0.25;
            this.hesitationTypeOne = this.isLegit && Math.random() < 0.35;

            const legitTotalMoveTime = this.calculateMoveTime(getPieceAmount());
            const elapsedMoveTime = (Date.now() - lastMoveRequestTime); // How long did it take for the engine to calculate the move
            const remainingTime = Math.max(legitTotalMoveTime - elapsedMoveTime, 500);

            const delays = this.generateDelaysForDesiredTime(remainingTime);

            for(const key of Object.keys(delays)) {
                this[key] = delays[key];
            }
        }

        this.start();
    }

    generateDelaysForDesiredTime(desiredTotalTime) {
        // Fixed minimum values
        const PROMOTION_DELAY = this.getRandomIntegerBetween(1000, 1111); // just can't be done very fast for some reason at least on Chess.com

        if(desiredTotalTime > 6000) {
            const timelines = [
                { move: .4, to: .2, hesitation: .15, hesitationResolve: .15, secondHesitationResolve: .15 },
                { move: .1, to: .3, hesitation: .25, hesitationResolve: .15, secondHesitationResolve: .2 },
                { move: .2, to: .25, hesitation: .2, hesitationResolve: .2, secondHesitationResolve: .15 }
            ];

            const timeline = timelines[Math.floor(Math.random() * timelines.length)];

            return {
                promotionDelay: PROMOTION_DELAY,
                moveDelay: desiredTotalTime * timeline.move,
                toSquareSelectDelay: desiredTotalTime * timeline.to,
                hesitationDelay: desiredTotalTime * timeline.hesitation,
                hesitationResolveDelay: desiredTotalTime * timeline.hesitationResolve,
                secondHesitationResolveDelay: desiredTotalTime * timeline.secondHesitationResolve
            };
        }
        // There is time for one hesitation
        if(desiredTotalTime > 3000) {
            const timelines = [
                { move: .3, to: .2, hesitation: .25, hesitationResolve: .25 },
                { move: .1, to: .3, hesitation: .45, hesitationResolve: .15 },
                { move: .2, to: .25, hesitation: .2, hesitationResolve: .35 }
            ];

            const timeline = timelines[Math.floor(Math.random() * timelines.length)];

            return {
                promotionDelay: PROMOTION_DELAY,
                moveDelay: desiredTotalTime * timeline.move,
                toSquareSelectDelay: desiredTotalTime * timeline.to,
                hesitationDelay: desiredTotalTime * timeline.hesitation,
                hesitationResolveDelay: desiredTotalTime * timeline.hesitationResolve,
                secondHesitationResolveDelay: -1
            };
        }
        // There is not enough time to hesitate
        else {
            const timelines = [
                { move: .9, to: .1 },
                { move: .45, to: .55 },
                { move: .6, to: .4 },
                { move: .4, to: .6 },
                { move: .1, to: .9 }
            ];

            const timeline = timelines[Math.floor(Math.random() * timelines.length)];

            return {
                promotionDelay: PROMOTION_DELAY,
                moveDelay: desiredTotalTime * timeline.move,
                toSquareSelectDelay: desiredTotalTime * timeline.to,
                hesitationDelay: -1, hesitationResolveDelay: -1, secondHesitationResolveDelay: -1
            };
        }
    }

    calculateMoveTime(pieceCount) {
        for(let range of this.timeRanges) {
            if(pieceCount >= range.minPieces && pieceCount <= range.maxPieces) {
                return this.getRandomIntegerBetween(range.timeRange[0], range.timeRange[1]);
            }
        }

        return 500;
    }

    getRandomIntegerBetween(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    getRandomIntegerNearAverage(min, max) {
        const mid = (min + max) / 2;
        const range = (max - min) / 2;

        let value = Math.floor(mid + (Math.random() - 0.5) * range * 1.5);

        return Math.max(min, Math.min(max, value));
    }

    delay(ms) {
        return this.active ? new Promise(resolve => setTimeout(resolve, ms)) : true;
    }

    async triggerPieceClick(input) {
        const parentExists = activeAutomoves.find(x => x.move === this) ? true : false;

        if(!parentExists) {
            return;
        }

        let clientX, clientY;

        if(input instanceof Element) {
            const rect = input.getBoundingClientRect();
            clientX = rect.left + rect.width / 2;
            clientY = rect.top + rect.height / 2;
        } else if (typeof input === 'object') {
            clientX = input[0];
            clientY = input[1];
        } else {
            return;
        }

        const xDivider = Math.random() < 0.85 ? 4 : Math.random() < 0.15 ? 3 : 2;
        const yDivider = Math.random() < 0.65 ? 3 : Math.random() < 0.35 ? 2 : 4;

        const randomVariationX = (lastPieceSize?.width - 4) / xDivider;
        const randomVariationY = (lastPieceSize?.height - 4) / yDivider;

        const randomOffsetX = (Math.random() - 0.5) * 2 * randomVariationX;
        const randomOffsetY = (Math.pow(Math.random(), 0.5) - 0.5) * 2 * randomVariationY;

        const randomizedX = clientX + randomOffsetX;
        const randomizedY = clientY + randomOffsetY;

        const pointerEventOptions = {
            bubbles: true,
            cancelable: true,
            clientX: randomizedX,
            clientY: randomizedY,
        };

        const elementToTrigger = (input instanceof Element) ? input : document.elementFromPoint(clientX, clientY);

        if(elementToTrigger) {
            switch(domain) {
                case 'chess.com':
                    elementToTrigger.dispatchEvent(new PointerEvent('pointerdown', pointerEventOptions));

                    if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125));

                    elementToTrigger.dispatchEvent(new PointerEvent('pointerup', pointerEventOptions));

                    break;
                case 'lichess.org':
                    elementToTrigger.dispatchEvent(new MouseEvent('mousedown', pointerEventOptions));

                    if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125));

                    elementToTrigger.dispatchEvent(new MouseEvent('mouseup', pointerEventOptions));

                    break;
                case 'worldchess.com':
                    elementToTrigger.dispatchEvent(new MouseEvent('mousedown', pointerEventOptions));

                    if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125));

                    elementToTrigger.dispatchEvent(new MouseEvent('mouseup', pointerEventOptions));

                    break;
            }
        }

        if(debugModeActivated) {
            const dot = document.createElement('div');
                    dot.style.position = 'absolute';
                    dot.style.width = '7px';
                    dot.style.height = '7px';
                    dot.style.borderRadius = '50%';
                    dot.style.backgroundColor = 'lime';
                    dot.style.left = `${randomizedX - 2.5}px`;
                    dot.style.top = `${randomizedY - 2.5}px`;

            const container = document.createElement('div');
                    container.style.position = 'absolute';
                    container.style.width = `${Math.round(randomVariationX * 2)}px`;
                    container.style.height = `${Math.round(randomVariationY * 2)}px`;
                    container.style.border = '2px dashed green';
                    container.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
                    container.style.left = `${clientX - randomVariationX}px`;
                    container.style.top = `${clientY - randomVariationY}px`;

            document.body.appendChild(container);
            document.body.appendChild(dot);

            setTimeout(() => {
                dot.remove();
                container.remove();
            }, 1000);
        }
    }

    click(domCoord) {
        if(this.active)
            this.triggerPieceClick(domCoord);
    }

    async hesitate() {
        const hesitationPieceDomCoord = getRandomOwnPieceDomCoord(this.fenMoveArr[0], getBoardMatrix());

        if(hesitationPieceDomCoord) {
            if(this.hesitationTypeOne) {
                this.click(this.moveDomCoords[0]);
                await this.delay(this.hesitationDelay);
            }

            this.click(hesitationPieceDomCoord);

            await this.delay(this.hesitationResolveDelay);

            if(this.shouldHesitateTwice && this.secondHesitationResolveDelay !== -1) {
                const secondHesitationPieceDomCoord = getRandomOwnPieceDomCoord(this.fenMoveArr[0], getBoardMatrix());
                this.click(secondHesitationPieceDomCoord);
                await this.delay(this.secondHesitationResolveDelay);
            }
        }

        this.finishMove(this.toSquareSelectDelay, this.promotionDelay);
    }


    async finishMove(delay01, delay02) {
        this.click(this.moveDomCoords[0]);

        await this.delay(delay01);

        this.click(this.moveDomCoords[1]);

        // Handle promotion click if necessary
        if(this.isPromotion) {
            this.isPromotingPawn = true;

            await this.delay(delay02);

            this.click(this.moveDomCoords[1]);

            this.isPromotingPawn = false;
        }

        this.onFinished(true);
    }

    async playLegit() {
        await this.delay(this.moveDelay);

        if(this.shouldHesitate && this.hesitationDelay !== -1)
            this.hesitate();
        else
            this.finishMove(this.toSquareSelectDelay, this.promotionDelay);
    }

    async start() {
        if(this.isLegit) {
            this.playLegit();
        } else {
            this.finishMove(5, 1111);
        }
    }

    async stop() {
        if(this.isPromotingPawn) {
            // Attempt to promote the pawn before closing
            this.click(this.moveDomCoords[1]);
        }

        this.onFinished(false);
    }
}

async function makeMove(profile, fenMoveArr, isLegit) {
    const move = new AutomaticMove(profile, fenMoveArr, isLegit, e => {
        // This is ran when the move finished

        if(debugModeActivated) console.warn('Move', fenMoveArr, move.id, 'finished', 'for profile:', profile);
    });
}

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'];

    function check(cfg) {
        const profiles = Object.keys(cfg);

        for(const profileName of profiles) {
            const profile = cfg[profileName];

            const isGhost = profile[configKeys.isUserscriptGhost];
            if(isGhost) return false;

            const externalMoves = profile[configKeys.displayMovesOnExternalSite];
            const renderingNeeded = profile[configKeys.renderOnExternalSite];
            const feedbackNeeded = profile[configKeys.feedbackOnExternalSite];
            const movesOnDemand = profile[configKeys.movesOnDemand];

            if(externalMoves || renderingNeeded || feedbackNeeded || movesOnDemand) {
                return true;
            }
        }
    }

    if(gP && check(gP)) return true;
    if(iP && check(iP)) 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 getBoardOrientation() {
    const playerColor = instanceVars.playerColor.get(commLinkInstanceID);

    return playerColor;
}

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);

    return result;
}

function addSupportedChessSite(domains, typeHandlerObj) {
    const domainList = Array.isArray(domains) ? domains : [domains];

    domainList.forEach(domain => {
        supportedSites[domain] = typeHandlerObj;
    });
}

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

    return boardElem || null;
}

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

    const boardQuerySelector = (getAll ? query => {
      const elems = boardElem?.querySelectorAll(query);
      return elems?.length ? [...elems] : null;
    } : 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 isNewMoveArr = getSiteData('isMutationNewMove', { mutationArr }); // [isNewMove, turn]

    return isNewMoveArr || false;
}

function getMutationTurn(mutationArr) {
    const turn = getSiteData('getMutationTurn', { mutationArr });

    return turn || null;
}

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

    const board = Array.from({ length: boardFiles }, () => Array(boardRanks).fill(1));
    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);

            try {
                const [xIdx, yIdx] = pieceCoordsArr;

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

    lastBoardMatrix = board;

    return board;
}

function getBoardPiece(fenCoord) {
    const [boardRanks, boardFiles] = getBoardDimensions();
    const indexArr = chessCoordinatesToIndex(fenCoord);

    return getBoardMatrix()?.[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 : '-';
}

function getBasicFen() {
    const boardMatrix = getBoardMatrix();

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

function getFen(onlyBasic) {
    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} ${getBoardOrientation()} ${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 countTotalPieces(fen) {
    let pieceCount = 0;
    const position = fen.split(' ')[0];

    for (let char of position) {
        if (/[rnbqkpRNBQKP]/.test(char)) {
            pieceCount += (pieceCount[char] || 0) + 1;
        }
    }

    return pieceCount;
}

function getPieceChangeAmount(lastFen, newFen) {
    if(!lastFen || !newFen) return 0;

    const lastPieceCount = countTotalPieces(lastFen);
    const newPieceCount = countTotalPieces(newFen);

    // (need to implement fix for variants which may add pieces legally)
    const countChange = newPieceCount - lastPieceCount;

    /* Possible "countChange" value explanations,
        (countChange < -1) -> multiple pieces have disappeared (atomic chess variant or a faulty newFen?)
        (countChange = -1) -> piece has been eaten
        (countChange = 0)  -> piece moved
        (countChange = 1)  -> piece has spawned
        (countChange > 1)  -> multiple pieces have spawned (possibly a new game?)
    */

    return countChange;
}

function getBoardSquareChangeAmount(lastFen, newFen) {
    if(!lastFen || !newFen) return 0;

    let board1 = lastFen.split(' ')[0].replace(/\d/g, d => ' '.repeat(d)).split('/').join('');
    let board2 = newFen.split(' ')[0].replace(/\d/g, d => ' '.repeat(d)).split('/').join('');

    let changedFrom = [];
    let diff = 0;

    for(let i = 0; i < board1.length; i++) {
        if(board1[i] !== board2[i]) {
            if(board1[i]?.trim()?.length > 0) changedFrom.push(board1[i]?.toLowerCase());

            diff += 1;
        }
    }

    /* Possible "diff" value explanations,
        (diff = 0) -> no changes, same board layout
        (diff = 1) -> only one tile abruptly changed, shouldn't be possible
        (diff = 2) -> a piece moved, maybe it ate another piece
        (diff = 3) -> three tiles had changes, shouldn't be possible (NOTE: it's possible if the fen has skipped one turn...)
        (diff > 3) -> a lot of tiles had changes, maybe a new game started, the change is significant so allowing it
        (diff = 4) -> takeback or castling
    */

    return diff;
}

function processBoardPosition(currentFullFen = getFen(), pieceAmountChange = 0) {
    if(debugModeActivated) console.warn('NEW MOVE DETECTED!');

    lastCalculatedFullFen = currentFullFen;
    lastMoveRequestTime = Date.now();

    clearVisuals();

    boardUtils.setBoardDimensions(getBoardDimensions());
    modLastEnteredSquare.squareFen = null;

    if(!modListeners.length)
        addMovesOnDemandListeners();

    // Most likely a new match started
    if(checkBoardOrientationChange() || pieceAmountChange > 7) {
        resetCachedValues();

        matchFirstSuggestionGiven = false;
    }

    CommLink.commands.updateBoardFen(currentFullFen);
}

// This is called by observeNewMoves()
async function determineBoardPositionValidity(turn) {
    const currentFullFen = await getFen();
    const fenChanged = currentFullFen?.split(' ', 1)?.[0] !== lastCalculatedFullFen?.split(' ', 1)?.[0]; // only compare the first part of the FENs to detect change

    const pieceAmountChange = getPieceChangeAmount(lastCalculatedFullFen, currentFullFen);
    const pieceAmount = getPieceAmount(); // this depends on the current DOM, not from FEN.

    // A new board just appeared and is still loading, most likely a new match!
    if(pieceAmount === 0) {
        lastCalculatedFullFen = null;

        await wait(100);

        return determineBoardPositionValidity(getBoardOrientation());
    }

    // Do not continue if FEN did not change!
    if(!fenChanged) return;
    // The board position is now different, so clear any rendered visuals (move arrows, etc.)
    //clearVisuals();

    if(turn) {
        if(lastTurn === turn) turn = getBoardOrientation();
        lastTurn = turn;

        instanceVars.turn.set(commLinkInstanceID, turn);
    }

    if(pieceAmountChange === -1) {
        const squareChangeAmount = getBoardSquareChangeAmount(lastCalculatedFullFen, currentFullFen);

        // Do not continue if a piece just disappeared, this is not possible legally!
        // (This happens sometimes because the mutationObserver detects DOM changes so fast)
        if(squareChangeAmount === 1) {
            clearVisuals();
            return;
        }
    }

    processBoardPosition(currentFullFen, pieceAmountChange);
}

// This also updates the turn instanceVariable that the GUI uses to determine the turn!
function observeNewMoves() {
    if(boardObserver?.disconnect) boardObserver.disconnect();
    if(dumbBoardObservingInterval) clearInterval(dumbBoardObservingInterval);

    dumbBoardObservingInterval = setInterval(() => {
        if(isUserMouseDown) return;
        determineBoardPositionValidity(lastMutationObsProcessedTurn || getBoardOrientation());
    }, 250);

    boardObserver = new MutationObserver(mutationArr => {
        try {
            lastMutationObservationDate = Date.now();

            const mutationMoveArr = isMutationNewMove(mutationArr); // returns [isNewMove, turn]
            const isNewMove = mutationMoveArr?.[0];
            let turn = mutationMoveArr?.[1];

            if(turn) lastMutationObsProcessedTurn = turn;

            // Do not continue if mutation was not detected as a possible new move! (Different for each chess site)
            // We later compare FENs to detect if it was actually a new valid move!
            if(!isNewMove) return;

            determineBoardPositionValidity(turn);
        } catch(e) {
            if(debugModeActivated) console.error(e);
        }
    });

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

async function checkBoardOrientationChange() {
    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);
    }

    return boardOrientationChanged;
}

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

┏┓┳┏┳┓┏┓  ┏┓┏┓┳┓┏┓┳┏┓
┗┓┃ ┃ ┣   ┃ ┃┃┃┃┣ ┃┃┓
┗┛┻ ┻ ┗┛  ┗┛┗┛┛┗┻ ┻┗┛
=====================
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 => {
                    if(elem?.dataset?.piece?.toLowerCase() === 'x') return false;

                    return !elem.closest('[class*="captured-pieces"]');
                });

            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];
        }
    },

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

        let blacks = 0;
        let whites = 0;

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            for(let i = 0; i < classList.length; i++) {
                const cls = classList[i];

                if(cls.length === 2) {
                    const prefix = cls[0];

                    if(prefix === 'b') blacks++;
                    else if(prefix === 'w') whites++;
                }
            }
        });

        const turn = blacks > whites ? 'w' : 'b';

        return turn || null;
    },

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

        // Process variant boards...
        if(pathname?.includes('/variants')) {
            if(isUserMouseDown) return [false, null];
            return [true, getBoardOrientation()]; // allow everything, always make own turn
        }

        // Not a variant board, processing differently...

        if(mutationArr.length === 1)
            return [false, null];

        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;

        const isNewMove = mutationArr.length >= 3 && !isPremove;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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];
    },

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

        let blacks = 0;
        let whites = 0;

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            if(classList?.contains('black')) blacks += 1;
            if(classList?.contains('white')) whites += 1;
        });

        const turn = blacks > whites ? 'w' : 'b';

        return turn || null;
    },

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

        const isNewMove = mutationArr.length >= 3;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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 = getBoardOrientation();
        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();
    },

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

        let ally = 0;
        let enemy = 0;

        const boardOrientation = getBoardOrientation();
        const isPlayerWhite = boardOrientation === 'w';

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            if(classList?.contains('ally')) ally += 1;
            if(classList?.contains('enemy')) enemy += 1;
        });

        const turn = isPlayerWhite
          ? ally > enemy ? 'b' : 'w'
          : ally > enemy ? 'w' : 'b';

        return turn || null;
    },

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

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

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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 = getBoardOrientation();
        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();
    },


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

        let ally = 0;
        let enemy = 0;

        const boardOrientation = getBoardOrientation();
        const isPlayerWhite = boardOrientation === 'w';

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            if(classList?.contains('ally')) ally += 1;
            if(classList?.contains('enemy')) enemy += 1;
        });

        const turn = isPlayerWhite
          ? ally > enemy ? 'b' : 'w'
          : ally > enemy ? 'w' : 'b';

        return turn || null;
    },

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

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

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

addSupportedChessSite('chess.org', {
    '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;

        return getElemCoordinatesFromTransform(pieceElem);
    },

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

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

        let blacks = 0;
        let whites = 0;

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            if(classList?.contains('black')) blacks += 1;
            if(classList?.contains('white')) whites += 1;
        });

        const turn = blacks > whites ? 'w' : 'b';

        return turn || null;
    },

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

        if(isUserMouseDown) {
            return false;
        }

        const isNewMove = true; // laggy but this is a non-popular site

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

addSupportedChessSite('chess.coolmathgames.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 document.querySelector('.ranks.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];
    },

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

        let blacks = 0;
        let whites = 0;

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            if(classList?.contains('black')) blacks += 1;
            if(classList?.contains('white')) whites += 1;
        });

        const turn = blacks > whites ? 'w' : 'b';

        return turn || null;
    },

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

        if(isUserMouseDown) {
            return false;
        }

        const isNewMove = true; // laggy but this is a non-popular site

        // NOTE! IF YOU'RE TRYING TO FIX DISAPPEARING MOVES, IT IS CAUSED BY THE BOARD CHANGING
        // AND THE USERSCRIPT TRIGGERING A WHOLE NEW MATCH STARTING. THIS IS A NON-POPULAR SITE
        // SO FIX HAS NOT BEEN MADE...

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

addSupportedChessSite('papergames.io', {
    'boardElem': obj => {
        return document.querySelector('.cm-chessboard');
    },

    '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];
    },

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

        const playerColor = getBoardOrientation();

        return playerColor || null;
    },

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

        const isNewMove = mutationArr.length >= 12;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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];
    },

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

        const playerColor = getBoardOrientation();

        return playerColor || null;
    },

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

        if(isUserMouseDown) {
            return false;
        }

        const isNewMove = mutationArr.length >= 5;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

addSupportedChessSite('worldchess.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];
    },

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

        let blacks = 0;
        let whites = 0;

        mutationArr.forEach(mutation => {
            const classList = mutation?.target?.classList;

            for(let i = 0; i < classList.length; i++) {
                const cls = classList[i];

                if(cls.length === 2) {
                    const prefix = cls[0];

                    if(prefix === 'b') blacks++;
                    else if(prefix === 'w') whites++;
                }
            }
        });

        const turn = blacks > whites ? 'w' : 'b';

        return turn || null;
    },

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

        if(isUserMouseDown) {
            return false;
        }

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

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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];
    },

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

        let blacks = 0;
        let whites = 0;

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            if(classList?.contains('black')) blacks += 1;
            if(classList?.contains('white')) whites += 1;
        });

        const turn = blacks > whites ? 'w' : 'b';

        return turn || null;
    },

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

        const isNewMove = mutationArr.length >= 3;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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];
    },

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

        let blacks = 0;
        let whites = 0;

        mutationArr.forEach(mutation => {
            const classList = mutation.target?.classList;

            if(classList?.contains('black')) blacks += 1;
            if(classList?.contains('white')) whites += 1;
        });

        const turn = blacks > whites ? 'w' : 'b';

        return turn || null;
    },

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

        const isNewMove = mutationArr.length >= 3;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

addSupportedChessSite('play.chessclub.com', {
    'boardElem': obj => {
        return document.querySelector('[data-boardid]');
    },

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

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

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

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

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

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

        const parentParent = pieceElem?.parentElement?.parentElement;

        if(parentParent) {
            return chessCoordinatesToIndex(parentParent?.dataset?.square);
        }
    },

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

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

        return getBoardOrientation() || null;
    },

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

        const isNewMove = mutationArr.find(mutation => mutation?.type === 'childList')
            ? true : false;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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];
    },

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

        return getBoardOrientation() || null;
    },

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

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

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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];
    },

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

        return getBoardOrientation() || null;
    },

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

        const isNewMove = mutationArr.length >= 2;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

addSupportedChessSite([
    backendConfig?.hosts?.prod || 'psyyke.github.io',
    backendConfig?.hosts?.dev || 'localhost'
], {
    '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.side');

        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 ?? [])]
          .map(cls => cls.replace('-piece', ''))
          .find(cls => Object.values(pieceNameToFen).includes(cls));

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

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

        const key = pieceElem?.cgKey;

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

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

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

        const mutationContainsBlack = mutationArr
            .find(mutation => mutation.target?.classList?.contains('black'));

        const turn = mutationContainsBlack ? 'w' : 'b';

        return turn || null;
    },

    'isMutationNewMove': obj => {
        const mutationArr = obj.mutationArr;
        const isNewMove = mutationArr.length >= 2;

        if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)];
        return [isNewMove, null];
    }
});

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

┳┓┏┓┏┓┏┳┓  ┏┓┏┓┏┓┳┳┏┓┳┓┏┓┏┓
┣┫┃┃┃┃ ┃   ┗┓┣ ┃┃┃┃┣ ┃┃┃ ┣
┻┛┗┛┗┛ ┻   ┗┛┗┛┗┻┗┛┗┛┛┗┗┛┗┛
===========================
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 refreshSettings() {
        const config = GM_getValue(dbValues.AcasConfig);
        const profiles = config?.global?.profiles;

        if(typeof profiles != 'object') return;

        isMovesOnDemandActive = Object.keys(profiles).some(profileName =>
            profiles[profileName]?.movesOnDemand === true);
}

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

    const pathname = window.location.pathname;
    const boardOrientation = getBoardOrientation();

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

    if(isBoardDrawerNeeded()) {
        if(BoardDrawer) BoardDrawer?.terminate();

        BoardDrawer = new UniversalBoardDrawer(chessBoardElem, {
            'window': window,
            'boardDimensions': getBoardDimensions(),
            'playerColor': getBoardOrientation(),
            'zIndex': Math.floor(Math.random() * (99 - 10 + 1)) + 10,
            'prepend': true,
            'debugMode': debugModeActivated,
            'adjustSizeByDimensions': domain === 'chess.com' && pathname?.includes('/variants'),
            'adjustSizeConfig': {
                'noLeftAdjustment': true
            },
            'ignoreBodyRectLeft': domain === 'app.edchess.io'
        });

        const waitForBoardMatrix = setInterval(() => {
            if(lastBoardMatrix) {
                clearInterval(waitForBoardMatrix);

                addMovesOnDemandListeners();
            }
        }, 50);
    }

    await checkBoardOrientationChange();

    refreshSettings();
    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);
        }
    }, 500);
}

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

    const bothElemsExist = boardElem && firstPieceElem;
    const isChessComImageBoard = domain === 'chess.com' && boardElem?.className.includes('webgl-2d');
    const boardElemChanged = chessBoardElem != boardElem;

    if((bothElemsExist || isChessComImageBoard) && 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) {
            processBoardPosition();
        } 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, 100);
// This slow rate might cause users to complain that settings aren't being applied fast enough
setInterval(refreshSettings, 2500);

} catch(e) { // Attempt to catch all errors on userscript (Note: ONLY LOG DURING DEVELOPMENT. ERRORS EXPOSE USERSCRIPT TO THE PAGE!)
    //console.warn(e);
}})(); // Wraps around the whole userscript to enable async.

/*////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////

Thank you for reading through this userscript! Please visit GitHub
Contributions are absolutely welcome >> github.com/Psyyke/A.C.A.S!

//////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////
000000000000000000000000000000000000000000000000000000000000000000
777777770000000007777777777777770000000077777777700000000077777777
777777777000000777777777777777777700000077777777770000007777777777
777077777000007777777000000077777700000777777777777000007777770000
777077777700007777770000000000000000007777770077777000007777777777
770007777770077777700000000000000000077777700077777700000777777777
700000777777007777770000000000000000077777700007777770000000007777
777777777777007777770000000077777700777777777777777777077777700000
777777777777700777777777777777777707777777777777777777007777777777
000000007777770007777777777777770077777770000000077777700777777777
000000007777777000007777777770000077777700000000007777770000777777
000000000000000000000000000000000000000000000000000000000000000000
//////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////*/