Markethunt plugin for Mousehunt

Adds a price chart and Markethunt integration to the MH marketplace screen.

// ==UserScript==
// @name         Markethunt plugin for Mousehunt
// @author       Program
// @namespace    https://greasyfork.org/en/users/886222-program
// @license      MIT
// @version      1.7.0
// @description  Adds a price chart and Markethunt integration to the MH marketplace screen.
// @resource     jq_confirm_css https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.css
// @resource     jq_toast_css https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.css
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/highcharts/9.3.2/highstock.min.js
// @include      https://www.mousehuntgame.com/*
// @grant        GM_addStyle
// @grant        GM_getResourceText
//
// ==/UserScript==

const markethuntDomain = 'markethunt.win';
const markethuntApiDomain = 'api.markethunt.win';

MutationObserver =
    window.MutationObserver ||
    window.WebKitMutationObserver ||
    window.MozMutationObserver;

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const RoundToIntLocaleStringOpts = {
    maximumFractionDigits: 0
}

const isDarkMode = (() => {
    let isDark = null;

    return () => {
        if (isDark == null) {
            isDark = !!getComputedStyle(document.documentElement).getPropertyValue('--mhdm-white');
        }

        return isDark;
    }
})();

/*******************************
 * 
 *  Plugin settings  
 * 
 *******************************/

class SettingsController {
    // TODO: make settings property private and convert init into static initializer once greasyfork adds support
    static settings;

    static init() {
        let settingsObj = {};

        if (localStorage.markethuntSettings !== undefined) {
            settingsObj = JSON.parse(localStorage.markethuntSettings);
        }

        this.settings = new Proxy(settingsObj, {
            set(obj, prop, value) {
                obj[prop] = value;
                localStorage.markethuntSettings = JSON.stringify(obj);
                return true;
            }
        });
    }

    static getStartChartAtZero() {
        if (this.settings.startChartAtZero === undefined) {
            return false;
        } else {
            return this.settings.startChartAtZero;
        }
    }

    static setStartChartAtZero(value) {
        this.settings.startChartAtZero = value;
    }

    static getEnablePortfolioButtons() {
        if (this.settings.enablePortfolioButtons === undefined) {
            return true;
        } else {
            return this.settings.enablePortfolioButtons;
        }
    }

    static setEnablePortfolioButtons(value) {
        this.settings.enablePortfolioButtons = value;
    }

    static getEnableFloatingVolumeLabels() {
        if (this.settings.enableFloatingVolumeLabels === undefined) {
            return false;
        } else {
            return this.settings.enableFloatingVolumeLabels;
        }
    }

    static setEnableFloatingVolumeLabels(value) {
        this.settings.enableFloatingVolumeLabels = value;
    }

    static getEnableChartAnimation() {
        if (this.settings.enableChartAnimation === undefined) {
            return true;
        } else {
            return this.settings.enableChartAnimation;
        }
    }

    static setEnableChartAnimation(value) {
        this.settings.enableChartAnimation = value;
    }
}

SettingsController.init();

function openPluginSettings() {
    $.alert({
        title: 'Markethunt Plugin Settings',
        content: `
            <div id="markethunt-settings-container">
                <h1>Chart settings</h1>
                <label for="checkbox-start-chart-at-zero" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-start-chart-at-zero" type="checkbox" ${SettingsController.getStartChartAtZero() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Y-axis starts at 0</b><br>
                        Make the Y-axis start at 0 gold/SB
                    </div>
                </label>
                <label for="checkbox-enable-floating-volume-labels" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-enable-floating-volume-labels" type="checkbox" ${SettingsController.getEnableFloatingVolumeLabels() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Volume labels</b><br>
                        Place floating labels indicating volume amount on the left side of the chart
                    </div>
                </label>
                <label for="checkbox-enable-chart-animation" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-enable-chart-animation" type="checkbox" ${SettingsController.getEnableChartAnimation() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Chart animation</b><br>
                        Enable chart animations
                    </div>
                </label>
                <h1>Other settings</h1>
                <label for="checkbox-enable-portfolio-buttons" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-enable-portfolio-buttons" type="checkbox" ${SettingsController.getEnablePortfolioButtons() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Portfolio quick-add buttons</b><br>
                        Place "Add to portfolio" buttons in your marketplace history and journal log
                    </div>
                </label>
            </div>
        `,
        boxWidth: '450px',
        useBootstrap: false,
        closeIcon: true,
        draggable: true,
        onOpen: function(){
            const startChartAtZeroCheckbox = document.getElementById("checkbox-start-chart-at-zero");
            startChartAtZeroCheckbox.addEventListener('change', function(event) {
                SettingsController.setStartChartAtZero(event.currentTarget.checked);
            });

            const enablePortfolioButtonsCheckbox = document.getElementById("checkbox-enable-portfolio-buttons");
            enablePortfolioButtonsCheckbox.addEventListener('change', function(event) {
                SettingsController.setEnablePortfolioButtons(event.currentTarget.checked);
            });

            const enableFloatingVolumeLabelsCheckbox = document.getElementById("checkbox-enable-floating-volume-labels");
            enableFloatingVolumeLabelsCheckbox.addEventListener('change', function(event) {
                SettingsController.setEnableFloatingVolumeLabels(event.currentTarget.checked);
            });

            const enableChartAnimationCheckbox = document.getElementById("checkbox-enable-chart-animation");
            enableChartAnimationCheckbox.addEventListener('change', function(event) {
                SettingsController.setEnableChartAnimation(event.currentTarget.checked);
            });
        }
    });
}

/*******************************
 * 
 *  Chart functions  
 * 
 *******************************/

// chart vars
const UtcTimezone = "T00:00:00+00:00"

// style
const primaryLineColor = "#4f52aa";
const secondaryLineColor = "#b91a05";
const sbiLineColor = "#00c000"
const volumeColor = "#51cda0";
const volumeLabelColor = '#3ab28a';

const eventBandColor = "#f2f2f2";
const eventBandFontColor = "#888888"; // recommend to have same or close color as yGridLineColor for visual clarity
const xGridLineColor = "#bbbbbb";
const yGridLineColor = "#aaaaaa";
const yGridLineColorLighter = "#dddddd";
const axisLabelColor = "#444444";
const crosshairColor = "#252525";

// dark mode style overrides
const darkMode = {
    backgroundColor: "#222222",
    eventBandColor: "#303030",
    eventBandFontColor: "#909090",
    primaryLineColor: "#9e8dfc",
    crosshairColor: "#e0e0e0"
}

const chartFont = "tahoma,arial,sans-serif";

// set global opts
Highcharts.setOptions({
    chart: {
        style: {
            fontFamily: chartFont,
        },
        spacingLeft: 0,
        spacingRight: 5,
        spacingTop: 7,
        spacingBottom: 6,
    },
    lang: {
        rangeSelectorZoom :""
    },
    plotOptions: {
        series: {
            showInLegend: true,
        },
    },
    // must keep scrollbar enabled for dynamic scrolling, so hide the scrollbar instead
    scrollbar: {
        height: 0,
        buttonArrowColor: "#ffffff00",
    },
    title: {
        enabled: false,
    },
    credits: {
        enabled: false,
    },
    rangeSelector: {
        buttonPosition: {
            y: 5,
        },
        inputEnabled: false,
        labelStyle: {
            color: axisLabelColor,
        },
        verticalAlign: 'top',
        x: -5.5,
    },
    legend: {
        align: 'right',
        verticalAlign: 'top',
        y: -23,
        padding: 0,
        itemStyle: {
            color: '#000000',
            fontSize: "13px",
        },
    },
    tooltip: {
        animation: false,
        shared: true,
        split: false,
        headerFormat: '<span style="font-size: 11px; font-weight: bold">{point.key}</span><br/>',
        backgroundColor: 'rgba(255, 255, 255, 1)',
        hideDelay: 0, // makes tooltip feel more responsive when crossing gap between plots
        style: {
            color: '#000000',
            fontSize: '11px',
            fontFamily: chartFont,
        }
    },
    navigator: {
        height: 25,
        margin: 0,
        maskInside: false,
        enabled: false,
    },
    xAxis: {
        tickColor: xGridLineColor,
        gridLineColor: xGridLineColor,
        labels: {
            style: {
                color: axisLabelColor,
                fontSize: '11px',
            }
        }
    },
    yAxis: {
        gridLineColor: yGridLineColor,
        labels: {
            style: {
                color: axisLabelColor,
                fontSize: '11px',
            },
            y: 3,
        }
    }
});

function setDarkThemeGlobalOpts() {
    Highcharts.setOptions({
        chart: {
            backgroundColor: darkMode.backgroundColor
        },
        legend: {
            itemStyle: {
                color: '#e0e0e0'
            },
            itemHoverStyle: {
                color: '#f0f0f0'
            },
            itemHiddenStyle: {
                color: '#777777'
            },
            title: {
                style: {
                    color: '#c0c0c0'
                }
            }
        },
        xAxis: {
            gridLineColor: '#707070',
            labels: {
                style: {
                    color: '#e0e0e0'
                }
            },
            lineColor: '#707070',
            minorGridLineColor: '#505050',
            tickColor: '#707070',
        },
        yAxis: {
            gridLineColor: '#707070',
            labels: {
                style: {
                    color: '#e0e0e0'
                }
            },
            lineColor: '#707070',
            minorGridLineColor: '#505050',
            tickColor: '#707070',
        },
        tooltip: {
            backgroundColor: '#000000',
            style: {
                color: '#f0f0f0'
            }
        },
        rangeSelector: {
            buttonTheme: {
                fill: '#444444',
                stroke: '#000000',
                style: {
                    color: '#cccccc'
                },
                states: {
                    hover: {
                        fill: '#707070',
                        stroke: '#000000',
                        style: {
                            color: 'white'
                        }
                    },
                    select: {
                        fill: '#000000',
                        stroke: '#000000',
                        style: {
                            color: 'white'
                        }
                    }
                }
            },
        },
    });
}

function UtcIsoDateToMillis(dateStr) {
    return (new Date(dateStr + UtcTimezone)).getTime();
}

function formatSISuffix(num, decimalPlaces) {
    const suffixes = ["", "K", "M", "B"];
    let order = Math.max(Math.floor(Math.log(num) / Math.log(1000)), 0);
    if (order > suffixes.length - 1) {
        order = suffixes.length - 1;
    }
    let significand = num / Math.pow(1000, order);
    return significand.toFixed(decimalPlaces) + suffixes[order];
}

function eventBand(IsoStrFrom, IsoStrTo, labelText) {
    return {
        from: UtcIsoDateToMillis(IsoStrFrom),
        to: UtcIsoDateToMillis(IsoStrTo),
        color: isDarkMode() ? darkMode.eventBandColor : eventBandColor,
        label: {
            text: labelText,
            rotation: 270,
            textAlign: 'right',
            y: 5, // pixels from top of chart
            x: 4, // fix slight centering issue
            style: {
                color: isDarkMode() ? darkMode.eventBandFontColor : eventBandFontColor,
                fontSize: '12px',
                fontFamily: chartFont,
            },
        },
    }
}

function updateEventData() {
    $.getJSON(`https://${markethuntApiDomain}/events?plugin_ver=${GM_info.script.version}`, function (response) {
        localStorage.markethuntEventDatesV2 = JSON.stringify(response);
        localStorage.markethuntEventDatesV2LastRetrieval = Date.now();
    });
}

function renderChartWithItemId(itemId, containerId, forceRender = false) {
    const containerElement = document.getElementById(containerId);

    if (forceRender === false && containerElement.dataset.lastRendered) {
        return;
    }

    itemId = Number(itemId);
    let eventData = [];

    if (localStorage.markethuntEventDatesV2LastRetrieval !== undefined) {
        JSON.parse(localStorage.markethuntEventDatesV2).forEach(event => eventData.push(eventBand(event.start_date, event.end_date, event.short_name)));

        if (Date.now() - Number(localStorage.markethuntEventDatesV2LastRetrieval) > 2 * 86400 * 1000) {
            updateEventData();
        }
    } else {
        updateEventData();
    }

    function renderChart(response) {
        // set HUD
        if (response.market_data.length > 0) {
            const newestPrice = response.market_data[response.market_data.length - 1];
            const utcTodayMillis = UtcIsoDateToMillis(new Date().toISOString().substring(0, 10));

            const priceDisplay = document.getElementById("infoboxPrice");
            const sbPriceDisplay = document.getElementById("infoboxSbPrice");
            const tradeVolDisplay = document.getElementById("infoboxTradevol");
            const goldVolDisplay = document.getElementById("infoboxGoldvol");
            const weeklyVolDisplay = document.getElementById("infobox7dTradevol");
            const weeklyGoldVolDisplay = document.getElementById("infobox7dGoldvol");

            // set gold price
            priceDisplay.innerHTML = newestPrice.price.toLocaleString();

            // set sb price
            try {
                let sbPriceText;
                let sbPrice = newestPrice.sb_price;
                
                if (sbPrice >= 100) {
                    sbPriceText = Math.round(sbPrice).toLocaleString();
                } else {
                    sbPriceText = sbPrice.toFixed(2).toLocaleString();
                }
                sbPriceDisplay.innerHTML = sbPriceText;
            } catch (e) {
                // do nothing
            }

            // set yesterday's trade volume
            let volText = '0';
            if (utcTodayMillis - UtcIsoDateToMillis(newestPrice.date) <= 86400 * 1000 && newestPrice.volume !== null) {
                volText = newestPrice.volume.toLocaleString();
            }
            tradeVolDisplay.innerHTML = volText;

            // set yesterday's gold volume
            let goldVolText = '0';
            if (utcTodayMillis - UtcIsoDateToMillis(newestPrice.date) <= 86400 * 1000 && newestPrice.volume !== null) {
                goldVolText = formatSISuffix(newestPrice.volume * newestPrice.price, 2);
            }
            goldVolDisplay.innerHTML = goldVolText;

            // set last week's trade volume
            let weeklyVolText = response.market_data.reduce(function(sum, dataPoint) {
                if (utcTodayMillis - UtcIsoDateToMillis(dataPoint.date) <= 7 * 86400 * 1000) {
                    return sum + (dataPoint.volume !== null ? dataPoint.volume : 0);
                } else {
                    return sum;
                }
            }, 0);
            weeklyVolDisplay.innerHTML = weeklyVolText.toLocaleString();

            // set last week's gold volume
            let weeklyGoldVol = response.market_data.reduce(function(sum, dataPoint) {
                if (utcTodayMillis - UtcIsoDateToMillis(dataPoint.date) <= 7 * 86400 * 1000) {
                    return sum + (dataPoint.volume !== null ? dataPoint.volume * dataPoint.price : 0);
                } else {
                    return sum;
                }
            }, 0);
            weeklyGoldVolDisplay.innerHTML = (weeklyGoldVol === 0) ? '0' : formatSISuffix(weeklyGoldVol, 2);
        }

        // process data for highcharts
        var dailyPrices = [];
        var dailyVolumes = [];
        var dailySbPrices = [];
        for (var i = 0; i < response.market_data.length; i++) {
            dailyPrices.push([
                UtcIsoDateToMillis(response.market_data[i].date),
                Number(response.market_data[i].price)
            ]);
            dailyVolumes.push([
                UtcIsoDateToMillis(response.market_data[i].date),
                Number(response.market_data[i].volume)
            ]);
            dailySbPrices.push([
                UtcIsoDateToMillis(response.market_data[i].date),
                Number(response.market_data[i].sb_price)
            ]);
        }

        if (isDarkMode()) {
            setDarkThemeGlobalOpts();
        }

        // Create the chart
        let chart = new Highcharts.stockChart(containerId, {
            chart: {
                // zoom animations
                animation: SettingsController.getEnableChartAnimation() ? { 'duration': 500 } : false,
            },
            plotOptions: {
                series: {
                    // initial animation
                    animation: SettingsController.getEnableChartAnimation() ? { 'duration': 900 } : false,
                    dataGrouping: {
                        enabled: itemId === 114,
                        units: [['day', [1]], ['week', [1]]],
                        groupPixelWidth: 3,
                    },
                },
            },
            rangeSelector: {
                buttons: [
                    {
                        type: 'month',
                        count: 1,
                        text: '1M'
                    }, {
                        type: 'month',
                        count: 3,
                        text: '3M'
                    }, {
                        type: 'month',
                        count: 6,
                        text: '6M'
                    }, {
                        type: 'year',
                        count: 1,
                        text: '1Y',
                    }, {
                        type: 'all',
                        text: 'All'
                    },
                ],
                selected: 3,
            },
            legend: {
                enabled: true
            },
            tooltip: {
                xDateFormat: '%b %e, %Y',
            },
            series: [
                {
                    name: 'Average price',
                    id: 'dailyPrice',
                    data: dailyPrices,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 0,
                    color: isDarkMode() ? darkMode.primaryLineColor : primaryLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString()}g</b><br/>`;
                        },
                    },
                    zIndex: 1,
                }, {
                    name: 'Volume',
                    type: 'column',
                    data: dailyVolumes,
                    pointPadding: 0, // disable point and group padding to simulate column area chart
                    groupPadding: 0,
                    yAxis: 2,
                    color: volumeColor,
                    tooltip: {
                        pointFormatter: function() {
                            let volumeAmtText = this.y !== 0 ? this.y.toLocaleString() : 'n/a';
                            return `<span style="color:${this.color}">\u25CF</span>`
                                    + ` ${this.series.name}:`
                                    + ` <b>${volumeAmtText}</b><br/>`;
                        },
                    },
                    zIndex: 0,
                }, {
                    name: 'SB Price',
                    id: 'sbi',
                    data: dailySbPrices,
                    visible: false,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 1,
                    color: sbiLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            let sbiText;

                            if (this.y >= 1000) {
                                sbiText = Math.round(this.y).toLocaleString();
                            } else if (this.y >= 100) {
                                sbiText = this.y.toFixed(1).toLocaleString();
                            } else if (this.y >= 10) {
                                sbiText = this.y.toFixed(2).toLocaleString();
                            } else {
                                sbiText = this.y.toFixed(3).toLocaleString();
                            }
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` SB Index:`
                                + ` <b>${sbiText} SB</b><br/>`;
                        },
                    },
                    zIndex: 2,
                },
            ],
            yAxis: [
                {
                    min: SettingsController.getStartChartAtZero() ? 0 : null,
                    labels: {
                        formatter: function() {
                            return this.value.toLocaleString() + 'g';
                        },
                        x: -8,
                    },
                    showLastLabel: true, // show label at top of chart
                    crosshair: {
                        dashStyle: 'ShortDot',
                        color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
                    },
                    opposite: false,
                    alignTicks: false, // disabled, otherwise autoranger will create too large a Y-window
                }, {
                    min: SettingsController.getStartChartAtZero() ? 0 : null,
                    gridLineWidth: 0,
                    labels: {
                        formatter: function() {
                            return this.value.toLocaleString() + ' SB';
                        },
                        x: 5,
                    },
                    showLastLabel: true, // show label at top of chart
                    opposite: true,
                    alignTicks: false,
                }, {
                    top: '75%',
                    height: '25%',
                    offset: 0,
                    min: 0,
                    opposite: false,
                    tickPixelInterval: 35,
                    allowDecimals: false,
                    alignTicks: false,
                    gridLineWidth: 0,
                    labels: {
                        enabled: SettingsController.getEnableFloatingVolumeLabels(),
                        align: 'left',
                        x: 0,
                        style: {
                            color: volumeLabelColor,
                        },
                    },
                    showLastLabel: true,
                    showFirstLabel: false,
            }],
            xAxis: {
                type: 'datetime',
                ordinal: false, // show continuous x axis if dates are missing
                plotBands: eventData,
                crosshair: {
                    dashStyle: 'ShortDot',
                    color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
                },
                dateTimeLabelFormats:{
                    day: '%b %e',
                    week: '%b %e, \'%y',
                    month: '%b %Y',
                    year: '%Y'
                },
                tickPixelInterval: 120,
            },
        });

        containerElement.dataset.lastRendered = Date.now().toString();
    }

    $.getJSON(`https://${markethuntApiDomain}/items/${itemId}?plugin_ver=${GM_info.script.version}`, function (response) {
        renderChart(response);
    });
}

function renderStockChartWithItemId(itemId, containerId, forceRender = false) {
    const containerElement = document.getElementById(containerId);

    if (forceRender === false && containerElement.dataset.lastRendered) {
        return;
    }

    itemId = Number(itemId);
    let eventData = [];

    if (localStorage.markethuntEventDatesV2LastRetrieval !== undefined) {
        JSON.parse(localStorage.markethuntEventDatesV2).forEach(event => eventData.push(eventBand(event.start_date, event.end_date, event.short_name)));
    }

    function renderStockChart(response) {
        const bid_data = [];
        const ask_data = [];
        const supply_data = [];

        response.stock_data.forEach(x => {
            bid_data.push([x.timestamp, x.bid]);
            ask_data.push([x.timestamp, x.ask]);
            supply_data.push([x.timestamp, x.supply]);
        })

        if (isDarkMode()) {
            setDarkThemeGlobalOpts();
        }

        // Create the chart
        let chart = new Highcharts.stockChart(containerId, {
            chart: {
                // zoom animations
                animation: SettingsController.getEnableChartAnimation() ? { 'duration': 500 } : false,
            },
            plotOptions: {
                series: {
                    // initial animation
                    animation: SettingsController.getEnableChartAnimation() ? { 'duration': 900 } : false,
                    dataGrouping: {
                        enabled: true,
                        units: [['hour', [2, 4, 6]], ['day', [1]], ['week', [1]]],
                        groupPixelWidth: 2,
                        dateTimeLabelFormats: {
                            hour: ['%b %e, %Y %H:%M UTC', '%b %e, %Y %H:%M UTC'],
                            day: ['%b %e, %Y']
                        }
                    },
                },
            },
            rangeSelector: {
                buttons: [
                    {
                        type: 'day',
                        count: 7,
                        text: '7D'
                    }, {
                        type: 'month',
                        count: 1,
                        text: '1M'
                    }, {
                        type: 'month',
                        count: 3,
                        text: '3M'
                    }, {
                        type: 'month',
                        count: 6,
                        text: '6M'
                    }, {
                        type: 'year',
                        count: 1,
                        text: '1Y'
                    }, {
                        type: 'all',
                        text: 'All'
                    },
                ],
                selected: 1,
            },
            legend: {
                enabled: true
            },
            tooltip: {
                xDateFormat: '%b %e, %Y %H:%M UTC',
            },
            series: [
                {
                    name: 'Ask',
                    id: 'ask',
                    data: ask_data,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 0,
                    color: isDarkMode() ? darkMode.primaryLineColor : primaryLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}g</b><br/>`;
                        },
                    },
                    zIndex: 1,
                }, {
                    name: 'Bid',
                    id: 'bid',
                    data: bid_data,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 0,
                    color: secondaryLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}g</b><br/>`;
                        },
                    },
                    zIndex: 2,
                }, {
                    name: 'Supply',
                    id: 'supply',
                    data: supply_data,
                    type: 'area',
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 1,
                    color: volumeColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}</b><br/>`;
                        },
                    },
                    zIndex: 0,
                },
            ],
            yAxis: [
                {
                    min: SettingsController.getStartChartAtZero() ? 0 : null,
                    labels: {
                        formatter: function() {
                            return this.value.toLocaleString() + 'g';
                        },
                        x: -8,
                    },
                    showLastLabel: true, // show label at top of chart
                    opposite: false,
                    alignTicks: false
                }, {
                    top: '75%',
                    height: '25%',
                    offset: 0,
                    min: 0,
                    opposite: false,
                    tickPixelInterval: 35,
                    allowDecimals: false,
                    alignTicks: false,
                    gridLineWidth: 0,
                    labels: {
                        enabled: SettingsController.getEnableFloatingVolumeLabels(),
                        align: 'left',
                        x: 0,
                        style: {
                            color: volumeLabelColor,
                        },
                    },
                    showLastLabel: true,
                    showFirstLabel: false,
                }],
            xAxis: {
                type: 'datetime',
                ordinal: false, // show continuous x axis if dates are missing
                plotBands: eventData,
                crosshair: {
                    dashStyle: 'ShortDot',
                    color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
                },
                dateTimeLabelFormats:{
                    day: '%b %e',
                    week: '%b %e, \'%y',
                    month: '%b %Y',
                    year: '%Y'
                },
                tickPixelInterval: 120,
            }
        });

        containerElement.dataset.lastRendered = Date.now().toString();
    }

    $.getJSON(`https://${markethuntApiDomain}/items/${itemId}/stock?&plugin_ver=${GM_info.script.version}`, function (response) {
        renderStockChart(response);
    });
}

if (localStorage.markethuntEventDatesV2LastRetrieval === undefined) {
    updateEventData();
}

/*******************************
 * 
 *  Marketplace view observer  
 * 
 *******************************/

// chart-icon-90px.png minified with TinyPNG then converted to base 64
const chartIconImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAMAAAAPdrEwAAAAVFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU4H24AAAAG3RSTlMABvTkbYjGz8kamvoO0EYg6H0koqabYVSqNDILUie0AAABU0lEQVRYw+2V2Y6DMAx" +
    "FyUIgwLB1mxn//3+OhEQd6o5EypUqVT6PoBwcbmIXiqIo78b42traG7w5ftFCFeE1L+bFja7b0x0PVtesDmC1ZbWFpS/V2PQnYgIyfXOmBA9MP3KG/HlI+r9uY46vpj+It7f1tQvWhryL3lNC2" +
    "wz/BFhn3/CuoS3taU4CPK2Pv7tcc++IYbkIsDxaMv+W+RpGG9wawQ1Q8lPcT27JF147BTuG69y0qZEDzOsYYeKSm3tEwxP52WR2DMb1BSPlU3bHECULeX6f86JkwWBfU9ep+dIhZ4olpsdOwj2" +
    "bNdVjCzWlowVXmmMDNFYPLbTkVeXBsW/8toW6JPli12Z3QwnFns2i1bxZvJqR6cPUMn2UWqY/gtWUoGqxTJwZlFqeGZhanhmwmhI+Xi3SR6hl+jC1TB+spgRVq1rVqla1qlUNUSuKooD4Az6O4" +
    "MtRLQLhAAAAAElFTkSuQmCC";

// sb.png minified with TinyPNG then converted to base 64
const sbImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAABcVBMVEX+/v/5///+/fzwyJTuxJD1////6cn416z31ab206SOi4ny///t///b" +
    "///J///2/P/9/Pj4+Pjq6/b8+PPw8PPh4+7+9+3K5s//7M7/6Mb+5cP63bfv17b53LP21an1zJzWuprzzJDRrovpuoHmuXzktXu6k3Rd0HOzi2vp///Q//+z//+t///n/v/0+P/q7v/t7v79" +
    "+PjW//P/+/P2+vHZ7PHn6e3/+eft5+f/9ubi8eT/9OPw59//8t377dzh3tz+7dXN7dTa09Sv1tP35tHO1czX4cu+3cLazcLCysL44L3Uw7vDuLj32rar3LGvtbHq167gyK6S16nlyanl06je" +
    "w6jy06fxz6Z8oqSooKDszJ7Ls56MypvoxZqel5fQsZblwJWVkJB0xY3nvI10y4vswIv30IrvwInRp4jxyYeIg4XdtYPQq4LNpH65mnt9e3u+sXr0xHjZq3dZwHTPonR1dXHgqnC6kGtpbGnh" +
    "qWEs0UvWjFe8AAAA4klEQVQY02PACvgYITSvlbo4mCEY4V9awZUf4+ieUqUOFmFK5OKKjMtKCioW9zPRBAowAhFIJUSnFhBrczMwAJGIkKiomQhIkFWHj0GXQc+An4df3yfPlRUoxMNgaGFv" +
    "6uTpHF1SpqIA0StWWaCqzBwlL8+RngFxhnlhSJiblxSbhCRzEViE1ShNWlaGnZMzIFU1HqLLWFGOnZOZmYWFRcUD6g1FFg52DrnY3HINIahIpnJ2jpqGmlJCsjdUJFBJIViGTZJNOjwUKiLr" +
    "KyXhYGtpbediAxURExYWYGIAQgGgDwEEwCDFO/6WiQAAAABJRU5ErkJggg==";

const settingsImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAACKElEQVR4nO2YMUucQRCGH9Cgoi" +
    "YgNolVqhAVm6SyNoGAyP0Q8TT+ABG0C3ZRbMU2gprC4vA/HNx5tQZTxSimCDHKJwsjHEe8b2bX1cV8DwzICTPvu+x+M7tQUFBwl3wFspzYI2G+Kwz8AjpJkCGF+Jt4Q4JMGQzMkiCLBgNfSIhhYAP4" +
    "bTBwBWwCr+5D4AegDiyI2Bu6gU/ApUF4a1wCK0BPy4IsSE1XO4gnQKOlaA1YAg4ChLdGQ3LW/vG70+DN3B2K9I1ZX/EDwEkCBk6BQR8DawmIzyRWreLdQfqbgPCs6bCPWQxUAorVZN+OAL0SI3Ke6g" +
    "F5K1rxJc8Cf4BpoKNNbve/GeDCs0ZJY+A1cOghfkK7QsA7DxOHok3FC6BqSO5W3krZkL8qmkw8A/aVe77dtrkNN1JrmuG+aPGiCziKOFl+zMl9JBqCOMsp0jwfWRlVNLFgrnKK9AXk7ld8/6MbcCJ8" +
    "eXofBk5zirgmlewW6lIcYtdhfZmPeYi1n9F6wGe0Eeszam1kbjyIedeoWhqZzyhxIeOBlvcxR4mSMXGziXLOo1WnrHzUYS50nK5Lhx2VHtEnf88H3qMr/E8XGuQalyUSn/G81P9IQPxP30s9Mmk+tI" +
    "EykR62NE1IG9+A5RgPW+2eFnvkWTDkaTEDtoHnsZ4WNbwE1o1G3IS7C4yTEFsGAzskyJzBgNsiyfHWYGCSBOkAzpUGmg9tUuwpxB8/tMiCgsfENevgYdmM/xZUAAAAAElFTkSuQmCC";

const kofiImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAABm1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8AAAD/Xluzs7PIyMj/XV7/Wmz/W2b/XW" +
    "D//f3/WXH/Wm7/W2n/XGT8/Pz/+Pn/XGLQ0ND/W2q6urq3t7esrKympqb/V3j/WHX/+vr19fX/ydDCwsKwsLCioqKMjIx9fX1YWFhKSko7Ozv9/f3//Pz5+fn/9fXm5ub/2tzb29v/0dXNzc3Ex" +
    "MT/pqf/mqX/kKX/n56enp6RkZH/kpD/joz/i4mIiIj/h4b/hIJubm5iYmJNTU0jIyMeHh4PDw8MDAz/4+TS0tL/xcj/u8jFxcW+vr7/rbT/lqz/k6D/jZ3/gZz/i5r/ipmYmJiXl5f/mJaVlZX/" +
    "lJWUlJT/kJP/eYz/comCgoJ5eXl0dHRra2s0NDQsLCwnJycTExMRERH4jQN3AAAAKXRSTlMAiqaR9J+W+/h6dQ+5rI6Jg4BvZ1ZJNycH1svGwmM9MyshCe/ksqE/Gb0RW4gAAAH8SURBVEjH7dR" +
    "nVxpBGIbhiVnpKki1JWrq+4KKi7pgaAqIIGrsGks09vTee//ZbmFndhTQ73p9fu4zs+fsLrlwnvhdl2posnnthNN5DU8h1PuNQSNuh2oY+7jzDy1WwtTjItSWGT/Ahm5D0AOnmfuDJhrU1Qgy28" +
    "XirzEJYGYfvWcJelBRmgFIYOvxQJx/8XrlSRTE7Kuvb7JiOXD4PO1YmobJ3+ZOGiyo+5V0Oj0y8m32bWSgr+/OsqgFdYQE2jEP8B3dNBgGWVZeRyKRAWU9OBgOP6YBuYITACE08cGyYT00FOv/w" +
    "gK78BdgGBv44KW2Dqvr/t7eLRZ0C3s0YA/93LgOBoP3jl0pgSY+eCjfJKatVbM0sLdiCqCAbj6ApZg81tbqAVrQ1uG8jrsSTO9ZuojmZjkY/aSuNfdHQbGAMvNOBmANb7B36SloxQbdP4iCSlor" +
    "bK7OAUDu0OJnQQ400Q19fxc4k6lDs5voTPgM9GJd3W/xeym3i+ZmYgjioJv6oNw/yrbvN38U9xFbbnFfXAgocSm4zvawiDLB4QkQgyZMAiPOTwETQofHZyc8F+ahmnGkd2c68CdUIZXw6sngtnD" +
    "wqOoBbaQCG/4vJOLxeKL8X0kmk6lUfvXd5wkU6AEcqwUra/GRyrqaL+salb+j0+myWm02b4BcOK+OAN6AtFxkFAmAAAAAAElFTkSuQmCC";

const mpObserverTarget = document.querySelector("#overlayPopup");

function CurrentViewedItemId() {
    const itemIdRaw = mpObserverTarget.querySelector(".marketplaceView-item.view")?.dataset.itemId;
    return itemIdRaw ? Number(itemIdRaw) : null;
}

const mpObserver = new MutationObserver(function () {
    // Check if the Marketplace interface is open
    if (!mpObserverTarget.querySelector(".marketplaceView")) {
        return;
    }

    // detect item page and inject chart
    const backButton = mpObserverTarget.querySelector("a.marketplaceView-breadcrumb");
    if (backButton) {
        const targetContainer = mpObserverTarget.querySelector(".marketplaceView-item-description");

        if (targetContainer && !mpObserverTarget.querySelector("#chartArea")) {
            // Disconnect and reconnect later to prevent mutation loop
            mpObserver.disconnect();

            // Setup chart divs
            const itemId = CurrentViewedItemId();

            targetContainer.insertAdjacentHTML(
                "beforebegin",
                `<div id="chartArea" style="display: flex; padding: 0 20px 0 20px; height: 315px;">
                    <div style="flex-grow: 1; position: relative">
                        <div id="chartContainer" style="text-align: center; height: 100%; width: 100%">
                            <img style="opacity: 0.07; margin-top: 105px" src="${chartIconImageData}" alt="Chart icon" class="${isDarkMode() ? 'inverted' : ''}">
                            <div style="color: grey">Loading ...</div>
                        </div>
                        <div id="stockChartContainer" style="text-align: center; position: absolute; background-color: ${isDarkMode() ? darkMode.backgroundColor : 'white'}; top: 0; left: 0; width: 100%; height: 100%; display: none">
                            <img style="opacity: 0.07; margin-top: 105px" src="${chartIconImageData}" alt="Chart icon" class="${isDarkMode() ? 'inverted' : ''}">
                            <div style="color: grey">Loading ...</div>
                        </div>
                    </div>
                    <div id="markethuntInfobox" style="text-align: center; display: flex; flex-direction: column; padding: 34px 0 12px 5px; position: relative;">
                        <div class="marketplaceView-item-averagePrice infobox-stat infobox-small-spans infobox-striped">
                            Trade volume:<br>
                            <span id="infoboxTradevol">--</span><br>
                            <span id="infoboxGoldvol" class="marketplaceView-goldValue">--</span>
                        </div>
                        <div class="marketplaceView-item-averagePrice infobox-stat infobox-small-spans">
                            7-day trade volume:<br>
                            <span id="infobox7dTradevol">--</span><br>
                            <span id="infobox7dGoldvol" class="marketplaceView-goldValue">--</span>
                        </div>
                        <div style="user-select: none; text-align: left">
                            <label class="cl-switch" for="markethuntShowStockData" style="cursor: pointer">
                                <input type="checkbox" id="markethuntShowStockData">
                                <span class="switcher"></span>
                                <span class="label">Stock chart</span>
                            </label>
                        </div>
                        <div style="flex-grow: 1"></div> <!-- spacer div -->
                        <div>
                            <div style="display: flex;">
                                <div style="flex-grow: 1;"></div>
                                <div>
                                    <a id="markethuntSettingsLink" href="#">
                                        <img src="${settingsImageData}" class="markethunt-settings-btn-img ${isDarkMode() ? 'inverted' : ''}">
                                    </a>
                                </div>
                                <div>
                                    <a href="https://ko-fi.com/vsong_program" target="_blank" alt="Donation Link">
                                        <img src="${kofiImageData}" class="markethunt-settings-btn-img" alt="Settings">
                                    </a>
                                </div>
                                <div style="flex-grow: 1;"></div>
                            </div>
                            <div style="font-size: 0.8em; color: grey">v${GM_info.script.version}</div>
                        </div>
                    </div>
                </div>`
            );

            const itemPriceContainer = mpObserverTarget.querySelector(".marketplaceView-item-averagePrice");
            itemPriceContainer.classList.add("infobox-stat");
            itemPriceContainer.insertAdjacentHTML(
                "beforeend",
                `<br><span id="infoboxSbPrice" class="marketplaceView-sbValue">--</span><img style="vertical-align: bottom" src="${sbImageData}" alt="SB icon" />`
            );

            const itemPriceDisplay = itemPriceContainer.querySelector("span");
            itemPriceDisplay.id = "infoboxPrice";

            const infoBox = document.getElementById("markethuntInfobox");
            infoBox.prepend(itemPriceContainer);

            // Set infobox minimum width to prevent layout shifts, *then* reset price display
            const infoBoxInitialWidth = $(infoBox).width();
            infoBox.style.minWidth = `${infoBoxInitialWidth}px`;

            itemPriceDisplay.innerHTML = "--";

            // Set stock chart checkbox listener
            const stockChartCheckbox = document.getElementById('markethuntShowStockData');
            stockChartCheckbox?.addEventListener('change', (e) => {
                if (e.target.checked) {
                    document.getElementById('stockChartContainer').style.display = 'block';
                    renderStockChartWithItemId(itemId, 'stockChartContainer');
                } else {
                    document.getElementById('stockChartContainer').style.display = 'none';
                    renderChartWithItemId(itemId, 'chartContainer');
                }
            });

            // Set Plugin Settings listener
            const settingsLink = document.getElementById("markethuntSettingsLink");
            settingsLink.addEventListener('click', openPluginSettings);

            // Render chart
            renderChartWithItemId(itemId, "chartContainer");

            // Re-observe after mutation-inducing logic
            mpObserver.observe(mpObserverTarget, {
                childList: true,
                subtree: true
            });
        }
    }

    // detect history page and inject portfolio buttons
    const historyTab = mpObserverTarget.querySelector("[data-tab=history].active");
    if (SettingsController.getEnablePortfolioButtons() && historyTab) {
        mpObserver.disconnect();

        let rowElem = mpObserverTarget.querySelectorAll(".marketplaceMyListings tr.buy");
        rowElem.forEach(function(row) {
            if (!row.querySelector(".mousehuntActionButton.tiny.addPortfolio")) {
                let itemElem = row.querySelector(".marketplaceView-itemImage");
                const itemId = itemElem.getAttribute("data-item-id");

                let qtyElem = row.querySelector("td.marketplaceView-table-numeric");
                const qty = Number(qtyElem.innerText.replace(/\D/g, ''));

                let priceElem = row.querySelector("td.marketplaceView-table-numeric .marketplaceView-goldValue");
                const price = Number(priceElem.innerText.replace(/\D/g, ''));

                let buttonContainer = row.querySelector("td.marketplaceView-table-actions");
                let addPortfolioBtn = document.createElement("a");
                addPortfolioBtn.href = `https://${markethuntDomain}/portfolio.php?action=add_position&item_id=${itemId}&add_qty=${qty}&add_mark=${price}`;
                addPortfolioBtn.innerHTML = "<span>+ Portfolio</span>";
                addPortfolioBtn.className = "mousehuntActionButton tiny addPortfolio lightBlue";
                addPortfolioBtn.target = "_blank";
                addPortfolioBtn.style.display = "block";
                addPortfolioBtn.style.marginTop = "2px";
                buttonContainer.appendChild(addPortfolioBtn);
            }
        });

        mpObserver.observe(mpObserverTarget, {
            childList: true,
            subtree: true
        });
    }
});

// Initial observe
mpObserver.observe(mpObserverTarget, {
    childList: true,
    subtree: true
});

const marketplaceCssOverrides = `
.marketplaceView-item {
    padding-top: 10px;
}
.marketplaceView-item-content {
    padding-top: 10px;
    padding-bottom: 0px;
    min-height: 0px;
}
.marketplaceView-item-descriptionContainer {
    padding-bottom: 5px;
    padding-top: 5px;
}
.marketplaceView-item-averagePrice {
    margin-top: 5px;
}
.marketplaceView-item-footer {
    padding-top: 10px;
    padding-bottom: 10px;
}
.markethunt-cross-link {
    color: #000;
    font-size: 10px;
    background: #fff;
    display: block;
    padding: 0.1em 0;
    margin: 0.4em 0;
    box-shadow: #797979 1px 1px 1px 0;
    border-radius: 0.2em;
    border: 1px solid #a8a8a8;
}
.markethunt-cross-link:hover {
    color: white;
    background-color: ${primaryLineColor};
}
.markethunt-settings-btn-img {
    height: 26px;
    padding: 3px;
    margin: 0 5px 0 5px;
    border-radius: 999px;
    box-shadow: 0px 0px 3px gray;
}
.markethunt-settings-btn-img:hover {
    box-shadow: 0px 0px 3px 1px gray;
}
.inverted {
    filter: invert(1);
}
.markethunt-settings-row-input {
    display: flex;
    align-items: center;
    padding-right: 5px;
}
.markethunt-settings-row {
    display: flex;
    padding: 5px;
}
.markethunt-settings-row-description {
}
.marketplaceView-item-averagePrice.infobox-stat {
    text-align: left;
    margin-bottom: 14px;
    white-space: nowrap;
}
.marketplaceView-item-leftBlock .marketplaceHome-block-viewAll {
    margin-top: 5px;
}
.infobox-striped {
}
.infobox-small-spans span {
    font-size: 11px;
}
.infobox-small-spans .marketplaceView-goldValue::after {
    width: 17px;
    height: 13px;
}
`;

const materialSwitchCss = `
.cl-switch input[type="checkbox"] {
    display: none;
    visibility: hidden;
}

.cl-switch .switcher {
    display: inline-block;
    border-radius: 100px;
    width: 2.25em;
    height: 1em;
    background-color: #ccc;
    position: relative;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    vertical-align: middle;
    cursor: pointer;
}

.cl-switch .switcher:before {
    content: "";
    display: block;
    width: 1.3em;
    height: 1.3em;
    background-color: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    border-radius: 50%;
    margin-top: -0.15em;
    position: absolute;
    top: 0;
    left: 0;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    margin-right: 0;
    -webkit-transition: all 0.2s;
    -moz-transition: all 0.2s;
    -ms-transition: all 0.2s;
    -o-transition: all 0.2s;
    transition: all 0.2s;
}

.cl-switch .switcher:active:before {
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 0 0 1em rgba(63, 81, 181, 0.3);
    transition: all, 0.1s;
}

.cl-switch .label {
    cursor: pointer;
    vertical-align: middle;
}

.cl-switch input[type="checkbox"]:checked+.switcher {
    background-color: #8591d5;
}

.cl-switch input[type="checkbox"]:checked+.switcher:before {
    left: 100%;
    margin-left: -1.3em;
    background-color: #3f51b5;
}

.cl-switch [disabled]:not([disabled="false"])+.switcher {
    background: #ccc !important;
}

.cl-switch [disabled]:not([disabled="false"])+.switcher:active:before {
    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important;
}

.cl-switch [disabled]:not([disabled="false"])+.switcher:before {
    background-color: #e2e2e2 !important;
    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important;
}`;

/*******************************
 * 
 *  Journal observer  
 * 
 *******************************/

// add_portfolio_journal.png minified with TinyPNG then converted to base 64
const addPfolioBtnImgData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAALBAMAAACEzBAKAAAAFVBMVEUAAAAAAAD/swD06DD6zhj///8PDgMsru0CAAAAAXRSTlMA" +
      "QObYZgAAADhJREFUCNdjCBQEAwEGYWMwMERmmBkbCoIZhkAobMgg4igo6Cjo4sggpKQIhEqKyAwgQGEwQk0GAIl6DBhSGEjXAAAAAElFTkSuQmCC";

function addJournalButtons(supplyTransferJournalEntries) {
    supplyTransferJournalEntries.forEach(function(supplyTransferEntry) {
        const journalActionsElem = supplyTransferEntry.querySelector(".journalactions");
        const textElem = supplyTransferEntry.querySelector(".journaltext");

        if (journalActionsElem.querySelector("a.actionportfolio")) {
            return;
        }
        if (textElem.textContent.includes("SUPER|brie+") || textElem.textContent.includes("Passing Parcel")) {
            return;
        }
        // Disable button on sending transfers until portfolio sending feature implemented
        if (textElem.textContent.includes("I sent")) {
            return;
        }

        const addPortfolioBtn = document.createElement("a");
        addPortfolioBtn.href = "#";
        addPortfolioBtn.className = "actionportfolio";
        addPortfolioBtn.addEventListener('click', addSbTradeToPortfolio);
        journalActionsElem.prepend(addPortfolioBtn)
    });
}

async function updateItemMetadata() {
    console.log("Retrieving marketplace item data");
    return new Promise((resolve, reject) => {
        hg.utils.Marketplace.getMarketplaceData(
            function (response) {
                const itemMetadata = response.marketplace_items.reduce(
                    function (items, item) {
                        items[normalizeItemName(item.name)] = item.item_id;
                        return items;
                    },
                    {}
                );
                localStorage.markethuntItemMetadata = JSON.stringify(itemMetadata);
                localStorage.markethuntItemMetadataLastRetrieval = Date.now();
                resolve(itemMetadata);
            },
            function (e) {
                reject(e);
            }
        );
    });
}

function normalizeItemName(name) {
    return name.trim();
}

async function addSbTradeToPortfolio(event) {
    event.preventDefault(); // prevent scroll to top

    const targetTransferJournalEntry = event.target.parentNode.parentNode.parentNode;
    const textElem = targetTransferJournalEntry.querySelector(".journaltext");
    const targetEntryId = Number(targetTransferJournalEntry.dataset.entryId);
    // group 1 = qty, group 2 = item name, group 3 = trade partner snuid
    const regex = /^I received (\d[\d,]*) (.+?) from <a href.+snuid=(\w+)/

    // get item and partner data
    const targetEntryMatch = textElem.innerHTML.match(regex);
    const targetItemQty = Number(targetEntryMatch[1].replace(",", ""));
    const targetItemName = targetEntryMatch[2];
    const partnerSnuid = targetEntryMatch[3];
    const partnerName = textElem.querySelector('a').innerHTML;

    // get item ID
    let targetItemId = undefined;
    if (localStorage.markethuntItemMetadata !== undefined) {
        const itemMetadata = JSON.parse(localStorage.markethuntItemMetadata);
        targetItemId = itemMetadata[normalizeItemName(targetItemName)];
    }

    if (targetItemId === undefined) {
        $.toast({
            text: "Please wait ...",
            heading: localStorage.markethuntItemMetadata === undefined ? 'Downloading item data' : 'Reloading item data',
            icon: 'info',
            position: 'top-left',
            loader: false,  // Whether to show loader or not. True by default
        });
        const itemMetadata = await updateItemMetadata();
        await sleep(600); // allow user to read toast before opening new tab
        targetItemId = itemMetadata[normalizeItemName(targetItemName)];
    }

    // detect all sb send entries
    const allSupplyTransferJournalEntries = document.querySelectorAll("#journalContainer div.entry.supplytransferitem");
    const matchingSbSendEntries = Array.from(allSupplyTransferJournalEntries).reduce(
        function(results, journalEntry) {
            const innerHTML = journalEntry.querySelector(".journaltext").innerHTML;
            if (!innerHTML.includes(partnerSnuid)) {
                return results;
            }
            const candidateSbMatch = innerHTML.match(/^I sent (\d[\d,]*) SUPER\|brie\+ to <a href/);
            if (!candidateSbMatch) {
                return results;
            }
            const candidateSbSent = Number(candidateSbMatch[1].replace(",", ""));
            const candidateEntryId = Number(journalEntry.dataset.entryId);
            results.push({sbSent: candidateSbSent, entryId: candidateEntryId});
            return results;
        },
        []
    );

    // choose best sb send entry
    let bestSbSendEntryMatch = null;
    let bestMatchDistance = null;
    matchingSbSendEntries.forEach(function(candidateEntry) {
        const entryPairDistance = Math.abs(targetEntryId - candidateEntry.entryId);
        if (bestMatchDistance === null || bestMatchDistance > entryPairDistance) {
            bestSbSendEntryMatch = candidateEntry;
            bestMatchDistance = entryPairDistance;
        }
    });

    let avgSbPriceString = "none";
    if (bestSbSendEntryMatch !== null) {
        const avgSbPrice = bestSbSendEntryMatch.sbSent / targetItemQty;
        avgSbPriceString = avgSbPrice.toFixed(2);
    }

    // prepare modal message
    let actionMsg = 'Markethunt plugin: ';
    if (bestSbSendEntryMatch !== null) {
        actionMsg += `Found a transfer of ${bestSbSendEntryMatch.sbSent.toLocaleString()} SB to ${partnerName}.` + 
            ` Buy price has been filled in for you.`;
    } else {
        actionMsg += 'No matching SB transfer found. Please fill in buy price manually.';
    }

    // open in new tab
    window.open(`https://${markethuntDomain}/portfolio.php?action=add_position` + 
        `&action_msg=${encodeURIComponent(actionMsg)}` +
        `&item_id=${targetItemId}` + 
        `&add_qty=${targetItemQty}` + 
        `&add_mark=${avgSbPriceString}` +
        `&add_mark_type=sb`,
        '_blank');
}

const journalObserverTarget = document.querySelector("#mousehuntContainer");
const journalObserver = new MutationObserver(function () {
    // Disconnect and reconnect later to prevent mutation loop
    journalObserver.disconnect();

    const journalContainer = journalObserverTarget.querySelector("#journalContainer");
    if (SettingsController.getEnablePortfolioButtons() && journalContainer) {
        // add portfolio buttons
        const supplyTransferJournalEntries = journalContainer.querySelectorAll("div.entry.supplytransferitem");
        addJournalButtons(supplyTransferJournalEntries);
    }

    // Reconnect observer once all mutations done
    journalObserver.observe(journalObserverTarget, {
        childList: true,
        subtree: true
    });
});

// Initial observe
journalObserver.observe(journalObserverTarget, {
    childList: true,
    subtree: true
});

const journalCssOverrides = `
.journalactions a {
    display: inline-block;
}
.journalactions a.actionportfolio {
    margin-right: 5px;
    background: url('${addPfolioBtnImgData}');
    width: 16px;
}
`;

/*******************************
 * 
 *  Import Portfolio  
 * 
 *******************************/

function addTouchPoint() {
    if ($('.invImport').length === 0) {
        const invPages = $('.inventory .torn_pages');
        //Inventory History Button
        const invImportElem = document.createElement('li');
        invImportElem.classList.add('crafting');
        invImportElem.classList.add('invImport');
        const invImportBtn = document.createElement('a');
        invImportBtn.href = "#";
        invImportBtn.innerText = "Export to Markethunt";
        invImportBtn.onclick = function () {
            onInvImportClick();
        };
        const icon = document.createElement("div");
        icon.className = "icon";
        invImportBtn.appendChild(icon);
        invImportElem.appendChild(invImportBtn);
        $(invImportElem).insertAfter(invPages);
    }
}

function submitInv() {
    if (!document.forms["import-form"].reportValidity()) {
        return;
    }

    const gold = Number($('.hud_gold').text().replaceAll(/[^\d]/g, ''));

    if (isNaN(gold)) {
        return;
    }

    const itemsToGet = ['weapon','base', 'trinket', 'bait', 'skin', 'crafting_item','convertible', 'potion', 'stat','collectible','map_piece','adventure']; //future proof this to allow for exclusions
    
    hg.utils.UserInventory.getItemsByClass(itemsToGet, true, function(data) {
        let importData = {
            itemsArray: [],
            inventoryGold: gold
        };
        data.forEach(function(arrayItem, index) {
            importData.itemsArray[index] = [arrayItem.item_id, arrayItem.quantity];
        });

        $('#import-data').val(JSON.stringify(importData));
        document.forms["import-form"].submit();
    })
}

function onInvImportClick(){
    $.dialog({
        title: 'Export inventory to Markethunt',
        content: `
        <form id="import-form" name="import-form" action="https://${markethuntDomain}/import_portfolio.php" method="post" target="_blank">
                <label for="import-portfolio-name">Portfolio name: <span style="color: red">*</span></label>
                <input type="text" id="import-portfolio-name" name="import-portfolio-name" required pattern=".+"/>
                <input type="hidden" id="import-data" name="import-data"/>
        </form>
        <div id="export-dialog-buttons" class="jconfirm-buttons" style="float: none; margin-top: 10px;"><button type="button" class="btn btn-primary">Export</button></div>`,
        boxWidth: '600px',
        useBootstrap: false,
        closeIcon: true,
        draggable: true,
        onOpen: function(){
            $('#import-portfolio-name').val('Portfolio ' + (new Date()).toISOString().substring(0, 10));
            this.$content.find('button').click(function(){
                submitInv();
            });
        }
    });
}

/*******************************
 * 
 *  Final setup and add css  
 * 
 *******************************/

$(document).ready(function() {
    GM_addStyle(GM_getResourceText("jq_confirm_css"));
    GM_addStyle(GM_getResourceText("jq_toast_css"));
    GM_addStyle(marketplaceCssOverrides);
    GM_addStyle(journalCssOverrides);
    GM_addStyle(materialSwitchCss);

    addTouchPoint();

    const supplyTransferJournalEntries = document.querySelectorAll("#journalContainer div.entry.supplytransferitem");
    addJournalButtons(supplyTransferJournalEntries);
});