Market History

Keep track of your market buy/sale history for Dead Frontier to instantly see your profit and losses

// ==UserScript==
// @name         Market History
// @namespace    http://tampermonkey.net/
// @version      1.9.3
// @description  Keep track of your market buy/sale history for Dead Frontier to instantly see your profit and losses
// @author       Runonstof
// @match        *fairview.deadfrontier.com/onlinezombiemmo/index.php*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deadfrontier.com
// @grant        unsafeWindow
// @grant        GM.getValue
// @grant        GM.setValue
// @license      MIT
// ==/UserScript==

(async function() {
    'use strict';

    /******************************************************
     * Initialize script
     ******************************************************/

    const searchParams = new URLSearchParams(window.location.search);
    const page = parseInt(searchParams.get('page'));
    // If is not on the market page or yard page, stop script
    if (![35, 24].includes(page)) {
        return;
    }

    /**
     * Detect if SilverScripts is installed
     * 
     * We only need to know this to render hover item info differently
     * Because if SilverScripts is installed and its HoverPrices are enabled
     * The hover info box can get cluttered and overflows, causing data to be hidden (Found out during testing)
     * So if this is detected, a setting will appear to show our data only when the shift key is pressed
     */
    const silverScriptsInstalled = unsafeWindow.hasOwnProperty('silverRequestItem');

    /* === Global variables === */
    unsafeWindow.historyScreen = 'list'; // 'list', 'stats'
    unsafeWindow.historyScreenSet = false;

    // Will be set after page init (below script)
    // Because DeadFrontier does a call to stackables.json, we need to wait for that to complete
    let SEARCHABLE_ITEMS = [];

    const TIMEFRAME_OPTIONS = {
        all: 'All time',
        last_24hr: 'Last 24 hours',
        last_week: 'Last week',
        last_month: 'Last month',
        last_3_months: 'Last 3 months',
        last_6_months: 'Last 6 months',
        last_year: 'Last year',
        ytd: 'Since january 1st',
        mtd: 'Since 1st of month',
        wtd: 'Since monday',
    };

    const SHIFT_HOVER_OPTIONS = {
        disabled: 'Disabled', // Just show everything
        history: 'Enabled', // Show history info when shift is pressed
    };

    if (silverScriptsInstalled) {
        SHIFT_HOVER_OPTIONS.history = 'History'; // Only show history info when shift is pressed
        SHIFT_HOVER_OPTIONS.silverscripts = 'SilverScripts'; // Show SilverScripts HoverPrices when shift is pressed
    }

    const WEBCALL_HOOKS = {
        before: {},
        after: {},
        afterAll: [],
        beforeAll: [],

        lastExecutedAt: {},
    };

    const LOOKUP = {
        category__item_id: {},
    };
    
    // Our history object
    // That is responsible for keeping track of all trades
    // And calculating statistics
    const _HISTORY = {
        entries: [],
        selectedItem: null,
        filters: {
            minDate: null,
            maxDate: null,
            type: 'all',
        },

        getFilteredEntries() {
            let historyEntries = this.entries;

            if (this.selectedItem || this.filters.minDate || this.filters.maxDate || this.filters.type !== 'all') {
                historyEntries = historyEntries.filter(entry => {

                    // Check by item id
                    if (this.selectedItem && (!entry.item || getBaseItemId(entry.item) != HISTORY.selectedItem)) {
                        return false;
                    }

                    // Check by action type
                    if (this.filters.type === 'buy' && entry.action !== 'buy') {
                        return false;
                    }

                    if (this.filters.type === 'sell' && entry.action !== 'sell') {
                        return false;
                    }

                    if (this.filters.type === 'scrap' && entry.action !== 'scrap') {
                        return false;
                    }

                    if (this.filters.type === 'sell_scrap' && entry.action !== 'sell' && entry.action !== 'scrap') {
                        return false;
                    }

                    // Check by date
                    if (this.filters.minDate && entry.date < this.filters.minDate) {
                        return false;
                    }

                    if (this.filters.maxDate) {
                        const checkMaxDate = this.filters.maxDate + 86400000; // Add 1 day

                        if (entry.date > checkMaxDate) {
                            return false;
                        }
                    }

                    return true;
                });
            }

            return historyEntries;
        },

        // Cached values, to prevent having to loop through all entries every time
        // Causing performance to improve
        cache: {},
        resetCache() {
            this.cache = {
                trade_id: {}, // trades indexed by trade_id
                item_id: {}, // trades indexed by item_id
    
                item_id__amount_sold: {}, // total sell numbers, indexed by item_id
                item_id__amount_bought: {}, // total buy numbers, indexed by item_id
                item_id__avg_price_sold: {}, // average sell price, indexed by item_id
                item_id__avg_price_bought: {}, // average buy price, indexed by item_id
                item_id__total_price_sold: {}, // total sell price, indexed by item_id
                item_id__total_price_bought: {}, // total buy price, indexed by item_id
                item_id__last_price_sold: {}, // last sell price, indexed by item_id
                item_id__last_price_bought: {}, // last buy price, indexed by item_id
                item_id__last_quantity_sold: {}, // last sell quantity, indexed by item_id
                item_id__last_quantity_bought: {}, // last buy quantity, indexed by item_id
                item_id__last_date_sold: {}, // last sell date, indexed by item_id
                item_id__last_date_bought: {}, // last buy date, indexed by item_id
    
                item_id__sold: {}, // trades indexed by trade_id
                item_id__bought: {}, // trades indexed by trade_id
                item_id__scrapped: {}, // trades indexed by trade_id
    
                pending_trade_ids: [], // trade_ids of pending trades
            };

            this.initCache();
        },
        storageKey(key) {
            return 'HISTORY_' + key + '_' + unsafeWindow.userVars.userID;
        },
        initCache() {
            const entries = this.entries;
            for (const entry of entries) {
                const tradeId = entry.trade_id;
                const itemId = getBaseItemId(entry.item);
                // const globalItemId = getGlobalDataItemId(entry.item);

                this.cache.trade_id[tradeId] = entry;

                if (!this.cache.item_id.hasOwnProperty(itemId)) {
                    this.cache.item_id[itemId] = [];
                }
                this.cache.item_id[itemId].push(entry);

                // if (!this.cache.item_id__amount_sold.hasOwnProperty(itemId)) {
                //     this.cache.item_id__amount_sold[itemId] = 0;
                // }

                // if (!this.cache.item_id__amount_bought.hasOwnProperty(itemId)) {
                //     this.cache.item_id__amount_bought[itemId] = 0;
                // }

                // const action = entry.action; // 'buy' or 'sell'
                // const itemcat = unsafeWindow.globalData[globalItemId].itemcat;
                // const quantity = realQuantity(entry.quantity, itemcat)

                // if (action === 'buy') {
                //     this.cache.item_id__amount_bought[itemId] += quantity;
                // } else if (action === 'sell') {
                //     this.cache.item_id__amount_sold[itemId] += quantity;
                // }
            }
        },
        clearCacheForItem(itemId) {
            delete this.cache.item_id__avg_price_sold[itemId];
            delete this.cache.item_id__avg_price_bought[itemId];
            delete this.cache.item_id__last_price_sold[itemId];
            delete this.cache.item_id__last_price_bought[itemId];
            delete this.cache.item_id__last_quantity_sold[itemId];
            delete this.cache.item_id__last_quantity_bought[itemId];
            delete this.cache.item_id__last_date_sold[itemId];
            delete this.cache.item_id__last_date_bought[itemId];
        },
        getTrade(tradeId) {
            if (this.cache.item_id.hasOwnProperty(tradeId)) {
                return this.cache.item_id[tradeId];
            }

            return this.cache.item_id[tradeId] = this.entries.find(entry => entry.trade_id === tradeId);;
        },
        async pushTrade(entry) {
            if (!entry.date) {
                entry.date = Date.now();
            }

            await this.load();
            this.entries.push(entry);
            await this.forceSave();

            this.cache.trade_id[entry.trade_id] = entry;

            const itemId = getBaseItemId(entry.item);

            // Update cache
            if (!this.cache.item_id.hasOwnProperty(itemId)) {
                this.cache.item_id[itemId] = [];
            }

            this.cache.item_id[itemId].push(entry);

            // Init amount sold and amount bought cache
            if (!this.cache.item_id__amount_sold.hasOwnProperty(itemId)) {
                this.cache.item_id__amount_sold[itemId] = 0;
            }

            if (!this.cache.item_id__amount_bought.hasOwnProperty(itemId)) {
                this.cache.item_id__amount_bought[itemId] = 0;
            }

            // Add to quantity cache
            const action = entry.action; // 'buy' or 'sell'
            const quantity = parseInt(entry.quantity);

            if (action === 'buy') {
                // TODO: check date treshold?
                this.cache.item_id__amount_bought[itemId] += quantity;
            } else if (action === 'sell') {
                // TODO: check date treshold?
                this.cache.item_id__amount_sold[itemId] += quantity;
            }

            this.clearCacheForItem(itemId);
        },

        async sortEntries() {
            await this.load();
            this.entries.sort((a, b) => (a.date > b.date) ? 1 : -1);
            await this.forceSave();
        },

        // Remove trade from history
        async removeTrade(tradeId, isIndex = false) {
            await this.load();
            let index = isIndex ? tradeId : this.entries.findIndex(entry => entry.trade_id === tradeId);

            if (isIndex) {
                if (index < 0 || index >= this.entries.length) {
                    return false;
                }
            }

            if (index == -1) {
                return false;
            }

            const entry = this.entries[index];

            this.entries.splice(index, 1);
            await this.forceSave();

            // Update cache
            delete this.cache.trade_id[tradeId];

            // Remove from item_id cache
            const itemId = getBaseItemId(entry.item);
            if (this.cache.item_id.hasOwnProperty(itemId)) {
                const itemIndex = this.cache.item_id[itemId].findIndex(entry => entry.trade_id === tradeId);
                if (itemIndex > -1) {
                    this.cache.item_id[itemId].splice(itemIndex, 1);
                }
            }

            const quantity = entry.quantity;
            const action = entry.action; // 'buy' or 'sell'
            
            if (action === 'buy' && this.cache.item_id__amount_bought.hasOwnProperty(itemId)) {
                this.cache.item_id__amount_bought[itemId] -= quantity;
            }
            else if (action === 'sell' && this.cache.item_id__amount_sold.hasOwnProperty(itemId)) {
                this.cache.item_id__amount_sold[itemId] -= quantity;
            }

            const pendingTradeIndex = this.cache.pending_trade_ids.indexOf(tradeId);
            if (pendingTradeIndex > -1) {
                this.cache.pending_trade_ids.splice(pendingTradeIndex, 1);
            }

            this.clearCacheForItem(itemId);
            return true;
        },

        // Get info about an item, based on its trades
        // Looks up the cache first, if not found, calculates it
        getItemInfo(itemId, key, timeframe = undefined) {
            let cacheKey = 'item_id__' + key;
            let trades, total, action, amount, lastTradeId, lastTradePrice, lastTradeQuantity, lastTradeDate;
            const isDateInTreshold = function (entry) {
                const entryDate = entry.date;
                const tresholdDate = getTresholdDateForTimeframe(SETTINGS.values.hoverStatisticsTimeframe);

                if (tresholdDate === null) {
                    return true;
                }

                return entryDate >= tresholdDate.getTime();
            }
            switch (key) {
                case 'amount_sold':
                case 'amount_bought':
                    if (this.cache[cacheKey].hasOwnProperty(itemId)) {
                        return this.cache[cacheKey][itemId];
                    }

                    trades = this.cache.item_id[itemId] || [];
                    action = key == 'amount_sold' ? 'sell' : 'buy';

                    return this.cache[cacheKey][itemId] = trades.reduce((total, trade) => {
                        if (!isDateInTreshold(trade)) {
                            return total;
                        }
                        
                        let isAction = trade.action === action;
                        if (!isAction && SETTINGS.values.countScraps && action == 'sell') {
                            isAction = trade.action === 'scrap';
                        }

                        if (!isAction) {
                            return total;
                        }

                        const tradeItemId = getGlobalDataItemId(trade.item);
                        
                        const quantity = realQuantity(trade.quantity, unsafeWindow.globalData[tradeItemId].itemcat);
                        return total + quantity;
                    }, 0);
                    break;

                case 'last_price_sold':
                case 'last_price_bought':
                    if (this.cache[cacheKey].hasOwnProperty(itemId)) {
                        return this.cache[cacheKey][itemId];
                    }

                    trades = this.cache.item_id[itemId] || [];
                    action = key == 'last_price_sold' ? 'sell' : 'buy';

                    lastTradeId = 0;
                    lastTradeDate = 0;
                    lastTradePrice = 0;

                    let tradeCount = 0;

                    for (const trade of trades) {
                        if (!isDateInTreshold(trade)) {
                            continue;
                        }
                        let isAction = trade.action === action;
                        if (!isAction && SETTINGS.values.countScraps && action == 'sell') {
                            isAction = trade.action === 'scrap';
                        }

                        if (!isAction) {
                            continue;
                        }
                        if (trade.item !== itemId) {
                            continue;
                        }

                        if (trade.date <= lastTradeDate) {
                            continue;
                        }

                        tradeCount++;
                        lastTradeId = trade.trade_id;
                        lastTradePrice = trade.price;
                        lastTradeDate = trade.date;
                    }

                    if (tradeCount === 0) {
                        return this.cache[cacheKey][itemId] = null;
                    }
                    
                    return this.cache[cacheKey][itemId] = lastTradePrice;
                    break;

                case 'last_quantity_sold':
                case 'last_quantity_bought':
                    if (this.cache[cacheKey].hasOwnProperty(itemId)) {
                        return this.cache[cacheKey][itemId];
                    }

                    trades = this.cache.item_id[itemId] || [];
                    action = key == 'last_quantity_sold' ? 'sell' : 'buy';

                    lastTradeId = 0;
                    lastTradeQuantity = 0;

                    for (const trade of trades) {
                        if (!isDateInTreshold(trade)) {
                            continue;
                        }

                        if (trade.action !== action) {
                            continue;
                        }
                        if (trade.date <= lastTradeDate) {
                            continue;
                        }
                        const tradeItemId = getGlobalDataItemId(trade.item);

                        lastTradeId = trade.trade_id;
                        lastTradeQuantity = realQuantity(trade.quantity, unsafeWindow.globalData[tradeItemId].itemcat);
                        lastTradeDate = trade.date;
                    }
                    
                    return this.cache[cacheKey][itemId] = lastTradeQuantity;
                    break;

                case 'last_date_sold':
                case 'last_date_bought':
                    if (this.cache[cacheKey].hasOwnProperty(itemId)) {
                        return this.cache[cacheKey][itemId];
                    }

                    trades = this.cache.item_id[itemId] || [];
                    action = key == 'last_date_sold' ? 'sell' : 'buy';

                    lastTradeId = 0;
                    lastTradeDate = 0;

                    for (const trade of trades) {
                        if (!isDateInTreshold(trade)) {
                            continue;
                        }
                        
                        if (trade.action !== action) {
                            continue;
                        }
                        if (trade.date <= lastTradeDate) {
                            continue;
                        }

                        lastTradeId = trade.trade_id;
                        lastTradeDate = trade.date;
                    }

                    return this.cache[cacheKey][itemId] = lastTradeDate;
                    break;

                case 'avg_price_sold':
                case 'avg_price_bought':
                    if (this.cache[cacheKey].hasOwnProperty(itemId)) {
                        return this.cache[cacheKey][itemId];
                    }

                    amount = this.getItemInfo(itemId, key.replace('avg_price', 'amount')); // amount_sold or amount_bought
                    if (amount == 0) {
                        return this.cache[cacheKey][itemId] = 0;
                    }

                    action = (key == 'avg_price_sold' ? 'sell' : 'buy');

                    trades = this.cache.item_id[itemId];
                    total = this.getItemInfo(itemId, key.replace('avg_price', 'total_price')); // total_price_sold or total_price_bought

                    return this.cache[cacheKey][itemId] = total / amount;
                    break;

                case 'total_price_sold':
                case 'total_price_bought':
                    if (this.cache[cacheKey].hasOwnProperty(itemId)) {
                        return this.cache[cacheKey][itemId];
                    }

                    amount = this.getItemInfo(itemId, key.replace('total_price', 'amount')); // amount_sold or amount_bought
                    if (amount == 0) {
                        return this.cache[cacheKey][itemId] = 0;
                    }

                    action = (key == 'total_price_sold' ? 'sell' : 'buy');

                    trades = this.cache.item_id[itemId];
                    total = 0;
                    for (const trade of trades) {
                        if (!isDateInTreshold(trade)) {
                            continue;
                        }
                        let isAction = trade.action === action;
                        if (!isAction && SETTINGS.values.countScraps && action == 'sell') {
                            isAction = trade.action === 'scrap';
                        }

                        if (!isAction) {
                            continue;
                        }
                        total += parseInt(trade.price);
                    }

                    return this.cache[cacheKey][itemId] = total;
                    break;

                case 'avg_stack_price_sold':
                case 'avg_stack_price_bought':
                    const avgPrice = this.getItemInfo(itemId, key.replace('avg_stack_price', 'avg_price')); // avg_price_sold or avg_price_bought
                    // const totalAmount = this.getItemInfo(itemId, key.replace('avg_stack_price', 'amount')); // amount_sold or amount_bought
                    const stack = maxStack(itemId);

                    return avgPrice * stack;
                    break;
            }

            throw new Error('Invalid key: ' + key);
        },
        

        async forceSave() {
            if (this._debugMode) {
                return;
            }
            await GM.setValue(this.storageKey('entries'), this.entries);
        },
        // Called during debugging
        async clearEntries() {
            this.entries = [];
            await this.forceSave();
        },

        async setSelectedItem(item) {
            this.selectedItem = item ? getBaseItemId(item) : null;
            await GM.setValue(this.storageKey('selectedItem'), item);
        },
        async saveFilters() {
            await GM.setValue(this.storageKey('filters'), this.filters);
        },
        async init() {
            this.selectedItem = await GM.getValue(this.storageKey('selectedItem'), null);
            this.filters = mergeDeep({}, this.filters, await GM.getValue(this.storageKey('filters'), {}));
            await this.load();
        },
        async load() {
            if (this._debugMode) {
                return;
            }
            this.entries = await GM.getValue(this.storageKey('entries'), []);
        },
        _debugMode: false,
        debugMode(mode = null) {
            if (mode === null) {
                this.debugMode(!this._debugMode);
                return;
            }

            this._debugMode = mode;
        },

        renderEntryPrompt(entry) {
            pageLock = true;

			unsafeWindow.prompt.classList.remove("warning");
			unsafeWindow.prompt.classList.remove("redhighlight");

        	unsafeWindow.prompt.style.height = "200px";

            const imgItemId = getGlobalDataItemId(entry.item);
            const imgUrl = 'https://files.deadfrontier.com/deadfrontier/inventoryimages/large/' + imgItemId + '.png';
            
            unsafeWindow.prompt.innerHTML = `
                <div style="text-align: center; text-decoration: underline;">Edit entry</div>
                <div style="text-align: right; position: absolute; right: 0;"><img src="${imgUrl}" /></div>
                <div style="z-index: 1000; position: relative;">
                    <span style="text-decoration: underline;">Item:</span><br>
                    ${entry.itemname}${entry.item == 'credits' ? '' : ` x${entry.quantity}`}
                </div>
                <br>
                <div style="position: relative;">
                    <div style="position: absolute;">
                        <span style="text-decoration: underline;">Action:</span><br>
                        <span style="color: #00FF00;">${entry.action}</span>
                    </div>
                    <div style="position: absolute; left: 100px;">
                        <span style="text-decoration: underline;">Price:</span><br>
                        ${formatMoneyHtml(entry.price, true)}
                    </div>
                    <br>
                    <br>
                </div>
                <br>
                <div>
                    <span style="text-decoration: underline;">Datetime:</span><br>
                    ${formatDate(new Date(entry.date))}
                </div>
            `;


            const historyEntryHolder = document.createElement("div");
            historyEntryHolder.id = "historyEntryHolder";

            
            // const footerButton = document.createElement("button");
            // footerButton.style.position = "absolute";
            // footerButton.style.bottom = "12px";
            // if (footerButtonInfo.action) {
            //     footerButton.addEventListener("click", footerButtonInfo.action);
            // }

            // footerButton.textContent = footerButtonInfo.label;
            
            // for(const styleKey in footerButtonInfo.style) {
            //     footerButton.style[styleKey] = footerButtonInfo.style[styleKey];
            // }
            // unsafeWindow.prompt.appendChild(footerButton);

            const closeBtn = document.createElement("button");
            closeBtn.style.position = "absolute";
            closeBtn.style.bottom = "12px";
            closeBtn.style.right = "12px";
            closeBtn.textContent = "close";
            closeBtn.addEventListener("click", () => {
                HISTORY.closeEntryPrompt();
            });
            unsafeWindow.prompt.appendChild(closeBtn);

            const itemInfoBtn = document.createElement("button");
            itemInfoBtn.style.position = "absolute";
            itemInfoBtn.style.bottom = "12px";
            itemInfoBtn.style.left = "100px";
            itemInfoBtn.textContent = "item stats";
            itemInfoBtn.addEventListener("click", () => {
                HISTORY.closeEntryPrompt();
                unsafeWindow.historyScreen = 'stats';
                HISTORY.setSelectedItem(entry.item);
                loadMarket();
            });
            unsafeWindow.prompt.appendChild(itemInfoBtn);

            const removeBtn = document.createElement("button");
            removeBtn.style.position = "absolute";
            removeBtn.style.bottom = "12px";
            removeBtn.style.left = "12px";
            removeBtn.textContent = "remove";
            removeBtn.addEventListener("click", async () => {
                const confirmed = confirm('Are you sure you want to remove this entry?');
                if (!confirmed) {
                    return;
                }
                
                unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>";
                const removed = await HISTORY.removeTrade(entry.trade_id);
                if (!removed) {
                    alert('Could not remove entry');
                    return;
                }

                HISTORY.closeEntryPrompt();
                // HISTORY.resetCache();
                loadMarket();
            });

            unsafeWindow.prompt.appendChild(removeBtn);

            const editBtn = document.createElement("button");
            editBtn.style.position = "absolute";
            editBtn.style.bottom = "30px";
            editBtn.style.left = "12px";
            editBtn.textContent = "edit";
            editBtn.addEventListener("click", async () => {
                HISTORY.renderEntryFormPrompt(entry);

                // const confirmed = confirm('Are you sure you want to remove this entry?');
                // if (!confirmed) {
                //     return;
                // }

                // const removed = await HISTORY.removeTrade(entry.trade_id);
                // if (!removed) {
                //     alert('Could not remove entry');
                //     return;
                // }

                // HISTORY.closeEntryPrompt();
                // // HISTORY.resetCache();
                // loadMarket();
            });

            unsafeWindow.prompt.appendChild(editBtn);
            
            unsafeWindow.prompt.parentNode.style.display = "block";
            unsafeWindow.prompt.focus();

        },
        renderEntryFormPrompt(entry) {
            pageLock = true;


			unsafeWindow.prompt.classList.remove("warning");
			unsafeWindow.prompt.classList.remove("redhighlight");

        	unsafeWindow.prompt.style.height = "200px";

            const imgItemId = getGlobalDataItemId(entry.item);
            const imgUrl = 'https://files.deadfrontier.com/deadfrontier/inventoryimages/large/' + imgItemId + '.png';

            
            const itemData = unsafeWindow.globalData[imgItemId];
            if (!entry.itemname) {
                entry.itemname = unsafeWindow.itemNamer(entry.item, '');
            }
            
            const maxQuantity = maxStack(entry.item, false);

            unsafeWindow.prompt.innerHTML = `
                <div class="historyEntryForm">
                    <div style="text-align: center; text-decoration: underline;">${entry.trade_id ? 'Edit' : 'Create'} entry</div>
                    <div style="text-align: right; position: absolute; right: 0;"><img src="${imgUrl}" /></div>
                    <div style="z-index: 1000; position: relative;">
                        <span style="text-decoration: underline;">Item:</span><br>
                        ${entry.itemname}
                        <br>
                        <input type="number" min="0" max="${maxQuantity}" placeholder="Quantity" style="width: 50px;" id="entryFormQuantity" value="${entry.quantity || maxQuantity}" />
                    </div>
                    <br>
                    <div style="position: relative;">
                        <div style="position: absolute;">
                            <span style="text-decoration: underline;">Action:</span><br>

                            <div data-value="${entry.action || 'buy'}" id="entryFormAction" class="historySelectComponent">
                                <div class="selectChoice">
                                    <span></span>
                                    <span></span>
                                </div>
                                <div class="selectList">
                                    <div data-value="buy" class="selectOption">Buy</div>
                                    <div data-value="sell" class="selectOption">Sell</div>
                                    <div data-value="scrap" class="selectOption">Scrap</div>
                                </div>
                            </div>

                        </div>
                        <div style="position: absolute; left: 100px;">
                            <span style="text-decoration: underline;">Price:</span><br>
                            <span style="color: #FFCC00;">$</span>&nbsp;<input type="number" min="1" max="9999999999" placeholder="Price" style="width: 50px;" id="entryFormPrice" value="${entry.price || 0}" />
                        </div>
                        <br>
                        <br>
                    </div>
                    <br>
                    <div>
                        <span style="text-decoration: underline;">Datetime:</span><br>
                        <input type="datetime-local" id="entryFormDate" value="${formatDate(new Date(entry.date || Date.now()))}" />
                    </div>
                </div>
            `;

            initHistorySelects();


            const historyEntryHolder = document.createElement("div");
            historyEntryHolder.id = "historyEntryHolder";

            
            // const footerButton = document.createElement("button");
            // footerButton.style.position = "absolute";
            // footerButton.style.bottom = "12px";
            // if (footerButtonInfo.action) {
            //     footerButton.addEventListener("click", footerButtonInfo.action);
            // }

            // footerButton.textContent = footerButtonInfo.label;
            
            // for(const styleKey in footerButtonInfo.style) {
            //     footerButton.style[styleKey] = footerButtonInfo.style[styleKey];
            // }
            // unsafeWindow.prompt.appendChild(footerButton);

            const closeBtn = document.createElement("button");
            closeBtn.style.position = "absolute";
            closeBtn.style.bottom = "12px";
            closeBtn.style.right = "12px";
            closeBtn.textContent = "cancel";
            closeBtn.addEventListener("click", () => {
                HISTORY.closeEntryPrompt();
            });
            unsafeWindow.prompt.appendChild(closeBtn);

            // const itemInfoBtn = document.createElement("button");
            // itemInfoBtn.style.position = "absolute";
            // itemInfoBtn.style.bottom = "12px";
            // itemInfoBtn.style.left = "100px";
            // itemInfoBtn.textContent = "item stats";
            // itemInfoBtn.addEventListener("click", () => {
            //     HISTORY.closeEntryPrompt();
            //     unsafeWindow.historyScreen = 'stats';
            //     HISTORY.setSelectedItem(entry.item);
            //     loadMarket();
            // });
            // unsafeWindow.prompt.appendChild(itemInfoBtn);

            const saveBtn = document.createElement("button");
            saveBtn.style.position = "absolute";
            saveBtn.style.bottom = "12px";
            saveBtn.style.left = "12px";
            saveBtn.textContent = entry.trade_id ? "save" : "add";
            saveBtn.addEventListener("click", async () => {
                if (saveBtn.disabled) {
                    return;
                }

                // validate and parse values
                const quantity = parseInt(document.getElementById('entryFormQuantity').value);
                const price = parseInt(document.getElementById('entryFormPrice').value);
                const date = new Date(document.getElementById('entryFormDate').value);
                const action = document.getElementById('entryFormAction').dataset.value;

                const maxQuantity = maxStack(entry.item, true);
                
                if (isNaN(quantity) || quantity < 1 || quantity > maxQuantity) {
                    alert('Invalid quantity, max is ' + maxQuantity);
                    return;
                }

                if (isNaN(price) || price < 0 || price > 9999999999) {
                    alert('Invalid price');
                    return;
                }

                if (isNaN(date.getTime())) {
                    alert('Invalid date');
                    return;
                }

                if (entry.trade_id) {
                    const confirmed = confirm('Are you sure you want to overwrite this entry?');
                    if (!confirmed) {
                        return;
                    }

                    saveBtn.disabled = true;

                    const newEntryData = {
                        quantity,
                        price,
                        date: date.getTime(),
                        action,
                    };

                    if (entry.item == 'credits') {
                        entry.itemname = unsafeWindow.itemNamer(entry.item, quantity);
                    }

                    const newEntry = mergeDeep({}, entry, newEntryData);

                    const removed = await HISTORY.removeTrade(entry.trade_id);
                    if (!removed) {
                        alert('Could not remove entry');
                        return;
                    }
                    
                    unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>";
                    await HISTORY.pushTrade(newEntry);
                } else {
                    saveBtn.disabled = true;
                    const newEntry = {
                        trade_id: uniqid(16),
                        item: entry.item,
                        itemname: unsafeWindow.itemNamer(entry.item, quantity),
                        quantity,
                        price,
                        date: date.getTime(),
                        action,
                    };
                    unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>";
                    await HISTORY.pushTrade(newEntry);
                }

                await HISTORY.sortEntries();
                
                HISTORY.closeEntryPrompt();
                loadMarket();
            });

            unsafeWindow.prompt.appendChild(saveBtn);

            // const editBtn = document.createElement("button");
            // editBtn.style.position = "absolute";
            // editBtn.style.bottom = "30px";
            // editBtn.style.left = "12px";
            // editBtn.textContent = "edit";
            // editBtn.addEventListener("click", async () => {
            //     // const confirmed = confirm('Are you sure you want to remove this entry?');
            //     // if (!confirmed) {
            //     //     return;
            //     // }

            //     // const removed = await HISTORY.removeTrade(entry.trade_id);
            //     // if (!removed) {
            //     //     alert('Could not remove entry');
            //     //     return;
            //     // }

            //     // HISTORY.closeEntryPrompt();
            //     // // HISTORY.resetCache();
            //     // loadMarket();
            // });

            // unsafeWindow.prompt.appendChild(editBtn);
            
            unsafeWindow.prompt.parentNode.style.display = "block";
            unsafeWindow.prompt.focus();

        },
        closeEntryPrompt() {
            pageLock = false;
            unsafeWindow.prompt.parentNode.style.display = "none";
            unsafeWindow.prompt.innerHTML = "";
			unsafeWindow.prompt.classList.remove("warning");
			unsafeWindow.prompt.classList.remove("redhighlight");

        },

        /**
         * Executed when an item is sold, and then the new sell listing is retrieved
         * The response contains the trade id, which is what we need to keep track of the trade
         */
        onSellItem(request, response) {
            // console.log('trying to push sell trade: ', JSON.stringify(response.dataObj, null, 4));
            // console.log('Share this info with Runon if needed');
            const tradeCount = response.dataObj.tradelist_totalsales;
            if (tradeCount == 0) {
                return;
            }
    
            let recentTrade = null;
            const props = [
                'category',
                'deny_private',
                'id_member',
                'id_member_to',
                'item',
                'itemname',
                'member_name',
                'member_to_name',
                'price',
                'pricerper',
                'quantity',
                'trade_id',
                'trade_zone',
            ];
            for(let i = 0; i < tradeCount; i++) {
                const tradeId = response.dataObj['tradelist_' + i + '_trade_id'];
    
                if (recentTrade && recentTrade.trade_id >= tradeId) {
                    continue;
                }
    
                const entry = {
                    action: 'sell',
                };
    
                for (const prop of props) {
                    entry[prop] = response.dataObj['tradelist_' + i + '_' + prop];
                }
    
                recentTrade = entry;
            }
    
            if (!recentTrade) {
                return;
            }
    
            this.pushTrade(recentTrade);
        },
    };

    const HISTORY = new Proxy(_HISTORY, {
        get(target, prop) {
            return Reflect.get(...arguments);
        },
        set(target, prop, value) {
            return Reflect.set(...arguments);
        },
    });

    const SETTINGS = {
        ui: {
            main: {
                title: 'History Menu',
                text: 'Welcome to History Help and Settings!',
                elements: {
                    settings: {
                        type: 'button',
                        title: 'Settings',
                        action() {
                            SETTINGS.renderSettingsPrompt('settings');
                        }
                    },
                    help: {
                        type: 'button',
                        title: 'Help',
                        action() {
                            SETTINGS.renderSettingsPrompt('help');
                        }
                    },
                    export: {
                        type: 'button',
                        title: 'Export',
                        action() {
                            SETTINGS.renderSettingsPrompt('export');
                        }
                    },
                    actions: {
                        type: 'button',
                        title: 'Actions',
                        action() {
                            SETTINGS.renderSettingsPrompt('actions');
                        }
                    },
                    credits: {
                        type: 'button',
                        title: 'Credits',
                        action() {
                            SETTINGS.renderSettingsPrompt('credits');
                        }
                    },
                },
                footerButtons: [
                    {
                        label: 'close',
                        action() {
                            SETTINGS.closePrompt();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ],
            },
            help: {
                text: 'This script keeps track of all your market trades and your item scraps, and calculates statistics like average sell price.<br><br><span style="color: #FF0000">NOTE:</span> Your history is saved into TamperMonkey data, so if you log in on another computer, your history will not be available there.<br><br>Use the export function to export your history',
                title: 'Help',
                elements: {},
                footerButtons: [
                    {
                        label: 'back',
                        action() {
                            SETTINGS.renderSettingsPrompt('main');
                        },
                        style: {
                            left: '12px',
                        }
                    },
                    {
                        label: 'close',
                        action() {
                            SETTINGS.closePrompt();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ],
            },
            export: {
                title: 'Export',
                text: 'Export your history to a csv file',
                elements: {
                    exportSortBy: {
                        title: 'Sort by',
                        type: 'switch',
                        description: 'The column to sort by',
                        options: {
                            name: 'Item name',
                            quantity: 'Quantity',
                            price: 'Price',
                            action: 'Trade type',
                            date: 'Date',
                        },
                    },
                    exportSortDirection: {
                        title: 'Sort direction',
                        type: 'switch',
                        description: 'The direction to sort by',
                        options: {
                            asc: 'Ascending',
                            desc: 'Descending',
                        },
                    },
                    exportTimeframe: {
                        title: 'Export timeframe',
                        // type: 'timeframeselect',
                        type: 'switch',
                        description: 'The timeframe that will be used to export the history.',
                        options: TIMEFRAME_OPTIONS
                    },
                    download: {
                        type: 'button',
                        title: 'Download export',
                        description: 'Starts the exports and downloads the csv file',
                        async action() {
                            await HISTORY.load();

                            const sortBy = SETTINGS.values.exportSortBy;
                            const sortDirection = SETTINGS.values.exportSortDirection;
                            const timeframe = SETTINGS.values.exportTimeframe;
                            const tresholdDate = getTresholdDateForTimeframe(timeframe);

                            let filteredEntries = HISTORY.entries.filter(entry => {
                                const entryDate = new Date(entry.date);
                                if (tresholdDate && entryDate < tresholdDate) {
                                    return false;
                                }
                                return true;
                            });

                            const sortByKey = sortBy == 'name' ? 'item' : sortBy;
                            const sortDirectionMultiplier = sortDirection == 'asc' ? 1 : -1;
                            const sortStrategy = ['price', 'quantity'].includes(sortByKey) ? 'numeric' : 'string';

                            filteredEntries.sort((a, b) => {
                                const aKey = a[sortByKey];
                                const bKey = b[sortByKey];

                                if (sortStrategy == 'numeric') {
                                    return (aKey - bKey) * sortDirectionMultiplier;
                                }

                                if (sortStrategy == 'string') {
                                    if (aKey < bKey) {
                                        return -1 * sortDirectionMultiplier;
                                    }
                                    if (aKey > bKey) {
                                        return 1 * sortDirectionMultiplier;
                                    }
                                    return 0;
                                }

                                throw new Error('Invalid sort strategy: ' + sortStrategy);
                            });

                            filteredEntries = filteredEntries.map(entry => [
                                entry.trade_id,
                                entry.item,
                                entry.itemname,
                                entry.quantity,
                                entry.price,
                                entry.action,
                                formatDate(new Date(entry.date)),
                            ]);

                            // Insert header at top
                            filteredEntries.unshift(['id', 'item', 'name', 'quantity', 'price', 'action', 'date']);

                            exportToCsv('market_trades_tracker_export.csv', filteredEntries, SETTINGS.values.exportSeperator);
                        }
                    }
                },
                footerButtons: [
                    {
                        label: 'back',
                        action() {
                            SETTINGS.renderSettingsPrompt('main');
                        },
                        style: {
                            left: '12px',
                        }
                    },
                    {
                        label: 'close',
                        action() {
                            SETTINGS.closePrompt();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ],
            },
            actions: {
                title: 'Actions',
                text: '',
                elements: {
                    clear: {
                        type: 'button',
                        title: 'Clear history',
                        description: 'Clears all market sell/buy/scrap history older than a specific timeframe<br><br><span style="color: #FF0000">WARNING:</span> This cannot be undone!',
                        action() {
                            SETTINGS.renderSettingsPrompt('clear_confirm');
                        }
                    },
                    clearCache: {
                        type: 'button',
                        title: 'Clear cache',
                        description: 'Clears the cache, which will cause the script to recalculate all statistics',
                        action() {
                            HISTORY.resetCache();
                            alert('Cache cleared');
                        }
                    },
                },
                footerButtons: [
                    {
                        label: 'back',
                        action() {
                            SETTINGS.renderSettingsPrompt('main');
                        },
                        style: {
                            left: '12px',
                        }
                    },
                    {
                        label: 'close',
                        action() {
                            SETTINGS.closePrompt();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ],
            },
            clear_confirm: {
                class: 'warning',
                title: 'Clear history',
                text: 'Are you really sure you want to clear history?<br><br>You can delete single entries by clicking on them in the history tab.<br><br><span style="color: #FF0000">WARNING:</span> This cannot be undone!',
                descriptionTop: '200px',
                elements: {
                    clearHistoryTimeframe: {
                        title: 'Timeframe',
                        type: 'switch',
                        description: 'History entries that are older than this timeframe will be deleted.',
                        options: TIMEFRAME_OPTIONS
                    },
                },
                footerButtons: [
                    {
                        label: 'no',
                        action() {
                            SETTINGS.renderSettingsPrompt('actions');
                        },
                        style: {
                            left: '12px',
                        }
                    },
                    {
                        label: 'yes',
                        async action() {
                            if (!confirm('Are you really sure? All entries OLDER than the selected timeframe will be deleted!')) {
                                return;
                            }

                            if (SETTINGS.values.clearHistoryTimeframe == 'all') {
                                await HISTORY.clearEntries();
                            } else {
                                const tresholdDate = getTresholdDateForTimeframe(SETTINGS.values.clearHistoryTimeframe);
                                await HISTORY.load();
                                const entries = HISTORY.entries.filter(entry => {
                                    const entryDate = new Date(entry.date);
                                    return entryDate >= tresholdDate;
                                });
                                HISTORY.entries = entries;
                                await HISTORY.forceSave();
                            }
                            HISTORY.resetCache();
                            SETTINGS.closePrompt();
                            window.location.reload();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ]
            },
            credits: {
                title: 'Credits',
                text: 'This script was made by <span style="color: #FF0000">Runonstof</span>. If you have any questions or suggestions, feel free to contact me on Discord: <span style="color: #FF0000">runon</span>',
                elements: {
                    donate: {
                        type: 'button',
                        title: 'Donate',
                        description: 'Redirects to my profile page, where you can donate anything if you want to support me.',
                        action() {
                            window.location.href = '/onlinezombiemmo/index.php?action=profile;u=12925065';
                        }
                    }
                },
                footerButtons: [
                    {
                        label: 'back',
                        action() {
                            SETTINGS.renderSettingsPrompt('main');
                        },
                        style: {
                            left: '12px',
                        }
                    },
                    {
                        label: 'close',
                        action() {
                            SETTINGS.closePrompt();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ],
            },
            settings: {
                title: 'Settings',
                text: '',
                elements: {
                    hoverSettings: {
                        type: 'button',
                        title: '>>  Hover info settings',
                        description: 'Settings for the hover info module.',
                        action() {
                            SETTINGS.renderSettingsPrompt('hoverSettings');
                        }
                    },
                    // hoverAvgPriceEnabled: {
                    //     title: 'Avg price hover enabled',
                    //     description: 'Show average sell/buy price and profit/loss in set timeframe on item hover',
                    //     type: 'checkbox',
                    // },
                    // hoverLastPriceEnabled: {
                    //     title: 'Last price hover enabled',
                    //     description: 'Show last sell/buy price and date on item hover',
                    //     type: 'checkbox',
                    // },
                    

                    // autoFillBreakEvenPrice: {
                    //     title: 'Auto fill price',
                    //     type: 'checkbox',
                    //     description: 'When selling an item, the price will be automatically filled in with the average sell price in the set timeframe.',
                    // },
                    countPendingTrades: {
                        title: 'Calculate with pending',
                        type: 'checkbox',
                        meta: {
                            resetCache: true,
                        },
                        description: 'When calculating statistics, pending trades will be taken into account, if changed, you might have to reload statistics screen.',
                    },
                    countScraps: {
                        title: 'Calculate with scraps',
                        type: 'checkbox',
                        meta: {
                            resetCache: true,
                        },
                        description: 'When calculating statistics, scraps will be taken into account, if changed, you might have to reload statistics screen.',
                    },
                    hoverStatisticsTimeframe: {
                        title: 'Timeframe',
                        // type: 'timeframeselect',
                        type: 'switch',
                        description: 'The timeframe that will be used to calculate statistics.',
                        options: {
                            all: 'All time',
                            last_24hr: 'Last 24 hours',
                            last_week: 'Last week',
                            last_month: 'Last month',
                            last_3_months: 'Last 3 months',
                            last_6_months: 'Last 6 months',
                            last_year: 'Last year',
                            ytd: 'Since january 1st',
                            mtd: 'Since 1st of month',
                        },
                        meta: {
                            resetCache: true,
                        },
                    },
                    defaultHistoryPage: {
                        title: 'Default page',
                        type: 'switch',
                        description: 'The page that will be shown by default when opening the history tab.',
                        options: {
                            list: 'List',
                            stats: 'Statistics',
                        },
                    },
                },
                footerButtons: [
                    {
                        label: 'back',
                        action() {
                            SETTINGS.renderSettingsPrompt('main');
                        },
                        style: {
                            left: '12px',
                        }
                    },
                    {
                        label: 'close',
                        action() {
                            SETTINGS.closePrompt();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ],
            },
            hoverSettings: {
                title: 'Hover settings',
                text: 'Here you can configure what elements you see when you hover an item.',
                descriptionStrategy: 'text',
                style: {
                    position: 'absolute',
                    bottom: '50px',
                },
                elements: {
                    hoverEnabled: {
                        title: 'Hover info enabled',
                        description: 'Show info on item hover',
                        type: 'checkbox',
                    },
                    hoverAvgSellPriceEnabled: {
                        title: 'Average sell price',
                        description: 'Show average sell price in set timeframe.',
                        type: 'checkbox',
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        },
                    },
                    hoverAvgBuyPriceEnabled: {
                        title: 'Average buy price',
                        description: 'Show average buy price in set timeframe.',
                        type: 'checkbox',
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        },
                    },
                    hoverAmountSoldEnabled: {
                        title: 'Amount sold',
                        description: 'Show amount sold in set timeframe.',
                        type: 'checkbox',
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        },
                    },
                    hoverAmountBoughtEnabled: {
                        title: 'Amount bought',
                        description: 'Show amount bought in set timeframe.',
                        type: 'checkbox',
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        },
                    },
                    hoverLastSellPriceEnabled: {
                        title: 'Last sell price',
                        description: 'Show the most recent price you sold this item for.',
                        type: 'checkbox',
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        },
                    },
                    hoverLastBuyPriceEnabled: {
                        title: 'Last buy price',
                        description: 'Show the most recent price you bought this item for.',
                        type: 'checkbox',
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        },
                    },
                    hoverAvgProfitEnabled: {
                        title: 'Average profit per item',
                        description: 'Show average profit per item in set timeframe.',
                        type: 'checkbox',
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        }
                    },
                    shiftHoverMode: {
                        title: 'Hold shift mode',
                        type: 'switch',
                        description() {
                            if (SETTINGS.values.shiftHoverMode == 'disabled') {
                                return 'Shift hover is disabled' + (silverScriptsInstalled ? ', both SilverScripts and History Data are shown without holding SHIFT.' : ', History Data is shown without holding SHIFT.');
                            } else if (SETTINGS.values.shiftHoverMode == 'history') {
                                return 'When SHIFT is held, History Data will only be shown' + (silverScriptsInstalled ? ', otherwise SilverScripts\' HoverPrices are shown.' : '.');
                            } else if (SETTINGS.values.shiftHoverMode == 'silverscripts') {
                                return 'When SHIFT is held, SilverScripts\' HoverPrices are only shown, otherwise History Data is shown.';
                            }

                            return '';
                        },
                        disabled() {
                            return !SETTINGS.values.hoverEnabled;
                        },
                        options: SHIFT_HOVER_OPTIONS,
                    },
                },
                footerButtons: [
                    {
                        label: 'back',
                        action() {
                            SETTINGS.renderSettingsPrompt('settings');
                        },
                        style: {
                            left: '12px',
                        }
                    },
                    {
                        label: 'close',
                        action() {
                            SETTINGS.closePrompt();
                        },
                        style: {
                            right: '12px',
                        }
                    }
                ],
            },
        },
        // Seperate values object, where settings are loaded one by one, for if i decide to add settings later on
        values: {
            // If true, sell/buy statistics will be shown in the item tooltip when an item is hovered
            hoverEnabled: true,
            
            hoverAvgSellPriceEnabled: true,
            hoverAvgBuyPriceEnabled: true,
            hoverAmountSoldEnabled: true,
            hoverAmountBoughtEnabled: true,
            hoverLastSellPriceEnabled: true,
            hoverLastBuyPriceEnabled: true,
            hoverAvgProfitEnabled: true,
            shiftHoverMode: silverScriptsInstalled ? 'history' : 'disabled', // 'disabled', 'history', 'silverscripts'

            defaultHistoryPage: 'list',
            hoverStatisticsTimeframe: 'all',
            clearHistoryTimeframe: 'all',

            // If true, the script will automatically fill in the price when selling an item
            // The price will be the average buy price of the item of the configured timeframe
            autoFillBreakEvenPrice: true,
            // If true, the script will take trades that are still pending into account when calculating statistics
            countPendingTrades: true,
            countScraps: true,
            exportSortBy: 'date',
            exportSortDirection: 'asc',
            exportTimeframe: 'all',
            exportSeperator: ';',
        },
        async reset() {
            await GM.setValue('SETTINGS_values', {});
            this.values = {};

            await this.load();
        },
        async load() {
            const values = await GM.getValue('SETTINGS_values', {});

            // Merge values with default values
            this.values = mergeDeep(this.values, values);

            if (!silverScriptsInstalled && this.values.shiftHoverMode == 'silverscripts') {
                this.values.shiftHoverMode = 'disabled';
            }

            if (!unsafeWindow.historyScreenSet) {
                unsafeWindow.historyScreen = this.values.defaultHistoryPage;
                unsafeWindow.historyScreenSet = true;
            }
        },
        async save() {
            await GM.setValue('SETTINGS_values', this.values);
        },
        async toggle(setting) {
            await this.load();
            this.values[setting] = !this.values[setting];
            await this.save();
        },
        async set(setting, value) {
            await this.load();
            this.values[setting] = value;
            await this.save();
        },
        closePrompt() {
            unsafeWindow.prompt.parentNode.style.display = "none";
            unsafeWindow.prompt.innerHTML = "";
            unsafeWindow.prompt.style.height = "";
            pageLock = false;
			unsafeWindow.prompt.classList.remove("warning");
			unsafeWindow.prompt.classList.remove("redhighlight");

            console.log('reloading market:' + unsafeWindow.marketScreen + ' ' + unsafeWindow.historyScreen);
            if (unsafeWindow.marketScreen == 'history' && unsafeWindow.historyScreen == 'stats') {
                unsafeWindow.loadMarket();
            }
        },
        renderSettingsPrompt(page = 'main') {
            pageLock = true;

			unsafeWindow.prompt.classList.remove("warning");
			unsafeWindow.prompt.classList.remove("redhighlight");

            const pageInfo = this.ui[page];

            if (pageInfo.class) {
                unsafeWindow.prompt.classList.add(pageInfo.class);
            }

        	unsafeWindow.prompt.style.height = "280px";
            unsafeWindow.prompt.innerHTML = pageInfo.title ? '<div style="text-align: center; text-decoration: underline">' + pageInfo.title + '</div>' : '';
            if (pageInfo.text) {
                unsafeWindow.prompt.innerHTML += '<div id="historySettingsPageText">' + pageInfo.text + '</div>';
            }
            unsafeWindow.prompt.innerHTML += '<br />';

            const historySettingsHolder = document.createElement("div");
            historySettingsHolder.id = "historySettingsHolder";
            // historySettingsHolder.style.position = "absolute";

            this._renderUi(historySettingsHolder, page);

            unsafeWindow.prompt.appendChild(historySettingsHolder);

            for(const footerButtonInfo of pageInfo.footerButtons || []) {

                const footerButton = document.createElement("button");
                footerButton.style.position = "absolute";
                footerButton.style.bottom = "12px";
                if (footerButtonInfo.action) {
                    footerButton.addEventListener("click", footerButtonInfo.action);
                }

                footerButton.textContent = footerButtonInfo.label;
                
                for(const styleKey in footerButtonInfo.style) {
                    footerButton.style[styleKey] = footerButtonInfo.style[styleKey];
                }
                unsafeWindow.prompt.appendChild(footerButton);
            }

            
            unsafeWindow.prompt.parentNode.style.display = "block";
            unsafeWindow.prompt.focus();
        },
        _renderDescription(holder, descriptionText, pageInfo) {
            const strategy = pageInfo.descriptionStrategy || 'bottom';

            if (typeof descriptionText === 'function') {
                descriptionText = descriptionText();
            }

            if (strategy == 'bottom') {
                const descriptionElement = document.getElementById('historySettingsDescription');
                // delete the element
                if (descriptionElement) {
                    descriptionElement.parentNode.removeChild(descriptionElement);
                }
    
                if (!descriptionText) {
                    return;
                }
    
                const description = document.createElement('div');
                description.id = 'historySettingsDescription';
                description.innerHTML = descriptionText;
                description.style.position = 'absolute';
                const top = pageInfo.descriptionTop || '140px';
                description.style.top = top;
    
                holder.appendChild(description);
            } else if (strategy == 'text') {
                const historySettingsPageText = document.getElementById('historySettingsPageText');
                if (!historySettingsPageText) {
                    return;
                }
                historySettingsPageText.style.display = 'none';

                const existingDescription = document.getElementById('historySettingsDescription');
                if (existingDescription) {
                    existingDescription.parentNode.removeChild(existingDescription);
                }

                if (!descriptionText) {
                    historySettingsPageText.style.display = '';
                    return;
                }

                const description = document.createElement('div');
                description.id = 'historySettingsDescription';
                description.innerHTML = descriptionText;

                historySettingsPageText.parentNode.insertBefore(description, historySettingsPageText.nextSibling);
            }
        },
        _renderUi(holder, page = 'main') {
            const pageInfo = this.ui[page];

            if (pageInfo.style) {
                for(const styleKey in pageInfo.style) {
                    holder.style[styleKey] = pageInfo.style[styleKey];
                }
            }

            const elements = pageInfo.elements;
            holder.innerHTML = '';

            const self = this;
            
            for(const settingKey in elements) {
                const setting = elements[settingKey];
                const buttonHolder = document.createElement('div');

                switch (setting.type) {
                    case 'paragraph':
                        break;
                    case 'checkbox':
                        const checkbox = document.createElement('button');
                        checkbox.innerText = '[' + (this.values[settingKey] ? 'x' : ' ') + '] ' + setting.title;
                        if (typeof setting.disabled === 'function') {
                            checkbox.disabled = setting.disabled();
                        }

                        if (checkbox.disabled) {
                            buttonHolder.appendChild(checkbox);
                            holder.appendChild(buttonHolder);
                            break;
                        }

                        checkbox.addEventListener('click', async () => {
                            await self.toggle(settingKey);
                            if (setting.meta) {
                                if (setting.meta.resetCache) {
                                    HISTORY.resetCache();
                                }
                            }
                            self._renderUi(holder, page);
                        });
                        if (setting.description) {
                            checkbox.addEventListener('mouseover', function () {
                                self._renderDescription(holder, setting.description, pageInfo);
                            });
                            checkbox.addEventListener('mouseout', function () {
                                self._renderDescription(holder, null, pageInfo);
                            });
                        }
                        buttonHolder.appendChild(checkbox);
                        holder.appendChild(buttonHolder);
                        break;
                    case 'switch':
                        const switcher = document.createElement('button');
                        const settingValue = this.values[settingKey];
                        const settingValueTitle = setting.options[settingValue];
                        switcher.innerText = setting.title + ': ' + settingValueTitle;

                        if (typeof setting.disabled === 'function') {
                            switcher.disabled = setting.disabled();
                        }

                        if (switcher.disabled) {
                            buttonHolder.appendChild(switcher);
                            holder.appendChild(buttonHolder);
                            break;
                        }

                        switcher.addEventListener('click', async e => {
                            const valueKeys = Object.keys(setting.options);
                            const valueIndex = valueKeys.indexOf(settingValue);
                            let nextValueIndex = 0;
                            if (e.shiftKey) {
                                nextValueIndex = valueIndex - 1 < 0 ? valueKeys.length - 1 : valueIndex - 1;
                            } else {
                                nextValueIndex = valueIndex + 1 >= valueKeys.length ? 0 : valueIndex + 1;
                            }
                            const nextValue = valueKeys[nextValueIndex];
                            await self.set(settingKey, nextValue);
                            if (setting.meta) {
                                if (setting.meta.resetCache) {
                                    HISTORY.resetCache();
                                }
                            }
                            if (typeof setting.afterSaving === 'function') {
                                setting.afterSaving();
                            }
                            self._renderUi(holder, page);
                        });
                        if (setting.description) {
                            switcher.addEventListener('mouseover', function () {
                                self._renderDescription(holder, setting.description, pageInfo);
                            });
                            switcher.addEventListener('mouseout', function () {
                                self._renderDescription(holder, null, pageInfo);
                            });
                        }
                        buttonHolder.appendChild(switcher);
                        holder.appendChild(buttonHolder);
                        break;
                    case 'button':
                        const button = document.createElement('button');
                        button.innerText = setting.title;
                        
                        button.addEventListener('click', setting.action);
                        if (setting.description) {
                            button.addEventListener('mouseover', function () {
                                self._renderDescription(holder, setting.description, pageInfo);
                            });
                            button.addEventListener('mouseout', function () {
                                self._renderDescription(holder, null, pageInfo);
                            });
                        }
                        buttonHolder.appendChild(button);
                        holder.appendChild(buttonHolder);
                        break;
                }
            }
        }
    };

    const HOVER_INFOBOX_DATA = {
        event: null,
        run (shift=true) {
            if (infoBox.style.visibility == 'hidden') {
                // console.log('infoBox is hidden');
                return;
            }
            if (!this.event) {
                // console.log('no event');
                return;
            }
            if (!SETTINGS.values.hoverEnabled) {
                // console.log('hover info disabled');
                return;
            }
            if (SETTINGS.values.shiftHoverMode == 'disabled') {
                // console.log('shift hover mode disabled');
                return;
            }

            Object.defineProperty(this.event, 'shiftKey', {
                value: shift,
                writable: false,
                configurable: true,
            });

            unsafeWindow.infoCard(this.event);
        },
    };


    /******************************************************
     * Utility functions
     ******************************************************/
    function GM_addStyle(css) {
        const style = document.getElementById("GM_addStyle_Runon") || (function() {
            const style = document.createElement('style');
            style.type = 'text/css';
            style.id = "GM_addStyle_Runon";
            document.head.appendChild(style);
            return style;
        })();
        const sheet = style.sheet;
 
        sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length);
    }
 
    function GM_addStyle_object(selector, rules) {
        const nested = [];

        let ruleCount = 0;
        let css = selector + "{";
        for (const key in rules) {
            if (key[0] == '$') {
                nested.push({selector: key.substr(1).trim(), rules: rules[key]})
                continue;
            }
            ruleCount++;
            css += key.replace(/([A-Z])/g, g => `-${g[0].toLowerCase()}`) + ":" + rules[key] + ";";
        }
        css += "}";

        if (ruleCount) {
            GM_addStyle(css);
        }

        for(const nestedRules of nested) {
            const nestedSelector = nestedRules.selector.replace(/\&/g, selector);

            GM_addStyle_object(nestedSelector, nestedRules.rules);
        }

    }

    function stringExplode(string) {
        return Object.fromEntries(
            string.split("&").map((x) => x.split("="))
        );
    }

    function realQuantity(quantity, itemcategory) {
        if (itemcategory == 'armour' || itemcategory == 'weapon') {
            return 1;
        }

        return parseInt(quantity);
    }
 
    function maxStack(itemId, loose = false) {
        itemId = getGlobalDataItemId(itemId);
        const itemcat = unsafeWindow.globalData[itemId].itemcat;
        if (itemcat == 'armour' || itemcat == 'weapon') {
            return 1;
        }

        // TODO: check if doesnt cause unwanted side effects
        if (itemcat == 'credits') {
            return loose ? 9999999 : 1;
        }

        return unsafeWindow.globalData[itemId].max_quantity;
    }

    function uniqid(length = 16) { 
        return window.btoa(Array.from(window.crypto.getRandomValues(new Uint8Array(length * 2))).map((b) => String.fromCharCode(b)).join("")).replace(/[+/]/g, "").substr(0, length);
    }

    /**
     * Simple object check.
     * @param item
     * @returns {boolean}
     */
    function isObject(item) {
        return (item && typeof item === 'object' && !Array.isArray(item));
    }
    
    /**
     * Deep merge two objects.
     * @param target
     * @param ...sources
     */
    function mergeDeep(target, ...sources) {
        if (!sources.length) return target;
        const source = sources.shift();
    
        if (isObject(target) && isObject(source)) {
        for (const key in source) {
            if (isObject(source[key])) {
                if (!target.hasOwnProperty(key)) Object.assign(target, { [key]: {} });
                mergeDeep(target[key], source[key]);
            } else {
                Object.assign(target, { [key]: source[key] });
            }
        }
        }
    
        return mergeDeep(target, ...sources);
    }


    // Hook into webCall, after request is done, but before callback is executed
    function onBeforeWebCall(call, callback) {
        if (!call) { // If call is not specified, hook into all calls
            WEBCALL_HOOKS.beforeAll.push(callback);
            return;
        }

        if (!WEBCALL_HOOKS.before.hasOwnProperty(call)) {
            WEBCALL_HOOKS.before[call] = [];
        }

        WEBCALL_HOOKS.before[call].push(callback);
    }

    // Remove hook from webCall
    function offBeforeWebCall(call, callback) {
        if (!call) { // If call is not specified, remove hook from all calls
            const index = WEBCALL_HOOKS.beforeAll.indexOf(callback);
            if (index > -1) {
                WEBCALL_HOOKS.beforeAll.splice(index, 1);
            }
            return;
        }

        if (!WEBCALL_HOOKS.before.hasOwnProperty(call)) {
            return;
        }

        const index = WEBCALL_HOOKS.before[call].indexOf(callback);
        if (index > -1) {
            WEBCALL_HOOKS.before[call].splice(index, 1);
        }
    }

    // Hook into webCall, after request is done and after callback is executed
    function onAfterWebCall(call, callback) {
        if (!call) { // If call is not specified, hook into all calls
            WEBCALL_HOOKS.afterAll.push(callback);
            return;
        }

        if (!WEBCALL_HOOKS.after.hasOwnProperty(call)) {
            WEBCALL_HOOKS.after[call] = [];
        }

        WEBCALL_HOOKS.after[call].push(callback);
    }

    // Remove hook from webCall
    function offAfterWebCall(call, callback) {
        if (!call) { // If call is not specified, remove hook from all calls
            const index = WEBCALL_HOOKS.afterAll.indexOf(callback);
            if (index > -1) {
                WEBCALL_HOOKS.afterAll.splice(index, 1);
            }
            return;
        }

        if (!WEBCALL_HOOKS.after.hasOwnProperty(call)) {
            return;
        }

        const index = WEBCALL_HOOKS.after[call].indexOf(callback);
        if (index > -1) {
            WEBCALL_HOOKS.after[call].splice(index, 1);
        }
    }

    function formatDate(date, options = {}) {
        const {
            format = 'datetime',
        } = options;

        const offset = date.getTimezoneOffset()
        date = new Date(date.getTime() - (offset*60*1000))

        if (format == 'datetime') {
            return date.toISOString().split('.')[0].replace('T', ' ');
        } else if (format == 'date') {
            return date.toISOString().split('T')[0];
        }

        throw new Error('Invalid date format: ' + format);
    }

    function formatNumber(num, options = {}) {
        const {
            minimumFractionDigits = 0,
            maximumFractionDigits = 2,
        } = options;

        return (new Number(num))
            .toLocaleString('en-US', {minimumFractionDigits, maximumFractionDigits})
            .replace(/\.0+$/, '');
    }

    function formatMoney(num, options = {}) {
        if (typeof options == 'boolean') {
            options = { plus: options };
        }

        const {
            plus = false,
            showFree = false,
        } = options;

        if (num == 0 && showFree) {
            return 'Free';
        }

        const plusSign = plus ? '+' : '';

        return (num < 0 ? '-' : plusSign) + '$' + formatNumber(Math.abs(num), options);
    }

    function formatMoneyHtml(num, options = {}) {
        if (typeof options == 'boolean') {
            options = { neutralColor: options, plus: false };
        }

        const {
            neutralColor = false,
        } = options;

        let color = '#FFCC00';
        if (!neutralColor) {
            color = num < 0 ? '#FF0000' : '#00FF00';
        }

        return '<span style="color: ' + color + '">' + formatMoney(num, options) + '</span>';
    }

    function historyAction(e) {

        var question = false;
        var action;
        var extraData = {};
        switch(e.target.dataset.action) {
            case 'switchHistory':
                unsafeWindow.prompt.innerHTML = "<div style='text-align: center'>Loading, please wait...</div>";
                unsafeWindow.prompt.parentNode.style.display = "block";
                unsafeWindow.historyScreen = e.target.dataset.page;
                loadMarket();
                return;
                break;
        }
    }

    function debouncedItemSearch() {
        const searchFn = function (query) {
            query = query.toLowerCase().replace(/[\.\s]/g, '').trim();
            if (!query.length) {
                return [];
            }
            return SEARCHABLE_ITEMS
                .filter(itemId => {
                    const itemName = unsafeWindow.itemNamer(itemId, '')
                        .toLowerCase()
                        .replace(/[\.\s]/g, '');
                    return itemName.includes(query) || itemId.includes(query);
                })
                .map(item => {
                    return {
                        item,
                        // name: itemNamer(item, maxStack(item)),
                        name: itemNamer(item, ''),
                    }
                })
                .sort((a, b) => {
                    const aName = a.name?.toLowerCase();
                    const bName = b.name?.toLowerCase();
                    return aName.localeCompare(bName);
                });
        };
        
        let timeout;
        return function (query, callback) {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                callback(searchFn(query));
            }, 400);
        };
    }

    // To be called to check if results box should be hidden
    function onDocumentClick(e) {
        const historyItemSearchResultBox = document.getElementById('historyItemSearchResultBox');
        if (!historyItemSearchResultBox || (e.target.closest('#historyItemSearchResultBox') || e.target.closest('#historySearchArea input'))) {
            return;
        }
        historyItemSearchResultBox.classList.add('hidden');
    }

    unsafeWindow.addEventListener('click', onDocumentClick);

    // @see https://stackoverflow.com/a/24922761
    function exportToCsv(filename, rows, seperator) {
        var processRow = function (row) {
            var finalVal = '';
            for (var j = 0; j < row.length; j++) {
                var innerValue = row[j] === null ? '' : row[j].toString();

                if (row[j] instanceof Date) {
                    innerValue = row[j].toLocaleString();
                };
                var result = innerValue.replace(/"/g, '""');
                if (result.search(/("|,|\n|\s)/g) >= 0)
                    result = '"' + result + '"';
                if (j > 0)
                    finalVal += seperator;
                finalVal += result;
            }
            return finalVal + '\n';
        };
    
        var csvFile = '';
        for (var i = 0; i < rows.length; i++) {
            csvFile += processRow(rows[i]);
        }
    
        var blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' });
        if (navigator.msSaveBlob) { // IE 10+
            navigator.msSaveBlob(blob, filename);
        } else {
            var link = document.createElement("a");
            if (link.download !== undefined) { // feature detection
                // Browsers that support HTML5 download attribute
                var url = URL.createObjectURL(blob);
                link.setAttribute("href", url);
                link.setAttribute("download", filename);
                link.style.visibility = 'hidden';
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            }
        }
    }

    function getGlobalDataItemId(rawItemId) {
        return rawItemId.split('_')[0];
    }

    function getBaseItemId(rawItemId) {
        return rawItemId.replace(/_stats\d+/, '');
    }

    function getTresholdDateForTimeframe(timeframe) {
        let tresholdDate = null;
        switch (timeframe) {
            case 'last_24hr':
                tresholdDate = new Date();
                tresholdDate.setDate(tresholdDate.getDate() - 1);
                break;
            case 'last_week':
                tresholdDate = new Date();
                tresholdDate.setDate(tresholdDate.getDate() - 7);
                break;
            case 'last_month':
                tresholdDate = new Date();
                tresholdDate.setMonth(tresholdDate.getMonth() - 1);
                break;
            case 'last_3_months':
                tresholdDate = new Date();
                tresholdDate.setMonth(tresholdDate.getMonth() - 3);
                break;
            case 'last_6_months':
                tresholdDate = new Date();
                tresholdDate.setMonth(tresholdDate.getMonth() - 6);
                break;
            case 'last_year':
                tresholdDate = new Date();
                tresholdDate.setFullYear(tresholdDate.getFullYear() - 1);
                break;
            case 'ytd':
                tresholdDate = new Date();
                tresholdDate.setMonth(0);
                tresholdDate.setDate(1);
                break;
            case 'mtd':
                tresholdDate = new Date();
                tresholdDate.setDate(1);
                break;
            case 'wtd':
                tresholdDate = new Date();
                tresholdDate.setDate(tresholdDate.getDate() - tresholdDate.getDay());
                break;
        }

        return tresholdDate;
    }

    function parseDateTimeString(value) {
        const pattern = /^(\d{4})\-(\d{2})\-(\d{2})(?:\s(\d{2}):(\d{2})(?:\:(\d{2}))?)?$/;
        const match = value.match(pattern);

        if (!match) {
            return null;
        }

        const year = parseInt(match[1]);
        const month = parseInt(match[2]) - 1;
        const day = parseInt(match[3]);
        const hour = parseInt(match[4]) || 0;
        const minute = parseInt(match[5]) || 0;
        const second = parseInt(match[6]) || 0;

        return new Date(year, month, day, hour, minute, second);
    }

    function ready() {
        return new Promise(resolve => {
            if (document.readyState === "complete" || document.readyState === "interactive") {
                resolve();
            } else {
                document.addEventListener("DOMContentLoaded", resolve);
            }
        });
    }

    function injectHistoryTabIntoMarketplace() {
        if (unsafeWindow.document.getElementById('loadHistory')) {
            return;
        }
        const pageNavigation = document.getElementById('selectMarket');
        if (!pageNavigation) {
            return;
        }
        
        // Async context, i dont like nested callbacks, i like async/await
        (async function () {
            // Add history button
            const historyBtn = document.createElement('button');
            historyBtn.setAttribute('data-action', 'switchMarket');
            historyBtn.setAttribute('data-page', 'history');
            historyBtn.setAttribute('id', 'loadHistory');
            historyBtn.innerText = 'history';
            historyBtn.addEventListener("click", marketAction);
            pageNavigation.appendChild(historyBtn);

    
            switch (unsafeWindow.marketScreen) {
                case 'history':
                    historyBtn.disabled = true;
                    pageLogo.textContent = "Market History";

                    const historyNavigation = document.createElement('div');
                    historyNavigation.id = 'selectHistoryCategory';
                    pageNavigation.after(historyNavigation);

                    const listBtn = document.createElement('button');
                    listBtn.setAttribute('data-action', 'switchHistory');
                    listBtn.setAttribute('data-page', 'list');
                    listBtn.setAttribute('id', 'historyList');
                    listBtn.innerText = 'list';
                    listBtn.addEventListener("click", historyAction);
                    
                    const statsBtn = document.createElement('button');
                    statsBtn.setAttribute('data-action', 'switchHistory');
                    statsBtn.setAttribute('data-page', 'stats');
                    statsBtn.setAttribute('id', 'historyStats');
                    statsBtn.innerText = 'statistics';
                    statsBtn.addEventListener("click", historyAction);


                    historyNavigation.appendChild(listBtn);
                    historyNavigation.appendChild(statsBtn);

                    const searchBox = document.createElement("div");
                    searchBox.id = "historySearchArea";

                    const filterBox = document.createElement("div");
                    filterBox.id = "historyFilterArea";

                    filterBox.innerHTML = `
                    <div style="position: relative;">
                        <div class="opElem" id="filterActionTypeWrapper">
                            <div data-value="${HISTORY.filters.type}" id="filterActionType" class="historySelectComponent">
                                <div class="selectChoice">
                                    <span></span>
                                    <span></span>
                                </div>
                                <div class="selectList">
                                    <div data-value="all" class="selectOption">- All -</div>
                                    <div data-value="buy" class="selectOption">Buy</div>
                                    <div data-value="sell" class="selectOption">Sell</div>
                                    <div data-value="scrap" class="selectOption">Scrap</div>
                                    <div data-value="sell_scrap" class="selectOption">Sell/Scrap</div>
                                </div>
                            </div>
                        </div>
                        
                        <input type="date" id="filterMinDate" class="opElem" value="${HISTORY.filters.minDate ? formatDate(new Date(HISTORY.filters.minDate), {format: 'date'}) : ''}"/>
                        <input type="date" id="filterMaxDate" class="opElem" value="${HISTORY.filters.maxDate ? formatDate(new Date(HISTORY.filters.maxDate), {format: 'date'}) : ''}"/>

                        <button id="filterGo" class="opElem">Filter</button>
                    </div>
                    `;

                    initHistorySelects();

                    let searchInput;

                    if (HISTORY.selectedItem) {
                        const itemName = unsafeWindow.itemNamer(HISTORY.selectedItem, HISTORY.selectedItem == 'credits' ? '' : maxStack(HISTORY.selectedItem));
                        /*<div style='display: inline-block;' class="itemName cashhack cashhack-relative" data-cash="${itemName}">
                            </div> */
                        searchBox.innerHTML = `
                            <button id="clearHistoryItem">[x]</button>

                            <div class="opElem" id="selectedItemsWrapper">
                                <div data-value="${HISTORY.selectedItem}" id="selectedItems" class="historySelectComponent">
                                    <div class="selectChoice">
                                        <span></span>
                                        <span></span>
                                    </div>
                                    <div class="selectList">
                                        <div data-value="${HISTORY.selectedItem}" class="selectOption">${itemName}</div>
                                    </div>
                                </div>
                            </div>
                        `;

                        const clearHistoryItemBtn = searchBox.querySelector('#clearHistoryItem');
                        clearHistoryItemBtn.addEventListener('click', function () {
                            HISTORY.setSelectedItem(null);
                            loadMarket();
                        });
                    } else {
                        searchBox.innerHTML = `
                            <div style='text-align: left; width: 185px; display: inline-block;'>
                                <input id='historySearchField' placeholder='Type to search' type='text' name='historySearch' />
                            </div>
                        `;
                        
                        searchInput = searchBox.querySelector('#historySearchField');

                        
                        const searchFn = debouncedItemSearch();

                        searchInput.addEventListener('input', function () {
                            const value = this.value;
                            searchFn(value, function (results) {
                                searchResultBox.innerHTML = '';
                                for(const result of results) {
                                    // const resultRow = document.createElement('div');

                                    const resultButton = document.createElement('button');
                                    resultButton.innerText = result.name;
                                    resultButton.style.width = '100%';
                                    resultButton.style.textAlign = 'left';
                                    resultButton.classList.add("fakeItem");
                                    resultButton.setAttribute("data-type", result.item);
                                    resultButton.setAttribute("data-quantity", result.item == 'credits' ? '' : maxStack(result.item));
        
                                    resultButton.addEventListener('click', async function () {
                                        this.disabled = true;
                                        await HISTORY.setSelectedItem(result.item)
                                        searchResultBox.innerHTML = '';
                                        searchResultBox.classList.add('hidden');

                                        loadMarket();
                                        // searchInput.value = result.name;
                                        // searchResultBox.innerHTML = '';
                                    });
                                    // resultRow.appendChild(resultButton);
                                    // searchResultBox.appendChild(resultRow);
                                    searchResultBox.appendChild(resultButton);
                                }

                                if (!value.length) {
                                    searchResultBox.classList.add('hidden');
                                } else {
                                    searchResultBox.classList.remove('hidden');

                                    if (!results.length) {
                                        const noResults = document.createElement('div');
                                        noResults.innerText = 'No results found';
                                        searchResultBox.appendChild(noResults);
                                    }
                                }
                            });
                        });
                        searchInput.addEventListener('blur', function (e) {
                            // Check if not focused on result box
                            if (e.relatedTarget && e.relatedTarget.parentNode.id == 'historyItemSearchResultBox') {
                                return;
                            }

                            searchResultBox.classList.add('hidden');
                        });
                        searchInput.addEventListener('focus', function () {
                            // If result box has results, show it
                            if (searchResultBox.children.length) {
                                searchResultBox.classList.remove('hidden');
                            }
                        });

                        const searchResultBox = document.createElement("div");
                        searchResultBox.id = "historyItemSearchResultBox";
                        searchResultBox.classList.add("hidden");
                        
                        searchBox.appendChild(searchResultBox);

                    }
                    marketHolder.appendChild(searchBox);

                    if (unsafeWindow.historyScreen == 'list') {

                        marketHolder.appendChild(filterBox);
                        unsafeWindow.document.getElementById('filterActionType').oncustomselect = function (event) {
                            if (event.cause == 'init') {
                                return;
                            }
                            
                            HISTORY.filters.type = event.value;
    
                            // loadMarket();
                        }
    
                        unsafeWindow.document.getElementById('filterMinDate').addEventListener('change', function () {
                            // console.log('min date change');
                            // console.log(this.value);
                            let value = this.value || null;
    
                            if (value) {
                                // Convert yyyy-mm-dd to timestamp like from Date.now()
                                value = new Date(value).getTime();
                            }
    
                            HISTORY.filters.minDate = value;
                        });
    
                        unsafeWindow.document.getElementById('filterMaxDate').addEventListener('change', function () {
                            // console.log('max date change');
                            // console.log(this.value);
                            let value = this.value || null;
    
                            if (value) {
                                // Convert yyyy-mm-dd to timestamp like from Date.now()
                                value = new Date(value).getTime();
                            }
    
                            HISTORY.filters.maxDate = value;
                        });
    
                        unsafeWindow.document.getElementById('filterGo').addEventListener('click', async function () {
                            if (this.disabled) {
                                return;
                            }
    
                            this.disabled = true;
                            
                            await HISTORY.saveFilters();
                            
                            loadMarket();
                        });
                    }

                    initHistorySelects();
                    // Add history navbar below
    
                    switch (unsafeWindow.historyScreen) {
                        case 'list':
                            listBtn.disabled = true;

                            let historyEntries = HISTORY.getFilteredEntries();
                            
                            
                            const boxLabels = document.createElement("div");
                            boxLabels.id = "historyLabels";
                            boxLabels.innerHTML = `
                                <span>Item Name</span>
                                <span style='position: absolute; left: 208px; width: 80px; width: max-content;'>Type</span>
                                <span style='position: absolute; left: 320px; width: max-content;'>Price</span>
                                <span style='position: absolute; left: 480px; width: 70px; width: max-content;'>Datetime</span>
                            `;
                            boxLabels.classList.add("opElem");
                            boxLabels.style.top = "141px";
                            boxLabels.style.left = "26px";

                            const insertBtn = document.createElement("button");
                            insertBtn.id = "historyInsertBtn";
                            insertBtn.classList.add("opElem");
                            insertBtn.style.top = "80px";
                            insertBtn.style.right = "20px";
                            insertBtn.innerText = 'Create new entry';
                            insertBtn.addEventListener('click', function () {
                                if (pageLock) return;
                                if (!HISTORY.selectedItem) {
                                    alert('Please select an item first to create an entry of');
                                    searchInput?.focus();
                                    return;
                                }
                                HISTORY.renderEntryFormPrompt({
                                    item: HISTORY.selectedItem,
                                });
                            });

                            marketHolder.appendChild(insertBtn);


                            const historyResultsText = document.createElement("div");
                            historyResultsText.id = "historyResultsText";
                            historyResultsText.classList.add("opElem");
                            historyResultsText.style.top = "80px";
                            historyResultsText.style.left = "20px";
                            // historyResultsText.style.width = "100%";
                            historyResultsText.innerText = historyEntries.length + ' result' + (historyEntries.length == 1 ? '' : 's') + ' found';
            
                            const historyItemDisplay = document.createElement("div");
                            historyItemDisplay.id = "historyItemDisplay";
                            historyItemDisplay.classList.add("marketDataHolder");
                            historyItemDisplay.setAttribute('data-offset', 0);
                            historyItemDisplay.setAttribute('data-per-page', 20);
                            
                            const renderHistoryItems = function () {
                                const offset = parseInt(historyItemDisplay.getAttribute('data-offset'));
                                const perPage = parseInt(historyItemDisplay.getAttribute('data-per-page'));
        
                                const entryCount = historyEntries.length;
        
                                if (offset >= entryCount) {
                                    return false;
                                }
                                
                                for(let i = 0; i < perPage; i++) {
                                    const entryIndex = entryCount - offset - i - 1;
                                    const entry = historyEntries[entryIndex] || null;
                                    // const entryIndex = i + offset;
                                    // const entry = historyEntries[entryIndex] || null;
                                    if (!entry) {
                                        continue;
                                    }
                                    const isPending = HISTORY.cache.pending_trade_ids.includes(entry.trade_id);
                                    
                                    // if (isPending) {
                                    //     continue;
                                    // }
        
                                    // <div class="fakeItem" data-type="avalanchemg14_stats777" data-quantity="1" data-price="53000000"><div class="itemName cashhack credits" data-cash="Avalanche MG14">Avalanche MG14</div> <span style="color: #c0c0c0;">(AC)</span><div class="tradeZone">Outpost</div><div class="seller">ScarHK</div><div class="salePrice" style="color: red;">$53,000,000</div><button disabled="" data-action="buyItem" data-item-location="1" data-buynum="350533865">buy</button></div>
                                    const row = document.createElement("div");
                                    row.classList.add("fakeItem");
                                    if (isPending) {
                                        row.classList.add("pending");
                                    }
                                    row.setAttribute("data-type", entry.item || 'broken');
                                    row.setAttribute("data-quantity", entry.quantity || 1);
                                    row.setAttribute("data-price", entry.price || 0);
                                    row.setAttribute("data-trade-id", entry.trade_id);

                                    row.addEventListener('click', function () {
                                        if (pageLock) return;

                                        const type = this.getAttribute('data-type');
                                        if (type == 'broken') {
                                            alert('Entry is broken, contact Runon with this data: ' + JSON.stringify(entry));

                                            if (confirm('Remove this entry instead?')) {
                                                // const tradeId = this.getAttribute('data-trade-id');
                                                HISTORY.removeTrade(entryIndex, true);
                                                loadMarket();
                                            }

                                            return;
                                        }

                                        const tradeId = this.getAttribute('data-trade-id');
                                        let trade = HISTORY.cache.trade_id[tradeId];
                                        if (!trade) {
                                            trade = HISTORY.entries.find(trade => trade.trade_id == tradeId);
                                        }

                                        if (!trade) {
                                            alert('Could not find trade in cache or entries (ID: ' + tradeId + ')');
                                            return;
                                        }

                                        HISTORY.renderEntryPrompt(trade);
                                    });

        
                                    let afterName = entry.item ? calcMCTag(entry.item, false, "span", "") || '' : '';
        
                                    const itemId = entry.item ? getGlobalDataItemId(entry.item) : 'broken';
                                    const itemCat = entry.item ? getItemType(unsafeWindow.globalData[itemId]) : null;
                                    if (itemCat == 'ammo') {
                                        afterName += ' <span>(' + entry.quantity + ')</span>';
                                    }
                                    const displayPrice = formatMoney(entry.price || 0, {showFree: true});
                                    
                                    row.innerHTML = `
                                        <div class="itemName cashhack credits" data-cash="${entry.itemname}">${entry.itemname}</div>
                                        ${afterName}
                                        <div class="tradeType">${entry.action}</div>
                                        <div class="salePrice">${displayPrice}</div>
                                        <div class="saleDate">${formatDate(new Date(entry.date))}</div>
                                    `;
                                    // row.innerHTML = "<div class='itemName cashhack credits' data-cash='" + entry.itemname + "'>" + entry.itemname + "</div><div class='tradeZone'>" + entry.trade_zone + "</div><div class='seller'>" + entry.member_name + "</div><div class='salePrice' style='color: red;'>$" + entry.price + "</div>";
                                    historyItemDisplay.appendChild(row);
                                }
        
                                return true;
                            };
                            
                            const onHistoryScroll = function () {
                                const fullScrollHeight = historyItemDisplay.scrollHeight;
                                const scrolledHeight = historyItemDisplay.scrollTop + historyItemDisplay.clientHeight;
                                const diff = fullScrollHeight - scrolledHeight;
        
                                if (diff > 50) {
                                    return;
                                }
        
                                const perPage = parseInt(historyItemDisplay.getAttribute('data-per-page'));
                                historyItemDisplay.removeEventListener('scroll', onHistoryScroll);
                                historyItemDisplay.setAttribute('data-offset', parseInt(historyItemDisplay.getAttribute('data-offset')) + perPage);
                                const hasMore = renderHistoryItems();
                                if (hasMore) {
                                    historyItemDisplay.addEventListener('scroll', onHistoryScroll);
                                }
                            };
        
                            marketHolder.appendChild(historyItemDisplay);
                            marketHolder.appendChild(boxLabels);
                            marketHolder.appendChild(historyResultsText);
            
                            await HISTORY.load();
                            // retrieve current user's pending trades
                            await new Promise(resolve => {
                                const now = Date.now();
                                const lastCheck = WEBCALL_HOOKS.lastExecutedAt.trade_search || 0;

                                // Check if last check was less than 30 seconds ago
                                if (lastCheck && (now - lastCheck) < 30000) {
                                    resolve();
                                    return;
                                }

                                var dataArray = {};
        
                                dataArray["pagetime"] = userVars["pagetime"];
                                dataArray["tradezone"] = "";
                                dataArray["searchname"] = "";
                                dataArray["searchtype"] = "sellinglist";
                                dataArray["search"] = "trades";
                                dataArray["memID"] = userVars["userID"];
                                dataArray["category"] = "";
                                dataArray["profession"] = "";
                                
                                WEBCALL_HOOKS.lastExecutedAt.trade_search = now;

                                // Execute webCall
                                webCall("trade_search", dataArray, resolve, true);
                                // Cache will be updated by webCall hook somewhere else in the code
                            });
                            
                            renderHistoryItems();
                            historyItemDisplay.addEventListener('scroll', onHistoryScroll);
                            break;
                        case 'stats':
                            statsBtn.disabled = true;

                            // const filterBox = document.createElement("div");
                            // filterBox.id = "historyFilterArea";
                            // Filter box shows items that are added to search on
                            // But also a date range select
                            // filterBox.innerHTML =

                            // categorySelect += "<div style='display: inline-block; width: 260px;'>In Category:<br/><div id='categoryChoice' data-catname=''><span id='cat'>Everything</span><span id='dog' style='float: right;'>&#9668;</span></div>";
                            // <div class="historyDetailsText">Click on a trade to see more info</div>
                            const historyInfoBox = document.createElement("div");
                            historyInfoBox.id = "historyInfoBox";

                            if (HISTORY.selectedItem) {
                                const globalStatisticItem = unsafeWindow.globalData[getGlobalDataItemId(HISTORY.selectedItem)];
                                const isAmmo = globalStatisticItem.itemcat == 'ammo';
                                

                                const stackSize = maxStack(HISTORY.selectedItem);

                                const perNamer = function (amount) {
                                    let perName = 'item';
                                    if (isAmmo) {
                                        perName = 'round';
                                    }

                                    if (HISTORY.selectedItem == 'fuelammo') {
                                        return 'mL';
                                    }

                                    return perName + (amount == 1 ? '' : 's');
                                };
                                const perStackNamer = function (amount) {
                                    return 'stack' + (amount == 1 ? '' : 's');
                                };

                    
                                // === START OF STATS RENDER

                                // Title with timeframe
                                const timeframe = SETTINGS.values.hoverStatisticsTimeframe;

                                // Bought stats
                                const amountBought = HISTORY.getItemInfo(HISTORY.selectedItem, 'amount_bought');
                                const amountBoughtStacks = new Number(amountBought / stackSize).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 2});

                                const totalPriceBought = HISTORY.getItemInfo(HISTORY.selectedItem, 'total_price_bought');
                                const avgPriceBought = HISTORY.getItemInfo(HISTORY.selectedItem, 'avg_price_bought');

              
                                // Sold stats
                                const amountSold = HISTORY.getItemInfo(HISTORY.selectedItem, 'amount_sold');
                                const amountSoldStacks = new Number(amountSold / stackSize).toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 2});

                                const totalPriceSold = HISTORY.getItemInfo(HISTORY.selectedItem, 'total_price_sold');
                                const avgPriceSold = HISTORY.getItemInfo(HISTORY.selectedItem, 'avg_price_sold');

                                // Profit/Loss stats
                                const averageProfit = avgPriceSold - avgPriceBought;
                                const totalProfit = totalPriceSold - totalPriceBought;



                                const totalProfitItemCount = Math.min(amountSold, amountBought);
                                let totalRealProfit = 0;

                                if (totalProfitItemCount > 0) {
                                    totalRealProfit = (totalProfitItemCount * avgPriceSold) - (totalProfitItemCount * avgPriceBought);
                                }


                                const lastBoughtAt = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_date_bought');
                                const lastBoughtFor = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_price_bought');

                                const lastSoldAt = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_date_sold');
                                const lastSoldFor = HISTORY.getItemInfo(HISTORY.selectedItem, 'last_price_sold');

                                // === END OF STATS RENDER

                                historyInfoBox.innerHTML = `
                                    <table>
                                        <tr class="row">
                                            <td>Amount bought</td>
                                            <td>
                                                <span style="color: #FFCC00;">${amountBought}</span> ${perNamer(amountBought)}
                                                ${isAmmo
                                                    ? `<br>≈ <span style="color: #FFCC00;">${amountBoughtStacks}</span> ${perStackNamer(amountBoughtStacks)}`
                                                    : ``
                                                }
                                            </td>
                                            <td>
                                                for ${formatMoneyHtml(totalPriceBought, true)} total
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Amount sold</td>
                                            <td>
                                                <span style="color: #FFCC00;">${amountSold}</span> ${perNamer(amountSold)}
                                                ${isAmmo
                                                    ? `<br>≈ <span style="color: #FFCC00;">${amountSoldStacks}</span> ${perStackNamer(amountSoldStacks)}`
                                                    : ``
                                                }
                                            </td>
                                            <td>
                                                for ${formatMoneyHtml(totalPriceSold, true)} total
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Average buy price</td>
                                            <td>
                                                ${formatMoneyHtml(avgPriceBought, true)} per ${perNamer(1)}
                                                ${isAmmo
                                                    ? `<br>${formatMoneyHtml(avgPriceBought * stackSize, true)} per ${perStackNamer(1)}`
                                                    : ``
                                                }
                                            </td>
                                            <td>
                                                
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Average sell price</td>
                                            <td>
                                                ${formatMoneyHtml(avgPriceSold, true)} per ${perNamer(1)}
                                                ${isAmmo
                                                    ? `<br>${formatMoneyHtml(avgPriceSold * stackSize, true)} per ${perStackNamer(1)}`
                                                    : ``
                                                }
                                            </td>
                                            <td>
                                                
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Average profit/loss</td>
                                            <td>
                                                ${formatMoneyHtml(averageProfit, {neutralColor: false, maximumFractionDigits: 4})} per ${perNamer(1)}
                                                ${isAmmo
                                                    ? `<br>${formatMoneyHtml(averageProfit * stackSize, {neutralColor: false, maximumFractionDigits: 4})} per ${perStackNamer(1)}`
                                                    : ``
                                                }
                                            </td>
                                            <td>
                                                ${SETTINGS.values.countScraps
                                                    ? '(With scraps)'
                                                    : '(Without scraps)'
                                                }
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Real total profit/loss</td>
                                            <td>
                                                ${formatMoneyHtml(totalRealProfit, false)}
                                            </td>
                                            <td>
                                                (Based on <span style="color: #FFCC00;">${totalProfitItemCount}</span> buys & sells)
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Total profit/loss</td>
                                            <td>
                                                ${formatMoneyHtml(totalProfit, false)}
                                            </td>
                                            <td>
                                                (Based on <span style="color: #FFCC00;">${amountBought}</span> buys, <span style="color: #FFCC00;">${amountSold}</span> sells)
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Last bought</td>
                                            <td>
                                                ${lastBoughtAt
                                                    ? `at <span style="color: #FFCC00;">${formatDate(new Date(lastBoughtAt))}</span>`
                                                    : `Never bought`
                                                }
                                            </td>
                                            <td>
                                                ${lastBoughtFor
                                                    ? `for ${formatMoneyHtml(lastBoughtFor, true)}`
                                                    : ``
                                                }
                                            </td>
                                        </tr>
                                        <tr class="row">
                                            <td>Last sold</td>
                                            <td>
                                                ${lastSoldAt
                                                    ? `at <span style="color: #FFCC00;">${formatDate(new Date(lastSoldAt))}</span>`
                                                    : `Never sold`
                                                }
                                            </td>
                                            <td>
                                                ${lastSoldFor
                                                    ? `for ${formatMoneyHtml(lastSoldFor, true)}`
                                                    : ``
                                                }
                                            </td>
                                        </tr>

                                    </table>
                                `;


                            } else {
                                historyInfoBox.innerHTML = `
                                    <div class="historyDetailsContainer">
                                        <div class="historyDetailsText">Search for an item to see its statistics</div>
                                    </div>
                                `;
                            }


                            // marketHolder.appendChild(filterBox);
                            marketHolder.appendChild(historyInfoBox);

                            break;
                    }
                    
                    promptEnd();
                    break;
            }
        })();
    }

    function initHistorySelects() {

        const historySelectComponents = document.getElementsByClassName('historySelectComponent');
    
        for(const historySelectComponent of historySelectComponents) {
            if (historySelectComponent.dataset.init) {
                continue;
            }
    
            const initValue = historySelectComponent.dataset.value;
            const choiceElem = historySelectComponent.getElementsByClassName('selectChoice')[0];
            const [name, dog] = choiceElem.children;
            dog.textContent = '◄';
            const listElem = historySelectComponent.getElementsByClassName('selectList')[0];
    
            const options = listElem.children;
            listElem.style.display = 'none';
    
            const selectOption = function (value, isInit = false) {
                const eventObject = {
                    target: historySelectComponent,
                    value: value,
                    canceled: false,
                    cause: isInit ? 'init' : 'change',
                };

                if (historySelectComponent.oncustomselect) {
                    historySelectComponent.oncustomselect(eventObject);
                }

                if (eventObject.canceled) {
                    return;
                }

                historySelectComponent.dataset.value = value;
    
                const label = Array.from(options).find(option => option.dataset.value == value)?.textContent;
    
                name.textContent = label;
            };
    
            const toggleSelect = function () {
                const display = listElem.style.display;
                const isHidden = display == 'none';
    
                if (isHidden) {
                    listElem.style.display = 'block';
                    dog.textContent = '▼';
                } else {
                    listElem.style.display = 'none';
                    dog.textContent = '◄';
                }
            };
    
            choiceElem.addEventListener('click', toggleSelect);
    
            for(const option of options) {
                option.addEventListener('click', function () {
                    const value = this.dataset.value;
                    selectOption(value);
                    toggleSelect();
                });
            }
    
            if (initValue) {
                selectOption(initValue, true);
            }

            historySelectComponent.dataset.init = true;
        }
    }

    /******************************************************
     * Styles
     ******************************************************/
    // GM_addStyle_object('#marketplace', {});
    GM_addStyle_object('.cashhack.cashhack-relative', {
        '$ &:before': {
            position: 'relative',
        },
        position: 'relative',
    });

    GM_addStyle_object('.historyInfoContainer', {
        textAlign: 'left',
    });

    GM_addStyle_object('#marketplace #historySettings', {
        position: 'absolute',
        top: '5px',
        right: '5px',
        width: '20px',
        height: '20px',
        backgroundImage: 'url(../images/df_gear.png)',
        backgroundSize: 'cover',
        cursor: 'pointer',
    });

    GM_addStyle_object('#marketplace #historyItemDisplay', {
        top: '155px',
        bottom: '110px',
    });
    GM_addStyle_object('.historyEntryForm', {
        '$ & input::placeholder': {
            color: 'rgba(255, 255, 0, 0.3)',
        },
    });

    GM_addStyle_object('#marketplace #historySearchArea', {
        position: 'absolute',
        top: '100px',
        left: '20px',
        // right: '80px',
        height: '16px',
        width: '250px',
        padding: '8px',
        border: '1px #990000 solid',
        textAlign: 'left',
        backgroundColor: 'rgba(0,0,0,0.8)',

        '$ & #historySearchField::placeholder': {
            color: 'rgba(255, 255, 0, 0.3)',
        },
        
        '$ & #historyItemSearchResultBox': {
            position: 'absolute',
            // top: '184px',
            width: '250px',
            maxHeight: '300px',
            overflowY: 'auto',
            padding: '4px',
            border: '1px #990000 solid',
            textAlign: 'left',
            backgroundColor: 'rgba(0,0,0,0.8)',
            zIndex: '100',

            '$ &.hidden': {
                display: 'none',
            },
        },
    });
    GM_addStyle_object('#marketplace #historyFilterArea', {
        position: 'absolute',
        top: '100px',
        left: '290px',
        right: '20px',
        height: '16px',
        padding: '8px',
        border: '1px #990000 solid',
        textAlign: 'left',
        backgroundColor: 'rgba(0,0,0,0.8)',

        '$ & #filterMinDate': {
            left: '110px',
        },
        '$ & #filterMaxDate': {
            left: '210px',
        },
        '$ & #filterGo': {
            left: '310px',
        },
    });

    GM_addStyle_object('#marketplace #historyInfoBox', {
        position: 'absolute',
        overflowY: 'auto',
        top: '141px',
        left: '20px',
        right: '20px',
        bottom: '110px',
        // padding: '8px',
        border: '1px #990000 solid',
        textAlign: 'left',
        backgroundColor: 'rgba(0,0,0,0.8)',

        '$ & .historyDetailsContainer': {
            fontSize: '14px',
            width: '100%',
            height: '100%',
            '$ & .historyDetailsText': {
                margin: '0',
                position: 'absolute',
                top: '50%',
                left: '50%',
                width: '100%',
                textAlign: 'center',
                transform: 'translateY(-50%) translateX(-50%)',
            }
        },

        '$ & table': {
            fontSize: '12px',
            fontFamily: '"Courier New", "Arial"',
            lineHeight: '1',
            width: '100%',
            borderCollapse: 'collapse',

            // Table is full width, but only the last td takes up as much space, the rest is just as wide as the content
            '$ & td': {
                width: '1%',
                whiteSpace: 'nowrap',
                // padding: '4px',
                //padding x is 4px, padding y is 2px
                padding: '0px 4px',
                // border: '1px #990000 solid',
                textAlign: 'left',
                height: '32px',

                '$ &:first-child': {
                    width: '200px',
                },
                '$ &:last-child': {
                    width: '100%',
                },
            },


            '$ & tr': {
                borderBottom: '1px #990000 solid',
                '$ &.row:hover': {
                    backgroundColor: 'rgba(125, 0, 0, 0.4)',
                },
            },
        },

    });

    GM_addStyle_object('#marketplace #historyItemDisplay .fakeItem', {
        paddingLeft: '6px',
        fontSize: '9pt',
        height: '16px',
        cursor: 'pointer',
        userSelect: 'none',

        '$ &.pending': {
            opacity: '0.5',
        },
        '$ &:hover': {
            backgroundColor: 'rgba(125, 0, 0, 0.8)',
        },
        '$ & > div': {
            display: 'inline-block',
            position: 'relative',
        },
        '$ & .tradeType': {
            position: 'absolute',
            left: '214px',
            color: '#00FF00',
        },
        '$ & .salePrice': {
            position: 'absolute',
            left: '326px',
            color: '#FFCC00',
        },
        '$ & .saleDate': {
            position: 'absolute',
            left: '486px',
        },
    });

    GM_addStyle_object('#marketplace #selectHistoryCategory', {
        position: 'absolute',
        width: '100%',
        top: '70px',
        fontSize: '12pt',

        '$ & button': {
            width: '120px',
        },
    });

    GM_addStyle_object('.historySelectComponent', {
        position: 'relative',
        '$ & .selectChoice': {
            position: 'absolute',
            cursor: 'pointer',
            width: '80px',
            display: 'inline-block',
            textAlign: 'center',
            backgroundColor: '#222',
            border: '1px solid #990000',

            '$ & span:last-child': {
                position: 'absolute',
                right: '0',
            },

        },
        '$ & .selectList': {
            position: 'absolute',
            display: 'none',
            position: 'absolute',
            zIndex: '10',
            top: '18px',
            width: '80px',
            // overflowY: 'auto',
            // left: '193px',
            backgroundColor: '#111',
            // borderLeft: '1px solid #990000',
            border: '1px solid #990000',
            textAlign: 'center',

            '$ & div': {
                cursor: 'pointer',
                '$ &:hover': {
                    backgroundColor: '#333',
                },
            },

        },
    });

    GM_addStyle_object('#selectedItemsWrapper', {
        width: '200px',
        top: '8px',
        left: '40px',

        '$ & .historySelectComponent': {
            width: '100%',

            '$ & .selectChoice': {
                width: '100%',
            },
            '$ & .selectList': {
                width: '100%',
            },
        },
    });

    GM_addStyle_object('#filterActionTypeWrapper', {
        width: '100px',
        // top: '8px',
        // left: '40px',

        '$ & .historySelectComponent': {
            width: '100%',

            '$ & .selectChoice': {
                width: '100%',
            },
            '$ & .selectList': {
                width: '100%',
            },
        },
    });

    
    /******************************************************
     * DF Function Overrides
     ******************************************************/

    // Source: market.js
    // Explanation:
    // Allows this script to add a 'history' tab seemlessly into the marketplace
    // This approach should make it still compatible with other userscripts and official site scripts.
    const origLoadMarket = unsafeWindow.loadMarket;
    unsafeWindow.loadMarket = function() {
        console.log('override loadmarket');
        // Execute original function
        origLoadMarket.apply(unsafeWindow, arguments);

        injectHistoryTabIntoMarketplace();
    };

    // Source: base.js
    // Explanation:
    // Allows this script to hook into before and after the callback of webCall.
    // Which prevents us having to do extra requests while still getting the data we need
    // The less requests, the better.
    // Plus DeadFrontier's webCalls are executed at exactly the right moments we need (like after selling)
    // This approach should make it still compatible with other userscripts and official site scripts.
    const originalWebCall = unsafeWindow.webCall;
    unsafeWindow.webCall = function (call, params, callback, hashed) {
        // Override the callback function to execute any hooks
        // This still executes the original callback function, but with our hooks
        const callbackWithHooks = function(data, status, xhr) {
            const dataObj = stringExplode(data)
            const response = stringExplode(xhr.responseText);

            // Call all 'before' hooks
            if (WEBCALL_HOOKS.before.hasOwnProperty(call)) {
                // Copy the array, incase that hooks remove themselves during their execution
                const beforeHooks = WEBCALL_HOOKS.before[call].slice();

                for (const beforeHook of beforeHooks) {
                    beforeHook(
                        {
                            call,
                            params,
                            callback,
                            hashed,
                        },
                        {
                            dataObj,
                            response,
                            data,
                            status,
                            xhr,
                        }
                    );
                }
            }

            // Call all 'beforeAll' hooks
            const beforeAllHooks = WEBCALL_HOOKS.beforeAll.slice();
            for (const beforeAllHook of beforeAllHooks) {
                beforeAllHook(
                    {
                        call,
                        params,
                        callback,
                        hashed,
                    },
                    {
                        dataObj,
                        response,
                        data,
                        status,
                        xhr,
                    }
                );
            }
            
            // Execute the original callback
            const result = callback.call(unsafeWindow, data, status, xhr);

            // Call all 'after' hooks
            if (WEBCALL_HOOKS.after.hasOwnProperty(call)) {

                // Copy the array, incase that hooks remove themselves during their execution
                const afterHooks = WEBCALL_HOOKS.after[call].slice();

                for (const afterHook of afterHooks) {
                    afterHook(
                        {
                            call,
                            params,
                            callback,
                            hashed,
                        },
                        {
                            dataObj,
                            response,
                            data,
                            status,
                            xhr,
                        },
                        result
                    );
                }
            }

            // Call all 'afterAll' hooks
            const afterAllHooks = WEBCALL_HOOKS.afterAll.slice();
            for (const afterAllHook of afterAllHooks) {
                afterAllHook(
                    {
                        call,
                        params,
                        callback,
                        hashed,
                    },
                    {
                        dataObj,
                        response,
                        data,
                        status,
                        xhr,
                    },
                    result
                );
            }

            // Return the original callback result
            // As far as I see in the source code, the callbacks never return anything, but its cleaner to return it anyway
            return result;
        };

        // Call the original webCall function, but with our hooked callback function
        return originalWebCall.call(unsafeWindow, call, params, callbackWithHooks, hashed);
    };

    // Bugfix for DeadFrontier code
    const origAllowedInfoCard = unsafeWindow.allowedInfoCard;
    unsafeWindow.allowedInfoCard = function (elem) {
        if(elem && typeof elem.classList !== "undefined" && (elem.classList.contains("item") || elem.classList.contains("fakeItem") || elem.parentNode?.classList.contains("fakeItem")))
        {
            return true;
        } else
        {
            return false;
        }
    }
    
    // Source: inventory.js
    // Explanation:
    // Allows this script to hook into the infoCard function, which is used to display item info when hovering over an item
    // This approach makes it still compatible with SilverScript's HoverPrices
    var origInfoCard = unsafeWindow.infoCard || null;
    if (origInfoCard) {
        inventoryHolder.removeEventListener("mousemove", origInfoCard, false);
 
        unsafeWindow.infoCard = function (e) {
            // infoBox.style.color = '';
            
            //Remove previous history info
            let elems = document.getElementsByClassName("historyInfoContainer");
            for(var i = elems.length - 1; i >= 0; i--) {
                elems[i].parentNode.removeChild(elems[i]);
            }
            elems = document.getElementsByClassName("historyShiftNotice");
            for(var i = elems.length - 1; i >= 0; i--) {
                elems[i].parentNode.removeChild(elems[i]);
            }

            // Call the original infoCard function
            origInfoCard(e);
            if(active || pageLock || !allowedInfoCard(e.target)) {
                return;
            }
 
            var target;
            if(e.target.parentNode.classList.contains("fakeItem"))
            {
                target = e.target.parentNode;
            } else
            {
                target = e.target;
            }
            
            // if (!wasHidden) {
            //     return;
            // }
            
            // Used in the history tab
            if (target.classList.contains('pending')) {
                const container = document.createElement('div');
                // container.className = 'itemData historyInfoContainer';
                container.classList.add('itemData');
                container.classList.add('historyInfoContainer');
                container.style.color = '#FFCC00';
                container.style.marginTop = 'auto';
                container.innerHTML = 'This sale is still pending';
                infoBox.appendChild(container);
            }

            if (target.classList.contains('item') && SETTINGS.values.hoverEnabled) {
                HOVER_INFOBOX_DATA.event = e;

                if (SETTINGS.values.shiftHoverMode !== 'disabled') {
                    const shiftHoverStyle = document.createElement('style');
                    shiftHoverStyle.classList.add('historyInfoContainer'); // Will be removed when infoCard is called again
                    if (SETTINGS.values.shiftHoverMode == 'history') {
                        const classNameToHide = e.shiftKey ? 'silverStats' : 'historyData';
                        shiftHoverStyle.innerHTML = '.' + classNameToHide + ' { display: none; }';
                    }
                    if (SETTINGS.values.shiftHoverMode == 'silverscripts') {
                        const classNameToHide = e.shiftKey ? 'historyData' : 'silverStats';
                        shiftHoverStyle.innerHTML = '.' + classNameToHide + ' { display: none; }';
                    }
                    infoBox.appendChild(shiftHoverStyle);
                }

                const infoContainer = document.createElement('div');
                const isAmmo = target.dataset.itemtype == 'ammo';
                const item = target.dataset.type;
                const quantity = parseInt(target.dataset.quantity);
                infoContainer.classList.add('historyInfoContainer');
                infoContainer.classList.add('itemData');
                infoContainer.classList.add('historyData');
                let infoText = '';

                const perNamer = function (amount) {
                    let perName = 'unit';
                    if (isAmmo) {
                        perName = 'round';
                    }

                    if (item == 'fuelammo') {
                        return 'mL';
                    }

                    return perName + (amount == 1 ? '' : 's');
                };
                
                if (SETTINGS.values.hoverAvgBuyPriceEnabled) {
                    const avgPriceBought = HISTORY.getItemInfo(target.dataset.type, 'avg_price_bought');
                    infoText += 'Average buy price: ' + formatMoneyHtml(avgPriceBought, true) + '/' + perNamer(1);
                    if (isAmmo) {
                        infoText += ', ' + formatMoneyHtml(avgPriceBought * quantity, true) + '/stack(' + quantity + ')';
                    }
                    infoText += '<br>';
                }
                if (SETTINGS.values.hoverAvgSellPriceEnabled) {
                    const avgPriceSold = HISTORY.getItemInfo(target.dataset.type, 'avg_price_sold');
                    infoText += 'Average sell price: ' + formatMoneyHtml(avgPriceSold, true) + '/' + perNamer(1);
                    if (isAmmo) {
                        infoText += ', ' + formatMoneyHtml(avgPriceSold * quantity, true) + '/stack(' + quantity + ')';
                    }
                    infoText += '<br>';
                }
                if (SETTINGS.values.hoverAmountBoughtEnabled) {
                    const amountBought = HISTORY.getItemInfo(target.dataset.type, 'amount_bought');
                    infoText += 'Amount bought: ' + amountBought + '<br>';
                }
                if (SETTINGS.values.hoverAmountSoldEnabled) {
                    const amountSold = HISTORY.getItemInfo(target.dataset.type, 'amount_sold');
                    infoText += 'Amount sold: ' + amountSold + '<br>';
                }
                if (SETTINGS.values.hoverLastBuyPriceEnabled) {
                    const lastBuyPrice = HISTORY.getItemInfo(target.dataset.type, 'last_price_bought');
                    if (isAmmo) {
                        const lastBuyQuantity = HISTORY.getItemInfo(target.dataset.type, 'last_quantity_bought');
                        const lastBuyPerRound = lastBuyPrice === null ? null : (lastBuyPrice / lastBuyQuantity);
                        const lastBuyPerStack = lastBuyPrice === null ? null : (lastBuyPerRound * quantity);
                        infoText += 'Last bought for: ' + (lastBuyPerRound === null ? 'Never bought' : formatMoneyHtml(lastBuyPerRound, true) + '/round, ' + formatMoneyHtml(lastBuyPerStack, true) + '/stack(' + quantity + ')') + '<br>';
                    } else {
                        infoText += 'Last bought for: ' + (lastBuyPrice === null ? 'Never bought' : formatMoneyHtml(lastBuyPrice, true)) + '<br>';
                    }
                }
                if (SETTINGS.values.hoverLastSellPriceEnabled) {
                    const lastSellPrice = HISTORY.getItemInfo(target.dataset.type, 'last_price_sold');
                    if (isAmmo) {
                        const lastSellQuantity = HISTORY.getItemInfo(target.dataset.type, 'last_quantity_sold');
                        const lastSellPerRound = lastSellPrice === null ? null : (lastSellPrice / lastSellQuantity);
                        const lastSellPerStack = lastSellPrice === null ? null : (lastSellPerRound * quantity);
                        infoText += 'Last sold for: ' + (lastSellPerRound === null ? 'Never sold' : formatMoneyHtml(lastSellPerRound, true) + '/round, ' + formatMoneyHtml(lastSellPerStack, true) + '/stack(' + quantity + ')') + '<br>';
                    } else {
                        infoText += 'Last sold for: ' + (lastSellPrice === null ? 'Never sold' : formatMoneyHtml(lastSellPrice, true)) + '<br>';
                    }
                }
                if (SETTINGS.values.hoverAvgProfitEnabled) {
                    const avgPriceSold = HISTORY.getItemInfo(target.dataset.type, 'avg_price_sold');
                    const avgPriceBought = HISTORY.getItemInfo(target.dataset.type, 'avg_price_bought');
                    const avgProfit = avgPriceSold - avgPriceBought;
                    infoText += 'Average profit/loss: ' + formatMoneyHtml(avgProfit, false) + '/' + perNamer(1);
                    if (isAmmo) {
                        const avgProfitStack = avgProfit * quantity;
                        infoText += ', ' + formatMoneyHtml(avgProfitStack, false) + '/stack(' + quantity + ')';
                    }
                    infoText += '<br>';
                }
                

                if (infoText.trim()) {
                    infoText = '<div style="text-decoration: underline; text-align: center;">History Data</div>' + infoText;
                }

                if (SETTINGS.values.hoverEnabled && SETTINGS.values.shiftHoverMode == 'silverscripts' && !e.shiftKey && silverScriptsInstalled) {
                    infoText += '<div style="text-decoration: underline; font-size: 8pt;">Hold SHIFT to show SilverScript\'s HoverPrices</div>'
                }
                if (SETTINGS.values.hoverEnabled && SETTINGS.values.shiftHoverMode == 'history' && !e.shiftKey) {
                    const historyShiftNotice = document.createElement('div');
                    historyShiftNotice.classList.add('historyShiftNotice');
                    historyShiftNotice.innerHTML = '<div style="text-decoration: underline; font-size: 8pt;">Hold SHIFT to show History Data</div>'

                    infoBox.appendChild(historyShiftNotice);
                }

                infoContainer.innerHTML = infoText;
                infoBox.appendChild(infoContainer);
            }

        }.bind(unsafeWindow);
 
        inventoryHolder.addEventListener("mousemove", unsafeWindow.infoCard, false);
    }

    // Source: market.js
    var origSellMenuItemPopulate = unsafeWindow.SellMenuItemPopulate;
    unsafeWindow.SellMenuItemPopulate = function (itemElem) {
        // Call original function
        origSellMenuItemPopulate(itemElem);
    };

    /******************************************************
     * Webcall hooks
     ******************************************************/

    // Hook into when an item is sold
    onBeforeWebCall('inventory_new', function (request, response) {
        if (request.params.action !== 'newsell') {
            return;
        }
        if (response.xhr.status != 200) {
            return;
        }
        // if (!response.dataObj.hasOwnProperty('OK') && response.dataObj.done != '1') {
        //     return;
        // }
        
        // When the sell is successful, DeadFrontier will do a new webCall to retrieve the new sell listing
        // We hook ONCE into this webCall, to retrieve the trade id
        const onSellSuccess = function (request, response) {
            if (response.xhr.status == 200) {
                HISTORY.onSellItem(request, response);
            }
            
            // Remove self from hook
            offAfterWebCall('trade_search', onSellSuccess);
        };
        
        // Hook into the new sell listing webCall
        onAfterWebCall('trade_search', onSellSuccess);
    });

    // Hook into when credits are sold
    onBeforeWebCall('inventory_new', function (request, response) {
        if (request.params.action !== 'newsellcredits') {
            return;
        }
        if (response.xhr.status != 200) {
            return;
        }
        // if (!response.dataObj.hasOwnProperty('OK') && response.dataObj.done != '1') {
        //     return;
        // }

        // When the sell is successful, DeadFrontier will do a new webCall to retrieve the new sell listing
        // We hook ONCE into this webCall, to retrieve the trade id
        const onSellSuccess = function (request, response) {
            if (response.xhr.status == 200) {
                HISTORY.onSellItem(request, response);
            }

            // Remove self from hook
            offAfterWebCall('trade_search', onSellSuccess);
        };

        // Hook into the new sell listing webCall
        onAfterWebCall('trade_search', onSellSuccess);
    });

    // Hook into when an item is bought
    onAfterWebCall('inventory_new', function (request, response) {
        if (request.params.action !== 'newbuy') {
            return;
        }
        if (response.xhr.status != 200) {
            return;
        }
        if (!response.dataObj.hasOwnProperty('OK')) {
            return;
        }

        const dataObj = {};

        for(const key in response.dataObj) {
            if (key.indexOf('df_inv') !== 0) {
                continue;
            }

            dataObj[key.replace(/^df_inv\d+_/, '')] = response.dataObj[key];
        }
        
        const entry = {
            trade_id: request.params.buynum,
            action: 'buy',
            price: request.params.expected_itemprice,
            item: dataObj.type,
            itemname: unsafeWindow.itemNamer(dataObj.type, dataObj.quantity),
            quantity: dataObj.quantity, 
        };

        HISTORY.pushTrade(entry);
    });

    // Hook into when an item is scrapped
    onBeforeWebCall('inventory_new', function (request, response) {
        if (request.params.action !== 'scrap') {
            return;
        }
        if (response.xhr.status != 200) {
            return;
        }
        if (!response.dataObj.hasOwnProperty('OK')) {
            return;
        }

        const itemnum = request.params.itemnum;
        const quantity = unsafeWindow.userVars['DFSTATS_df_inv' + itemnum + '_quantity'];
        const itemTypeId = unsafeWindow.userVars['DFSTATS_df_inv' + itemnum + '_type'];

        if (!itemTypeId) {
            const logData = {
                price: request.params.price,
                item: request.params.expected_itemtype,
                itemnum,
                itemTypeId,
                quantity,
            };
            alert('Error: Could not find item type id for scrapped item\n\nContact Runonstof with this data: ' + JSON.stringify(logData));
            alert('You can also check the console (F12) for the data to share with Runonstof');
            console.info(JSON.stringify(logData, null, 2));
            console.info('Only share above data with Runonstof');
            return;
        }

        const entry = {
            trade_id: hash(objectJoin(request.params)),
            action: 'scrap',
            price: request.params.price,
            item: request.params.expected_itemtype,
            itemname: unsafeWindow.itemNamer(itemTypeId, quantity),
            quantity,
        };

        HISTORY.pushTrade(entry);
    });

    // Hook into when a sale is canceled
    onAfterWebCall('inventory_new', function (request, response) {
        if (request.params.action !== 'newcancelsale') {
            return;
        }
        if (response.xhr.status != 200) {
            return;
        }
        const tradeId = request.params.buynum;
        HISTORY.removeTrade(tradeId);
    });

    // Update 'pending sales' trade cache
    onAfterWebCall('trade_search', function (request, response) {
        if (response.xhr.status != 200) {
            return;
        }
        const tradeCount = response.dataObj.tradelist_totalsales;
        if (tradeCount == 0) {
            return;
        }

        const pendingTradeIds = [];
        for(let i = 0; i < tradeCount; i++) {
            const tradeId = response.dataObj['tradelist_' + i + '_trade_id'];
            pendingTradeIds.push(tradeId);
        }

        HISTORY.cache.pending_trade_ids = pendingTradeIds;
    });
    
    
    /******************************************************
     * Await Page Initialization
     ******************************************************/
    console.log('awaiting page initialization');
    // A promise that resolves when document is fully loaded and globalData is filled with stackables
    // This is because DeadFrontier does a request to stackables.json, which is needed for the max stack of items
    // Only after this request is done, globalData will contain ammo with a max_quantity
    await new Promise(resolve => {
        if (unsafeWindow.globalData.hasOwnProperty('32ammo')) {
            resolve();
            return;
        }

        // This is the original function that is called when the stackables.json request is done
        const origUpdateIntoArr = unsafeWindow.updateIntoArr;
        unsafeWindow.updateIntoArr = function (flshArr, baseArr) {
            // Execute original function
            origUpdateIntoArr.apply(unsafeWindow, [flshArr, baseArr]);

            // Check if globalData is filled with stackables
            if (unsafeWindow.globalData != baseArr) {
                return;
            }

            // revert override, we dont need it anymore
            unsafeWindow.updateIntoArr = origUpdateIntoArr;
            resolve();
        }
    });

    /******************************************************
     * Script Initialization
     ******************************************************/

    SEARCHABLE_ITEMS = Object.keys(unsafeWindow.globalData)
        .filter(itemId => !['brokenitem', 'undefined'].includes(itemId) && unsafeWindow.globalData[itemId].no_transfer != '1');

    SEARCHABLE_ITEMS.forEach(itemId => {
            const item = unsafeWindow.globalData[itemId];
            if (!item.needcook || item.needcook != '1') {
                return;
            }

            SEARCHABLE_ITEMS.push(itemId + '_cooked');
        });

    unsafeWindow.SEARCHABLE_ITEMS = SEARCHABLE_ITEMS;
    
    // Load History
    console.log('awaiting history initialization');
    await HISTORY.init();
    HISTORY.resetCache();
    // HISTORY.initCache();

    // Load settings
    console.log('awaiting settings initialization');
    await SETTINGS.load();

    //Populate LOOKUP
    for (const itemId in unsafeWindow.globalData) {
        const item = unsafeWindow.globalData[itemId];
        const categoryId = item.itemcat;

        if (!LOOKUP.category__item_id.hasOwnProperty(categoryId)) {
            LOOKUP.category__item_id[categoryId] = [];
        }
    }

    for (const categoryId in LOOKUP.category__item_id) {
        LOOKUP.category__item_id[categoryId].sort((a, b) => {
            const itemA = unsafeWindow.globalData[a];
            const itemB = unsafeWindow.globalData[b];

            const nameA = itemA.name?.toLowerCase() || '';
            const nameB = itemB.name?.toLowerCase() || '';

            return nameA.localeCompare(nameB);
        });
    }

    delete LOOKUP.category__item_id['broken'];

    // DEBUG
    unsafeWindow.LOOKUP = LOOKUP;
    unsafeWindow.SETTINGS = SETTINGS;
    unsafeWindow.HISTORY = HISTORY;


    var historySettingsButton = document.createElement("button");
    historySettingsButton.classList.add("opElem");
    historySettingsButton.style.left = page == 35 ? "200px" : "400px";
    historySettingsButton.style.bottom = "86px";
    historySettingsButton.textContent = "History Menu";
    inventoryHolder.appendChild(historySettingsButton);

    
    historySettingsButton.addEventListener("click", function () {
        const fn = SETTINGS.renderSettingsPrompt.bind(SETTINGS);

        fn();
    });

    console.log('awaiting ready');
    await ready();

    document.getElementById("invController").removeEventListener("contextmenu", unsafeWindow.openSellContextMenu, false);
    document.getElementById("invController").addEventListener("contextmenu", unsafeWindow.openSellContextMenu, false);

    const onShiftRelease = function (event) {
        if (event.key != 'Shift') {
            return;
        }

        // console.log('shift hover mode: ' + SETTINGS.values.shiftHoverMode);
        HOVER_INFOBOX_DATA.run(false);

        unsafeWindow.document.removeEventListener('keyup', onShiftRelease);
    };

    unsafeWindow.document.addEventListener('keydown', function (event) {
        if (event.key != 'Shift') {
            return;
        }

        // console.log('shift hover mode: ' + SETTINGS.values.shiftHoverMode);
        HOVER_INFOBOX_DATA.run(true);
        unsafeWindow.document.addEventListener('keyup', onShiftRelease);
    });


    // Create button if script loaded too early
    if (page == 35) {
        injectHistoryTabIntoMarketplace();
    }
})();