Crime Profitability

Show value per nerve on the crime pages

// ==UserScript==
// @name         Crime Profitability
// @namespace    heartflower.torn
// @version      1.1
// @description  Show value per nerve on the crime pages
// @author       Heartflower [2626587]
// @match        https://www.torn.com/loader.php?sid=crimes*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM.xmlHttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // Cached cracking data!

    console.log('[HF] Crime Profatibility script running');

    let emforusData = `https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/gviz/tq?tqx=out:csv&gid=560321570`;
    let crackingData = `https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/gviz/tq?tqx=out:csv&gid=1626436424`;

    let rememberedData = JSON.parse(localStorage.getItem('hf-crime-profitability-data'));
    let rememberedCrackingData = JSON.parse(localStorage.getItem('hf-crime-profitability-cracking-data'));
    let lastFetched = Number(localStorage.getItem('hf-crime-profitability-last-fetched'));

    let cachedData = null;
    let cachedCrackingData = null;
    let currentHref = window.location.href;

    let settings = {};
    let savedSettings = JSON.parse(localStorage.getItem('hf-crime-profitability-settings'));
    if (savedSettings) settings = savedSettings;

    // MAKE PDA COMPATIBLE
    let pda = ('xmlhttpRequest' in GM);
    let httpRequest = pda ? 'xmlhttpRequest' : 'xmlHttpRequest';

    // Display value per nerve on search for most crimes
    function crimePage(data, crackingData, observed, retries = 30) {
        let list = document.body.querySelector('.virtualList___noLef');
        let subCrimes = list?.querySelectorAll('.crimeOptionSection___hslpu');

        // If DOM isn't fully loaded, try again
        if (!list || !subCrimes || subCrimes.length < 2) {
            if (retries > 0) {
                setTimeout(() => crimePage(data, crackingData, observed, retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for Crime Page subtitles after 30 retries.');
            }
            return;
        }

        let maximum = -Infinity, maximumElement = null;

        // SPECIAL CHANGES FOR SPECIFIC CRIMES //

        // Create an observer on the pickpocketing page, as new targets are added and old ones removed
        if (window.location.href.includes('pickpocketing') && !observed) createObserver(list, data);

        // Create an observer on the cracking page for scrolling reasons
        if (window.location.href.includes('cracking') && !observed) createObserver(list, data, crackingData);

        // FORGERY: dropdown container on top of the page
        let forgeryPage = window.location.href.includes('forgery');
        let maximumForgery = -Infinity, maximumForgeryTarget = null;

        // CRACKING: check BFS as $/N differs on it
        let bfs = 0;
        let cracking = document.body.querySelector('.strength___DM3lW .value___FmWPr');
        if (cracking) bfs = Math.round(parseFloat(cracking.textContent.trim()));

        // BACK TO THE MAIN CODE //

        // Create a datamap for each subcrime
        let dataMap = new Map();
        for (let crimeData of data) {
            if (crimeData.target === 'City Centre') crimeData.target = 'City Center'; // Issue in the sheet for graffiti
            if (crimeData.target) dataMap.set(crimeData.target.toLowerCase(), crimeData);

            // If forgery, find the best forgery target!
            if (forgeryPage && crimeData.crime === 'Forgery' && parseInt(crimeData['$/N']?.replace(/[$,]/g, '')) > maximumForgery) {
                maximumForgery = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
                maximumForgeryTarget = crimeData.target;
            }
        }

        // SPECIAL CHANGES FOR SPECIFIC CRIMES //

        // Create a CRACKING separate datamap due to separate data sheet
        let crackingDataMap = new Map();
        if (cracking && crackingData) {
            for (let crimeData of crackingData) {
                if (crimeData['Targeted service ']) crackingDataMap.set(crimeData['Targeted service '].toLowerCase(), crimeData);
            }
        }


        // BACK TO THE MAIN CODE //

        for (let subCrime of subCrimes) {
            let firstTextNode = Array.from(subCrime.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
            let label = firstTextNode?.textContent.trim().toLowerCase();

            // SPECIAL CHANGES FOR SPECIFIC CRIMES //

            // PICKPOCKETING
            let pickpocketing = subCrime.querySelector('.titleAndProps___DdeVu div');
            if (pickpocketing) label = pickpocketing.textContent.trim().toLowerCase();

            // CRACKING
            cracking = subCrime.querySelector('.type___T9oMA');
            let crackingService = subCrime.querySelector('.service___uYhDL');


            // FORGERY (MOBILE)
            let mobileForgery = subCrime.querySelector('.tabletProjectTitle___Wsdf7');
            if (mobileForgery) {
                label = mobileForgery.textContent.trim().toLowerCase();
                firstTextNode = Array.from(mobileForgery.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
            }

            // FORGERY
            if (forgeryPage) {
                let dropdownWrapper = subCrime.querySelector('.optionWithLevelRequirement___cHH35');
                if ((!mobileForgery && subCrime.textContent === 'Begin a New Project') || dropdownWrapper) {
                    forgery(subCrime, dataMap, maximumForgery, maximumForgeryTarget);
                }
            }

            // SEARCH FOR CASH (MOBILE)
            let mobileSearchForCash = subCrime.querySelector('.titleAndIcon___h8RJV');
            if (mobileSearchForCash) {
                firstTextNode = Array.from(mobileSearchForCash.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
                label = firstTextNode?.textContent.trim().toLowerCase();
            }

            // GRAFFITI (MOBILE)
            let mobileGraffiti = subCrime.querySelector('.tabletTitleAndTagCount___vb0UQ');
            if (mobileGraffiti) {
                firstTextNode = Array.from(mobileGraffiti.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
                label = firstTextNode?.textContent.trim().toLowerCase();
            }

            // SHOPLIFTING (MOBILE)
            let mobileShoplifting = subCrime.querySelector('.tabletShopTitle___aqRuE');
            if (mobileShoplifting) label = mobileShoplifting.textContent.trim().toLowerCase();


            // MOBILE MISC
            let mobileOther = document.body.querySelector('.area-mobile___BH0Ku');

            // BACK TO THE MAIN CODE //
            let crimeData = dataMap.get(label);


            // SPECIAL CHANGES FOR SPECIFIC CRIMES //

            // CRACKING
            if (cracking) {
                label = crackingService.textContent.trim().toLowerCase();
                crimeData = crackingDataMap.get(label);

                if (bfs === 6) {
                    crimeData['$/N'] = crimeData['Estimated profit per nerve 6BFS'];
                } else if (bfs === 7) {
                    crimeData['$/N'] = crimeData['7BFS'];
                } else {
                    crimeData['$/N'] = crimeData['Estimated profit per nerve 6BFS'];
                }
            }

            // GRAFFITI (MOBILE)
            if (mobileGraffiti && !crimeData) crimeData = dataMap.get(`${label} district`);

            // If crime data does not exist (due to wrong element or other reason), skip the rest
            if (!crimeData) continue;

            // STYLE CHANGES
            subCrime.style.display = 'flex';

            // MOBILE SEARCH FOR CASH AND SHOPLIFTING EXTRA STILE CHANGES
            if (!mobileSearchForCash && !mobileShoplifting) subCrime.style.justifyContent = 'space-between';

            // MOBILE STYLE CHANGES
            let typeServiceWrapper = subCrime.querySelector('.typeAndServiceWrapper___AoONK');
            if (typeServiceWrapper) typeServiceWrapper.style.flex = '1';


            // BACK TO THE MAIN CODE //

            // CREATE VALUE SPAN
            let span = document.createElement('span');
            span.classList.add('hf-value');

            let moneyPerNerve = -Infinity;

            // FILL IN VALUE DATA IN SPAN
            if (crimeData['$/N']) { // Information found in the sheet
                moneyPerNerve = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));

                // Find the maximum money per nerve for this crime!
                if (moneyPerNerve > maximum) {
                    maximum = moneyPerNerve
                    maximumElement = subCrime.parentNode;
                }

                let spanText = `${moneyPerNerve < 0 ? '-' : ''}$${Math.abs(moneyPerNerve).toLocaleString('en-US')} / N`;
                span.textContent = spanText;

                // Color green by default, but red if negative $/N
                span.style.color = 'var(--default-base-green-color)';
                if (moneyPerNerve < 0) span.style.color = 'var(--default-base-important-color)';

                // SPECIAL TOOLTIP FOR MOBILE GRAFFITI AND SHOPLIFTING
                if (mobileGraffiti) mobileGraffiti.title = spanText;
                if (mobileShoplifting) mobileShoplifting.title = spanText;

            } else {
                let spanText = '$/N N/A';
                span.textContent = spanText;

                // Color yellow for N/A
                span.style.color = 'var(--default-base-gold-color)';

                // SPECIAL TOOLTIP FOR MOBILE GRAFFITI AND SHOPLIFTING
                if (mobileGraffiti) mobileGraffiti.title = spanText;
                if (mobileShoplifting) mobileShoplifting.title = spanText;
            }

            // Set attribute to the item container, so I can easily use that later
            subCrime.parentNode.parentNode.parentNode.parentNode.setAttribute('data-hf-value', moneyPerNerve);

            // If the SPAN doesn't exist yet, append it! DIFFERENT FOR SPECIAL CRIMES
            let existingSpan = subCrime.querySelector('.hf-value');
            if (!existingSpan) {
                if (cracking) {
                    subCrime.appendChild(span);
                } else if (mobileSearchForCash) {
                    mobileSearchForCash.insertBefore(span, mobileSearchForCash.childNodes[1] || null);
                } else if (!forgeryPage && !mobileGraffiti && !mobileShoplifting) {
                    subCrime.insertBefore(span, subCrime.childNodes[1] || null);
                }
            }


            // SPECIAL STYLE CHANGES FOR SPECIFIC (MOBILE) CRIMES //

            let graffiti = subCrime.querySelector('.reputationIconWrapper___CM05s');
            if (graffiti || pickpocketing) span.style.paddingLeft = '15px';
            if (pickpocketing) span.style.flex = '2';
            if (mobileOther && pickpocketing) span.style.fontSize = 'smaller'

            // On forgery, take care of the dropdown/overview container
            if (forgeryPage && !mobileForgery) {
                let div = document.createElement('div');
                div.style.display = 'flex';
                div.style.flexDirection = 'column';

                let typeSpan = document.createElement('span');
                typeSpan.textContent = firstTextNode.textContent;

                div.appendChild(typeSpan);
                div.appendChild(span);

                span.style.paddingTop = '5px';

                subCrime.insertBefore(div, subCrime.childNodes[1] || null);

                firstTextNode.remove();
            } else if (mobileForgery) {
                mobileForgery.appendChild(span);
                span.style.paddingLeft = '5px';
            }

            subCrime.parentNode.style.background = ''; // So crimes like pickpocketing aren't always showing multiple options
        }

        // BACK TO THE MAIN CODE //

        if (maximumElement) maximumElement.style.background = 'var(--default-bg-green-hover-color)';

        // DON'T SHOW SORT BUTTON ON PICKPOCKETING
        if (window.location.href.includes('pickpocketing')) return; // Button is just too hard with the way the crime works

        if (observed && crackingData) {
            let existingButton = document.body.querySelector('.hf-sort-button');
            if (existingButton) existingButton.remove();
        }

        // CREATE A SORT BUTTON
        let button = createSortButton();
        button.addEventListener('click', function () {
            let invalidOpposite = false;
            if (forgeryPage) invalidOpposite = true; // FORGERY has an overview as first "subcrime"
            sortButtonClick(list, button, invalidOpposite);
        });
    }

    // Display value per nerve on BOOTLEGGING
    function bootlegging(data, retries = 30) {
        let optionWrappers = document.body.querySelectorAll('.crimeOptionWrapper___IOnLO');

        // If the DOM hasn't loaded yet, try again!
        if (!optionWrappers || optionWrappers.length < 2) {
            if (retries > 0) {
                setTimeout(() => bootlegging(data, retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for Bootlegging subtitles after 30 retries.');
            }
            return;
        }

        // Map of UI labels to normalized data targets
        let labelToTargetMap = {
            'sell counterfeit dvds': 'sell counterfeit dvds',
            'online store': 'collect from online store',
        };

        let maximum = -Infinity;
        let maximumElement = null;

        for (let optionWrapper of optionWrappers) {
            // Find the "Sell" and "Online" buttons
            if (!optionWrapper.textContent.toLowerCase().includes('sell') && !optionWrapper.textContent.toLowerCase().includes('online')) continue;

            let options = optionWrapper.querySelectorAll('.crimeOptionSection___hslpu');
            for (let option of options) {
                let label = option.textContent.trim().toLowerCase();
                let target = labelToTargetMap[label];

                let moneyPerNerve = -Infinity;

                // If target can't be found, move on
                if (!target) continue;

                let match = data.find(c => c.target.toLowerCase() === target);
                if (match && match['$/N']) moneyPerNerve = parseInt(match['$/N'].replace('$', ''));

                // Create the span
                let span = document.createElement('span');
                span.classList.add('hf-value');
                span.textContent = `${moneyPerNerve < 0 ? '-' : ''}$${Math.abs(moneyPerNerve).toLocaleString('en-US')} / N`;
                span.style.paddingLeft = '15px';
                span.style.color = 'var(--default-base-green-color)';
                if (moneyPerNerve < 0) span.style.color = 'var(--default-base-important-color)';

                if (moneyPerNerve > maximum) {
                    maximum = moneyPerNerve
                    maximumElement = option.parentNode;
                }

                option.appendChild(span);
            }
        }

        maximumElement.style.background = 'var(--default-bg-green-hover-color)';
    }

    // Display value per nerve on DISPOSAL
    function disposal(data, observed, retries = 30) {
        let mobile = document.body.querySelector('.area-mobile___BH0Ku');
        let list = document.body.querySelector('.virtualList___noLef');
        let subCrimes = list?.querySelectorAll('.crimeOptionSection___hslpu');

        // If the DOM hasn't fully loaded yet, try again
        if (!list || !subCrimes || subCrimes.length < 2) {
            if (retries > 0) {
                setTimeout(() => disposal(data, observed, retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for Disposal subtitles after 30 retries.');
            }
            return;
        }

        if (!observed) {
            createObserver(list, data, crackingData, true);
        }

        let disposalData = {};


        for (let crimeData of data) {
            // If crime isn't disposal, no need to loop through it
            if (crimeData.crime !== 'Disposal') continue;

            // Disposal is formatted in the sheet as "Subtitle: Type"
            let [targetTitle, targetType] = crimeData.target.split(':').map(s => s.trim());
            if (!disposalData[targetTitle.trim().toLowerCase()]) {
                disposalData[targetTitle.trim().toLowerCase()] = {};
            }

            disposalData[targetTitle.trim().toLowerCase()][targetType] = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
        }

        let maximum = -Infinity;
        let maximumElement = null;

        for (let subCrime of subCrimes) {
            let firstTextNode = Array.from(subCrime.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
            let label = firstTextNode?.textContent.trim().toLowerCase();

            let data = disposalData[label]
            if (!data) continue; // If data is not found, break it off here

            let maximumValue = -Infinity;
            let maximumType = null;

            for (let type in data) {
                if (data[type] > maximumValue) {
                    maximumValue = data[type];
                    maximumType = type;
                }
            }

            // Take care of the method buttons
            let methods = subCrime.parentNode.querySelectorAll('.methodButton___lCgpf');
            for (let method of methods) {
                let ariaLabel = method.getAttribute('aria-label');
                let info = disposalData[label][ariaLabel.trim()];

                // Display $/N upon hover
                method.title = `${info < 0 ? '-' : ''}$${Math.abs(info).toLocaleString('en-US')} / N`;

                // Give the method borders based on profitability
                if (info < 0) method.style.border = '1px solid var(--default-base-important-color)';
                if (info >= 0) method.style.border = '1px solid var(--default-base-gold-color)';
                if (ariaLabel.trim().toLowerCase() === maximumType.toLowerCase()) method.style.border = '1px solid var(--default-base-green-color)';
            }

            if (maximumValue > maximum) {
                maximum = maximumValue
                maximumElement = subCrime.parentNode.parentNode;
            }

            // Create value div
            let div = document.createElement('div');
            div.classList.add('hf-value');
            div.style.display = 'flex';
            div.style.flexDirection = 'column';
            div.style.alignItems = 'flex-end';
            div.style.color = 'var(--default-base-green-color)';
            if (maximumValue < 0) div.style.color = 'var(--default-base-important-color)';

            let type = document.createElement('span');
            type.textContent = maximumType;
            div.appendChild(type);

            let value = document.createElement('span');
            value.textContent = `${maximumValue < 0 ? '-' : ''}$${Math.abs(maximumValue).toLocaleString('en-US')} / N`;
            div.appendChild(value);

            let existingDiv = subCrime.querySelector('.hf-value');
            if (!existingDiv) subCrime.appendChild(div);
            subCrime.style.display = 'flex';
            subCrime.style.justifyContent = 'space-between';

            // MOBILE STYLE CHANGES
            if (mobile) {
                subCrime.style.flexWrap = 'wrap';
                subCrime.style.alignContent = 'center';
                div.style.paddingTop = '5px';
                div.style.flexDirection = '';
                type.textContent += ':';
                value.style.paddingLeft = '5px';
            }

            subCrime.parentNode.parentNode.parentNode.parentNode.setAttribute('data-hf-value', maximumValue);

            subCrime.parentNode.parentNode.style.background = ''; // So it isn't showing multiple options upon page refresh
        }

        if (maximumElement) maximumElement.style.background = 'var(--default-bg-green-hover-color)';

        if (observed) {
            let existingButton = document.body.querySelector('.hf-sort-button');
            if (existingButton) existingButton.remove();
        }

        let button = createSortButton();
        button.addEventListener('click', function () {
            sortButtonClick(list, button);
        });
    }

    // HELPER FUNCTION for the dropdown and "Begin a New Project" in FORGERY
    function forgery(subCrime, dataMap, maximum, maximumTarget) {
        let mobileForgery = document.body.querySelector('.tabletProjectTitle___Wsdf7');

        if (subCrime.textContent === 'Begin a New Project') {
            if (mobileForgery) return; // No "Begin a New Project" title on mobile

            let div = document.createElement('div');
            div.classList.add('hf-best-forgery');
            div.style.display = 'flex';
            div.style.alignItems = 'flex-end';
            div.style.color = 'var(--default-base-green-color)';
            if (maximum < 0) div.style.color = 'var(--default-base-important-color)';

            let maximumTargetEl = document.createElement('span');
            maximumTargetEl.textContent = `${maximumTarget}`;

            let maximumEl = document.createElement('span');
            maximumEl.textContent = `(${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N)`;
            maximumEl.style.paddingLeft = '5px';

            div.appendChild(maximumTargetEl);
            div.appendChild(maximumEl);

            subCrime.style.display = 'flex';
            subCrime.style.justifyContent = 'space-between';
            subCrime.appendChild(div);

            return;
        }

        let dropdownWrapper = subCrime.querySelector('.optionWithLevelRequirement___cHH35');
        if (dropdownWrapper) {
            let typesEl = dropdownWrapper.lastChild;

            let mainSpan = document.createElement('span');
            mainSpan.textContent = ` (${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N)`;
            mainSpan.style.display = 'contents';
            mainSpan.style.color = 'var(--default-base-green-color)';
            if (maximum < 0) mainSpan.style.color = 'var(--default-base-important-color)';

            dropdownWrapper.appendChild(mainSpan);

            let ul = subCrime.querySelector('ul');
            let lists = ul.querySelectorAll('li');
            for (let list of lists) {
                let id = list.id;
                id = id.replace(/-/g, ' ').replace(/\d+$/, '').replace('option', '');

                let crimeData = dataMap.get(id.trim().toLowerCase());

                let option = list.querySelector('.optionWithLevelRequirement___cHH35');
                let listExplanation = option.lastChild;

                let value = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));

                // Create a green span for the value
                let span = document.createElement('span');
                span.style.display = 'contents';
                span.style.color = 'var(--default-base-green-color)';
                if (value < 0) span.style.color = 'var(--default-base-important-color)';

                let amount = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
                span.textContent = ` (${amount < 0 ? '-' : ''}$${Math.abs(amount).toLocaleString('en-US')})`;

                option.appendChild(span);

                continue;
            }
        }
    }

    // HELPER FUNCTION to create the sort button
    function createSortButton() {
        let headerElement = document.body.querySelector('.crimes-app-header');

        let button = document.createElement('button');
        button.textContent = '▲ Profitability';
        button.classList.add('hf-sort-button');
        button.style.background = 'var(--input-money-error-border-color)';
        button.style.color = 'var(--btn-color)';
        button.style.borderRadius = '8px';
        button.style.cursor = 'pointer';
        button.style.fontWeight = 'bold';

        headerElement.insertBefore(button, headerElement.lastElementChild);

        return button;
    }

    // HELPER FUNCTION to call when the sort button is clicked
    function sortButtonClick(list, button, invalidOpposite) {
        let subCrimeArray = Array.from(list.querySelectorAll('.virtual-item'));

        subCrimeArray.sort((a, b) => {
            let aRaw = a.getAttribute('data-hf-value');
            let bRaw = b.getAttribute('data-hf-value');

            let aVal = parseFloat(aRaw);
            let bVal = parseFloat(bRaw);

            let aInvalid = isNaN(aVal);
            let bInvalid = isNaN(bVal);

            // Push invalid (missing or NaN) values to the bottom or top depending on the crime
            if (invalidOpposite && (!a.classList.contains('virtualItemsBackdrop___oTwUm') || !b.classList.contains('virtualItemsBackdrop___oTwUm'))) {
                if (aInvalid && !bInvalid) return -1;
                if (!aInvalid && bInvalid) return 1;
                if (aInvalid && bInvalid) return 0;
            } else {
                if (aInvalid && !bInvalid) return 1;
                if (!aInvalid && bInvalid) return -1;
                if (aInvalid && bInvalid) return 0;
            }

            if (button.textContent.includes('▲')) {
                return bVal - aVal; // highest first
            } else {
                return aVal - bVal; // lowest first
            }
        });

        // Reposition elements visually
        subCrimeArray.forEach((el, i) => {
            if (invalidOpposite) {
                if (i === 0) {
                    return;
                } else if (i === 1) {
                    el.style.transform = `translateY(64px)`;
                    return;
                }

                el.style.transform = `translateY(${((i - 1) * 51) + 64}px)`;
            } else {
                el.style.transform = `translateY(${i * 51}px)`;
            }
        });

        let backDrop = list.querySelector('.virtualItemsBackdrop___oTwUm');

        // Toggle sort direction
        button.textContent = button.textContent.includes('▲') ? '▼ Profitability' : '▲ Profitability';
    }

    // BURGLARY create sheets button
    function createSheetsButton() {
        let headerElement = document.body.querySelector('.crimes-app-header');

        let button = document.createElement('button');
        button.textContent = 'Crime Profitability Sheet';
        button.title = `Crime Profitability will be added soonTM for this crime (data pending)`;
        button.classList.add('hf-sort-button');
        button.style.background = 'var(--input-money-error-border-color)';
        button.style.color = 'var(--btn-color)';
        button.style.borderRadius = '8px';
        button.style.cursor = 'pointer';
        button.style.fontWeight = 'bold';

        headerElement.insertBefore(button, headerElement.lastElementChild);

        button.addEventListener('click', function () {
            window.open(
                'https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=706159343#gid=706159343',
                '_blank'
            );
        });

        return button;
    }

    // UNAVAILABLE CRIME create sheets button
    function createUnavailable() {
        let headerElement = document.body.querySelector('.crimes-app-header');

        let button = document.createElement('button');
        button.textContent = 'Crime Profitability Sheet';
        button.title = `Crime Profitability is (currently) unavailable for this crime`;
        button.classList.add('hf-sort-button');
        button.style.background = 'var(--input-money-error-border-color)';
        button.style.color = 'var(--btn-color)';
        button.style.borderRadius = '8px';
        button.style.cursor = 'pointer';
        button.style.fontWeight = 'bold';

        headerElement.insertBefore(button, headerElement.lastElementChild);

        button.addEventListener('click', function () {
            window.open(
                'https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=0#gid=0',
                '_blank'
            );
        });

        return button;
    }

    // CRIME HUB display value per nerve
    function crimeHub(data, retries = 30) {
        let crimeHubRoot = document.body.querySelector('.crimes-hub-root');
        let crimes = crimeHubRoot?.querySelectorAll('.crimes-hub-crime');

        if (!crimeHubRoot || !crimes || crimes.length < 2) {
            if (retries > 0) {
                setTimeout(() => crimeHub(data, retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for the crime hub after 30 retries.');
            }
            return;
        }

        for (let crime of crimes) {
            let titleElement = crime.querySelector('.crimeTitle___Q9cpR');
            let title = titleElement?.textContent.trim();
            if (title) {
                findMaximumProfit(data, title, crime);
            }
        }
    }

    // HELPER FUNCTION for the CRIME HUB to find and display maximum profit per crime type
    function findMaximumProfit(data, crimeTitle, crimeElement) {
        if (!settings[crimeTitle]) return;

        let maximum = -Infinity;
        for (let crimeData of data) {
            if (crimeData.crime.toLowerCase() !== crimeTitle.toLowerCase()) continue;
            let moneyPerNerve = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
            if (moneyPerNerve > maximum) maximum = moneyPerNerve
        }

        let span = document.createElement('span');
        span.textContent = `${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N`;
        span.classList.add('hf-value');
        span.style.position = 'absolute';
        span.style.justifySelf = 'anchor-center';
        span.style.marginTop = '-38px';
        span.style.padding = '8px';
        span.style.borderRadius = '15px';
        span.style.background = 'var(--default-bg-19-gradient)';
        span.style.color = 'var(--default-base-white-color)';

        if (maximum < 0) span.style.background = 'var(--default-bg-17-gradient)';
        if (maximum === -Infinity) {
            span.textContent = '$/N N/A';
            span.style.background = 'var(--default-bg-18-gradient)';
        }

        let titleBar = crimeElement.querySelector('.titleBar___O6ygy');
        if (!titleBar) {
            titleBar = crimeElement.querySelector('.titleAndStatus___q2yBJ');
            let statusGroups = titleBar.querySelector('.statusGroups___EtLVR');
            titleBar.insertBefore(span, statusGroups);
            span.style.position = '';
            span.style.justifySelf = '';
            span.style.marginTop = '';
            span.style.background = '';
            span.style.color = 'var(--default-base-green-color)';
            span.style.padding = '';
            span.style.borderRadius = '';
            span.style.marginBottom = '-3px';

            if (maximum < 0) span.style.color = 'var(--default-base-important-color)';
            if (maximum === -Infinity) span.style.color = 'var(--default-base-gold-color)';
        } else {
            titleBar.insertBefore(span, titleBar.firstChild);
        }
    }

    // HELPER FUNCTION to convert a CSV string into an array of objects
    function parseCSV(text) {
        let rows = text.trim().split('\n');

        let parseRow = (row) => {
            let regex = /"(.*?)"(?:,|$)/g;
            let result = [];
            let match;

            while ((match = regex.exec(row)) !== null) result.push(match[1]);

            return result.slice(0, 8); // Only keep first 8!! columns
        };

        let headers = parseRow(rows.shift());

        // Rename the first "7BFS" to "7BFS attempts"
        let found = false;
        headers = headers.map(header => {
            if (header === '7BFS' && !found) {
                found = true;
                return '7BFS attempts';
            }
            return header;
        });

        return rows.map(row => {
            let values = parseRow(row);
            let entry = {};
            headers.forEach((header, index) => {
                entry[header] = values[index];
            });
            return entry;
        });
    }


    // FETCH FUNCTION to fetch data from Emforus' sheet
    function fetchData() {
        let currentEpoch = Math.floor(Date.now() / 1000);
        let twentyFourHours = 86400;

        if (rememberedCrackingData && lastFetched && (currentEpoch - lastFetched) <= twentyFourHours) {
            console.log('[HF] Using cached Crime Profitability data.');

            let data = rememberedData;
            cachedData = data;

            let crackingData = rememberedCrackingData;
            cachedCrackingData = crackingData;


            console.log('Data', data);
            console.log('Cached data', cachedData);
            console.log('Cracking data', crackingData);
            console.log('Cached cracking data', cachedCrackingData);

            displayData(data, crackingData);

            return;
        }

        let urls = {
            all: emforusData,
            cracking: crackingData
        };

        let results = {};
        let completed = 0;
        let total = Object.keys(urls).length;

        Object.entries(urls).forEach(([type, url]) => {
            GM[httpRequest]({
                method: 'GET',
                url: url,
                responseType: 'text',
                onload: function(response) {
                    const data = parseCSV(response.responseText);
                    results[type] = data;

                    if (type === 'all') {
                        cachedData = data;
                        localStorage.setItem('hf-crime-profitability-data', JSON.stringify(data));
                    } else if (type === 'cracking') {
                        cachedCrackingData = data;
                        localStorage.setItem('hf-crime-profitability-cracking-data', JSON.stringify(data));
                    }

                    completed++;
                    if (completed === total) {
                        let currentEpoch = Math.floor(Date.now() / 1000);
                        localStorage.setItem('hf-crime-profitability-last-fetched', currentEpoch);
                        displayData(results.all, results.cracking);
                    }
                },
                onerror: function(response) {
                    console.error(`Error fetching ${type} data:`, response);
                }
            });
        });
    }

    // HELPER FUNCTION to create a mutation observer to watch for changes on the page
    function createObserver(element, info, crackingInfo, disposalPage) {
        let target;
        target = element;

        if (!target) {
            console.error(`[HF] Mutation Observer target not found.`);
            return;
        }

        let observer = new MutationObserver(function(mutationsList, observer) {
            for (let mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.classList.contains('virtual-item')) {
                            if (!disposalPage) {
                                crimePage(info, crackingInfo, true);
                            } else {
                                disposal(info, true);
                            }
                        }
                    });

                    mutation.removedNodes.forEach(node => {
                        if (node.classList.contains('virtual-item')) {
                            if (!disposalPage) {
                                crimePage(info, crackingInfo, true);
                            } else {
                                disposal(info, true);
                            }
                        }
                    });
                }
            }
        });

        let config = { attributes: true, childList: true, subtree: true, characterData: true };
        observer.observe(target, config);
    }

    // Attach click event listener
    document.body.addEventListener('click', handleButtonClick);

    function handleButtonClick(event) {
        setTimeout(() => {
            handlePageChange();
        }, 500);
    }

    // Check if there's a page chance - if yes, rerun script
    function handlePageChange() {
        if (window.location.href === currentHref) return;

        let existingButton = document.body.querySelector('.hf-sort-button');
        if (existingButton) existingButton.remove();

        displayData(cachedData, cachedCrackingData);

        currentHref = window.location.href;
    }

    // Adds a settings button to the CRIME HUB page
    function createSettingsButton() {
        let mobile = document.body.querySelector('.area-mobile___BH0Ku');

        let headerElement = document.body.querySelector('.crimes-app-header');

        let button = document.createElement('button');
        button.textContent = 'Crime Profitability Settings';
        button.classList.add('hf-sort-button');
        button.style.background = 'var(--input-money-error-border-color)';
        button.style.color = 'var(--btn-color)';
        button.style.borderRadius = '8px';
        button.style.cursor = 'pointer';
        button.style.fontWeight = 'bold';

        if (mobile) button.textContent = `$/N Settings`;

        headerElement.insertBefore(button, headerElement.children[1]);

        button.addEventListener('click', function () {
            createModal();
        });

        return button;
    }

    // Function to create the SETTINGS modal
    function createModal() {
        let cachedSettings = settings;

        let modal = document.createElement('div');
        modal.style.position = 'fixed';
        modal.style.top = '50%';
        modal.style.left = '50%';
        modal.style.transform = 'translate(-50%, -50%)';
        modal.style.padding = '20px';
        modal.style.backgroundColor = 'var(--sidebar-area-bg-warning-active)';
        modal.style.border = '2px solid var(--default-tabs-color)';
        modal.style.borderRadius = '15px';
        modal.style.zIndex = '9999';

        let titleContainer = document.createElement('div');
        titleContainer.textContent = 'Crime Profitability Settings';
        titleContainer.style.fontSize = 'medium';
        titleContainer.style.fontWeight = 'bolder';
        titleContainer.style.paddingBottom = '8px';

        modal.appendChild(titleContainer);

        let toggleContainer = document.createElement('div');
        toggleContainer.style.display = 'flex';
        toggleContainer.style.flexDirection = 'column';

        let checkboxDiv = document.createElement('div');
        checkboxDiv.style.display = 'flex';
        checkboxDiv.style.flexDirection = 'column';

        let creditSpan = document.createElement('span');
        creditSpan.innerHTML = `Powered by <a href="https://www.torn.com/profiles.php?XID=2626587" target="_blank" rel="noopener noreferrer" style="color: var(--default-blue-color);">Heartflower [2626587]</a> and the <a href="https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=560321570#gid=560321570" target="_blank" rel="noopener noreferrer" style="color: var(--default-blue-color);">Crime Profitability Index Google Sheets</a> by <a href="https://www.torn.com/profiles.php?XID=2535044" target="_blank" rel="noopener noreferrer" style="color: var(--default-blue-color);">Emforus [2535044]</a>.`;
        creditSpan.style.paddingBottom = '8px';
        checkboxDiv.appendChild(creditSpan);

        let infoSpan = document.createElement('span');
        infoSpan.textContent = 'Enable/disable which crimes you want crime profitability to appear for here.';
        checkboxDiv.appendChild(infoSpan);

        let crimes = ['Enable All', 'Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing',
                      'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming'];

        for (let crime of crimes) {
            let checkbox = addToggle(checkboxDiv, crime);
        }

        createToggleStyleSheet();

        let warningSpan = document.createElement('span');
        warningSpan.textContent = `* Currently (temporarily) unavailable`;
        warningSpan.style.paddingTop = '5px';
        checkboxDiv.appendChild(warningSpan);

        toggleContainer.appendChild(checkboxDiv);

        // Create a container for "Done" and "Cancel" buttons
        let buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.justifyContent = 'space-between';
        buttonContainer.style.paddingTop = '15px';
        toggleContainer.appendChild(buttonContainer);

        let cancelButton = document.createElement('button');
        cancelButton.textContent = 'Cancel';
        cancelButton.style.color = 'black';
        cancelButton.style.border = '1px solid black';
        cancelButton.style.borderRadius = '5px';
        cancelButton.style.backgroundColor = '#ccc';
        cancelButton.addEventListener('click', function () {
            settings = cachedSettings;
            localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));

            modal.style.display = 'none';
        });
        buttonContainer.appendChild(cancelButton);

        // Add style for hover effect
        cancelButton.style.cursor = 'pointer';

        // Add event listeners for hover effect
        cancelButton.addEventListener('mouseover', function () {
            cancelButton.style.fontWeight = 'bold';
        });

        cancelButton.addEventListener('mouseout', function () {
            cancelButton.style.fontWeight = '';
        });

        let doneButton = document.createElement('button');
        doneButton.textContent = 'Done';
        doneButton.style.color = 'black';
        doneButton.style.border = '1px solid black';
        doneButton.style.borderRadius = '5px';
        doneButton.style.backgroundColor = '#ccc';
        doneButton.addEventListener('click', function () {
            location.reload();
            modal.style.display = 'none';
        });
        buttonContainer.appendChild(doneButton);

        // Add style for hover effect
        doneButton.style.cursor = 'pointer';

        // Add event listeners for hover effect
        doneButton.addEventListener('mouseover', function () {
            doneButton.style.fontWeight = 'bold';
        });

        doneButton.addEventListener('mouseout', function () {
            doneButton.style.fontWeight = '';
        });

        modal.appendChild(toggleContainer);
        document.body.appendChild(modal);

        return modal;
    }

    // Add the checkbox toggle in the SETTINGS modal
    function addToggle(container, labelText) {
        let toggleContainer = document.createElement('div');
        toggleContainer.classList.add('hf-toggle-container');
        toggleContainer.style.paddingTop = '5px';

        let text = document.createElement('span');
        text.textContent = labelText;
        text.style.paddingLeft = '5px';

        let label = document.createElement('label');
        label.classList.add('switch');

        let input = document.createElement('input');
        input.type = 'checkbox';
        input.classList.add('hf-checkbox');

        input.checked = true; // Check on by default

        let span = document.createElement('span');
        span.classList.add('slider', 'round');

        if (labelText === 'Graffiti' || labelText === 'Card Skimming' || labelText === 'Burglary' || labelText === 'Hustling' || labelText === 'Scamming') {
            text.textContent += '*';
            input.checked = false;
        }

        let savedInfo = settings[labelText];

        if (savedInfo === true) input.checked = true;
        if (savedInfo === false) input.checked = false;


        if (labelText === 'Enable All') input.checked = false;

        settings[labelText] = input.checked;
        localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));

        toggleContainer.appendChild(label);
        toggleContainer.appendChild(text);
        label.appendChild(input);
        label.appendChild(span);
        container.appendChild(toggleContainer);

        // Add event listener to detect changes in the checkbox state
        input.addEventListener('change', function() {
            if (input.checked) {
                if (labelText === 'Enable All') {
                    let inputs = document.body.querySelectorAll('.hf-checkbox');
                    for (let input of inputs) {
                        input.checked = true;
                        let event = new Event('change', { bubbles: true });
                        input.dispatchEvent(event);
                    }

                    text.textContent = 'Disable All';
                    return;
                }

                settings[labelText] = true;
                localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));
            } else {
                if (labelText === 'Enable All' || labelText === 'Disable All') {
                    let inputs = document.body.querySelectorAll('.hf-checkbox');
                    for (let input of inputs) {
                        input.checked = false;
                        let event = new Event('change', { bubbles: true });
                        input.dispatchEvent(event);
                    }

                    text.textContent = 'Enable All';
                    return;
                }

                settings[labelText] = false;
                localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));
            }
        });
    }

    // HELPER FUNCTION to create a style sheet to make the fancier toggles work
    function createToggleStyleSheet() {
        let styles = `
        .switch {
          position: relative;
          display: inline-block;
          width: 20px;
          height: 10px;
          top: 1px;
        }
        .switch input {
          opacity: 0;
          width: 0;
          height: 0;
        }
        .slider {
          position: absolute;
          cursor: pointer;
          top: 0;
          left: 0;
          right: 0;
          bottom: 0;
          background-color: #ccc;
          transition: .4s;
        }
        .slider:before {
          position: absolute;
          content: "";
          height: 10px;
          width: 10px;
          background-color: white;
          transition: .4s;
        }
        input:checked + .slider {
          background-color: #2196F3;
        }
        input:focus + .slider {
          box-shadow: 0 0 1px #2196F3;
        }
        input:checked + .slider:before {
          transform: translateX(10px);
        }
        .slider.round {
          border-radius: 34px;
        }
        .slider.round:before {
          border-radius: 50%;
        }
        `;

        // Add the styles to a <style> tag in the document head
        let styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = styles;
        document.head.appendChild(styleSheet);
    }

    // MAIN FUNCTION TO DISPLAY EVERYTHING BASED ON HREF
    function displayData(data, crackingData) {
        let href = window.location.href;
        let currentHref = href;

        let crimes = ['Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing',
                      'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming'];

        if (!settings || Object.keys(settings).length === 0) {
            settings = {};

            for (let crime of crimes) {
                settings[crime] = true;
                if (crime === 'Graffiti' || crime === 'Card Skimming' || crime === 'Burglary' || crime === 'Hustling' || crime === 'Scamming') settings[crime] = false;
            }
        }

        if (href.includes('searchforcash')) {
            if (settings['Search for Cash']) crimePage(data);
        } else if (href.includes('bootlegging')) {
            if (settings.Bootlegging) bootlegging(data);
        } else if (href.includes('graffiti')) {
            if (settings.Graffiti) createUnavailable();
            //crimePage(data);
        } else if (href.includes('shoplifting')) {
            if (settings.Shoplifting) crimePage(data);
        } else if (href.includes('pickpocketing')) {
            if (settings.Pickpocketing) crimePage(data);
        } else if (href.includes ('cardskimming')) {
            // Cannot be calculated as per Emforus
            if (settings['Card Skimming']) createUnavailable();
        } else if (href.includes('burglary')) {
            // On my to do list for if the sheet gets updated
            if (settings.Burglary) createSheetsButton(); // Creates a button that links to the google sheets for the meantime
        } else if (href.includes('hustling')) {
            // Cannot be calculated as per Emforus
            if (settings.Hustling) createUnavailable();
        } else if (href.includes('disposal')) {
            if (settings.Disposal) disposal(data);
        } else if (href.includes('cracking')) {
            if (settings.Cracking) crimePage(data, crackingData);
        } else if (href.includes('forgery')) {
            if (settings.Forgery) crimePage(data);
        } else if (href.includes('scamming')) {
            // Cannot be calculated as per Emforus
            if (settings.Scamming) createUnavailable();
        } else {
            setTimeout(() => createSettingsButton(), 500);
            if (settings.Overview) crimeHub(data);
        }
    }

    fetchData();

    // Checking arrows and document click handler only work like half of the time, so interval
    setInterval(handlePageChange, 200);


})();