The West Rankings CSV Exporter

Extract player rankings data (name, level, experience) and export to CSV, JSON

// ==UserScript==
// @name         The West Rankings CSV Exporter
// @namespace    TW-Export-Player-CSV
// @version      2.0
// @description  Extract player rankings data (name, level, experience) and export to CSV, JSON
// @author       Frozah
// @include https://*.the-west.*/game.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// ==/UserScript==

(function (fn) {
    var script = document.createElement('script');
    script.setAttribute('type', 'application/javascript');
    script.textContent = '(' + fn + ')();';
    document.body.appendChild(script);
    document.body.removeChild(script);
})(function () {

    RankingExporter = {
        version: '2.0',
        name: 'Rankings CSV Exporter',
        author: 'Frozah',

        // Configuration with persistent storage
        config: {
            includeAllPages: true,
            maxRetries: 3,
            baseDelay: 1000,
            adaptiveDelay: true,
            exportFormat: 'csv', // csv, json, excel
            columns: ['rank', 'name', 'level', 'experience'],
            filters: {
                minLevel: null,
                maxLevel: null,
                minRank: null,
                maxRank: null
            },
            autoSave: true,
            logLevel: 'info' // debug, info, warn, error
        },

        // Runtime data
        playersData: [],
        currentPage: 1,
        totalPages: 16,
        isScrapingInProgress: false,
        progressBar: null,
        progressDialog: null,
        selectBox: null,
        retryCount: 0,
        startTime: null,

        // Performance tracking
        performance: {
            averagePageTime: 1000,
            pageLoadTimes: [],
            errors: []
        },

        // Initialize the script
        init: function() {
            this.loadConfig();
            this.validateGameStructure();
            this.createAdvancedSelectBox();
            this.addMenuButton();
            this.log('Script initialized successfully', 'info');
        },

        // Load persistent configuration
        loadConfig: function() {
            try {
                if (typeof GM_getValue !== 'undefined') {
                    var savedConfig = GM_getValue('rankingExporterConfig', null);
                    if (savedConfig) {
                        this.config = Object.assign(this.config, JSON.parse(savedConfig));
                    }
                }
            } catch (e) {
                this.log('Error loading config: ' + e.message, 'warn');
            }
        },

        // Save configuration
        saveConfig: function() {
            try {
                if (typeof GM_setValue !== 'undefined') {
                    GM_setValue('rankingExporterConfig', JSON.stringify(this.config));
                }
            } catch (e) {
                this.log('Error saving config: ' + e.message, 'warn');
            }
        },

        // Validate game structure before operation
        validateGameStructure: function() {
            var requiredElements = [
                '.ranking-experience',
                '.rl_pagebar_ranking',
                '.exp_playername',
                '.exp_level',
                '.exp_exp',
                '.exp_rank'
            ];

            var missingElements = [];
            requiredElements.forEach(function(selector) {
                if ($(selector).length === 0) {
                    missingElements.push(selector);
                }
            });

            if (missingElements.length > 0) {
                this.log('Warning: Some game elements not found: ' + missingElements.join(', '), 'warn');
                new UserMessage('Game structure may have changed. Some features might not work correctly.', UserMessage.TYPE_ERROR).show();
                return false;
            }
            return true;
        },

        // Enhanced logging system
        log: function(message, level) {
            level = level || 'info';
            var levels = ['debug', 'info', 'warn', 'error'];
            var currentLevelIndex = levels.indexOf(this.config.logLevel);
            var messageLevelIndex = levels.indexOf(level);

            if (messageLevelIndex >= currentLevelIndex) {
                var timestamp = new Date().toISOString();
                var logMessage = '[' + timestamp + '] [' + level.toUpperCase() + '] RankingExporter: ' + message;

                switch(level) {
                    case 'error':
                        console.error(logMessage);
                        break;
                    case 'warn':
                        console.warn(logMessage);
                        break;
                    default:
                        console.log(logMessage);
                }
            }
        },

        // Create advanced select box with more options
        createAdvancedSelectBox: function() {
            var self = this;
            var listener = function(action) {
                switch(action) {
                    case 'export_current':
                        self.exportCurrentPage();
                        break;
                    case 'export_all':
                        self.exportAllPages();
                        break;
                    case 'export_filtered':
                        self.showFilterDialog();
                        break;
                    case 'export_range':
                        self.showRangeDialog();
                        break;
                    case 'stop_scraping':
                        self.stopScraping();
                        break;
                    case 'settings':
                        self.showSettingsDialog();
                        break;
                    case 'resume_export':
                        self.resumeExport();
                        break;
                }
            };

            this.selectBox = new west.gui.Selectbox()
                .setWidth(250)
                .addListener(listener)
                .addItem('export_current', 'Export Current Page')
                .addItem('export_all', 'Export All Pages')
                .addItem('export_filtered', 'Export with Filters')
                .addItem('export_range', 'Export Range')
                .addItem('stop_scraping', 'Stop Scraping')
                .addItem('settings', 'Settings')
                .addItem('resume_export', 'Resume Export');
        },

        // Add enhanced menu button
        addMenuButton: function() {
            var self = this;

            // Remove existing button if any
            $('#RankingExportermenu').parent().remove();

            var menuButton = $('<div id="RankingExportermenu" class="menulink" title="' + this.name + ' v' + this.version + '" />')
                .css({
                    'background': 'linear-gradient(45deg, #8B4513, #A0522D)',
                    'width': '25px',
                    'height': '25px',
                    'cursor': 'pointer',
                    'border': '2px solid #654321',
                    'border-radius': '3px',
                    'margin': '2px',
                    'position': 'relative',
                    'box-shadow': '0 2px 4px rgba(0,0,0,0.3)',
                    'transition': 'all 0.2s ease'
                })
                .on('mouseenter', function() {
                    $(this).css({
                        'background': 'linear-gradient(45deg, #A0522D, #CD853F)',
                        'transform': 'scale(1.1)'
                    });
                })
                .on('mouseleave', function() {
                    $(this).css({
                        'background': 'linear-gradient(45deg, #8B4513, #A0522D)',
                        'transform': 'scale(1)'
                    });
                })
                .click(function() {
                    self.toggleSelectbox();
                });

            // Add CSV icon
            $('<div>').css({
                'position': 'absolute',
                'top': '50%',
                'left': '50%',
                'transform': 'translate(-50%, -50%)',
                'color': 'white',
                'font-weight': 'bold',
                'font-size': '10px',
                'text-shadow': '1px 1px 1px rgba(0,0,0,0.7)'
            }).text('CSV').appendTo(menuButton);

            var div = $('<div class="ui_menucontainer" />')
                .append(menuButton)
                .append('<div class="menucontainer_bottom" />');

            // Try to add to menu bar, with fallback
            var menuBar = $('#ui_menubar');
            if (menuBar.length > 0) {
                menuBar.append(div);
                this.log('Menu button added successfully', 'debug');
            } else {
                // Fallback: try to find any menu container
                var menuContainer = $('.ui_menucontainer').first().parent();
                if (menuContainer.length > 0) {
                    menuContainer.append(div);
                    this.log('Menu button added to fallback location', 'debug');
                } else {
                    this.log('Could not find menu location', 'error');
                    // Create floating button as last resort
                    this.createFloatingButton();
                }
            }
        },

        // Create floating button as fallback
        createFloatingButton: function() {
            var self = this;

            var floatingButton = $('<div id="RankingExporterFloat" />')
                .css({
                    'position': 'fixed',
                    'top': '100px',
                    'right': '20px',
                    'width': '50px',
                    'height': '50px',
                    'background': 'linear-gradient(45deg, #8B4513, #A0522D)',
                    'border': '3px solid #654321',
                    'border-radius': '50%',
                    'cursor': 'pointer',
                    'z-index': '9999',
                    'box-shadow': '0 4px 8px rgba(0,0,0,0.4)',
                    'display': 'flex',
                    'align-items': 'center',
                    'justify-content': 'center',
                    'color': 'white',
                    'font-weight': 'bold',
                    'font-size': '12px',
                    'text-shadow': '1px 1px 1px rgba(0,0,0,0.7)',
                    'transition': 'all 0.2s ease'
                })
                .text('CSV')
                .on('mouseenter', function() {
                    $(this).css({
                        'background': 'linear-gradient(45deg, #A0522D, #CD853F)',
                        'transform': 'scale(1.1)'
                    });
                })
                .on('mouseleave', function() {
                    $(this).css({
                        'background': 'linear-gradient(45deg, #8B4513, #A0522D)',
                        'transform': 'scale(1)'
                    });
                })
                .click(function() {
                    self.toggleSelectbox();
                })
                .attr('title', this.name + ' v' + this.version);

            $('body').append(floatingButton);
            this.log('Floating button created as fallback', 'info');
        },

        // Show settings dialog
        showSettingsDialog: function() {
            var self = this;
            var content = $('<div></div>');

            // Export format selection
            content.append('<h3>Export Settings</h3>');
            var formatSelect = $('<select id="exportFormat">')
                .append('<option value="csv">CSV</option>')
                .append('<option value="json">JSON</option>')
                .val(this.config.exportFormat);
            content.append($('<p>Format: </p>').append(formatSelect));

            // Performance settings
            content.append('<h3>Performance Settings</h3>');
            var adaptiveDelay = $('<input type="checkbox" id="adaptiveDelay">')
                .prop('checked', this.config.adaptiveDelay);
            content.append($('<p>Adaptive delay: </p>').append(adaptiveDelay));

            var baseDelay = $('<input type="number" id="baseDelay" min="500" max="5000" step="100">')
                .val(this.config.baseDelay);
            content.append($('<p>Base delay (ms): </p>').append(baseDelay));

            // Auto-save settings
            var autoSave = $('<input type="checkbox" id="autoSave">')
                .prop('checked', this.config.autoSave);
            content.append($('<p>Auto-save progress: </p>').append(autoSave));

            var dialog = new west.gui.Dialog("Settings", content);
            dialog.addButton("Save", function() {
                self.config.exportFormat = formatSelect.val();
                self.config.adaptiveDelay = adaptiveDelay.is(':checked');
                self.config.baseDelay = parseInt(baseDelay.val());
                self.config.autoSave = autoSave.is(':checked');
                self.saveConfig();
                dialog.hide();
                new UserMessage('Settings saved!', UserMessage.TYPE_SUCCESS).show();
            });
            dialog.addButton("Cancel", function() {
                dialog.hide();
            });
            dialog.show();
        },

        // Show filter dialog
        showFilterDialog: function() {
            var self = this;
            var content = $('<div></div>');

            content.append('<h3>Filter Options</h3>');

            var minLevel = $('<input type="number" id="minLevel" placeholder="Min Level">');
            var maxLevel = $('<input type="number" id="maxLevel" placeholder="Max Level">');
            var minRank = $('<input type="number" id="minRank" placeholder="Min Rank">');
            var maxRank = $('<input type="number" id="maxRank" placeholder="Max Rank">');

            content.append($('<p>Level range: </p>').append(minLevel).append(' - ').append(maxLevel));
            content.append($('<p>Rank range: </p>').append(minRank).append(' - ').append(maxRank));

            var dialog = new west.gui.Dialog("Export with Filters", content);
            dialog.addButton("Export", function() {
                self.config.filters.minLevel = minLevel.val() || null;
                self.config.filters.maxLevel = maxLevel.val() || null;
                self.config.filters.minRank = minRank.val() || null;
                self.config.filters.maxRank = maxRank.val() || null;
                dialog.hide();
                self.exportAllPages(true);
            });
            dialog.addButton("Cancel", function() {
                dialog.hide();
            });
            dialog.show();
        },

        // Enhanced data extraction with error handling
        extractCurrentPageData: function() {
            var self = this;
            var data = [];
            var startTime = Date.now();

            try {
                // Check if we're actually on the rankings page
                if (!this.isOnRankingsPage()) {
                    new UserMessage('Please navigate to the rankings page first', UserMessage.TYPE_ERROR).show();
                    return [];
                }

                var rows = $('.ranking-experience .tbody .tw2gui_scrollpane_clipper_contentpane .row');
                this.log('Found ' + rows.length + ' rows on current page', 'debug');

                rows.each(function(index) {
                    try {
                        var row = $(this);
                        var playerData = self.extractPlayerData(row);

                        if (playerData && self.applyFilters(playerData)) {
                            data.push(playerData);
                        }
                    } catch (e) {
                        self.log('Error extracting data from row ' + index + ': ' + e.message, 'warn');
                    }
                });

                var extractionTime = Date.now() - startTime;
                this.log('Extracted ' + data.length + ' players in ' + extractionTime + 'ms', 'debug');

            } catch (e) {
                this.log('Error in extractCurrentPageData: ' + e.message, 'error');
                this.performance.errors.push({
                    timestamp: Date.now(),
                    error: e.message,
                    page: this.currentPage
                });
            }

            return data;
        },

        // Extract individual player data
        extractPlayerData: function(row) {
            var playerNameCell = row.find('.exp_playername a');
            var playerName = '';

            if (playerNameCell.length > 0) {
                playerName = playerNameCell.text().trim();
            } else {
                playerName = row.find('.exp_playername').text().trim();
            }

            var level = row.find('.exp_level').text().trim();
            var experience = row.find('.exp_exp').text().trim();
            var rank = row.find('.exp_rank').text().trim();

            if (playerName && level && experience && rank) {
                return {
                    rank: parseInt(rank) || rank,
                    name: playerName,
                    level: parseInt(level) || level,
                    experience: experience,
                    rawExperience: this.parseExperience(experience)
                };
            }
            return null;
        },

        // Parse experience string to number
        parseExperience: function(expString) {
            if (!expString) return 0;
            // Remove dots and commas for international number formats
            return parseInt(expString.replace(/[.,]/g, '')) || 0;
        },

        // Apply filters to player data
        applyFilters: function(playerData) {
            var filters = this.config.filters;

            if (filters.minLevel && playerData.level < filters.minLevel) return false;
            if (filters.maxLevel && playerData.level > filters.maxLevel) return false;
            if (filters.minRank && playerData.rank < filters.minRank) return false;
            if (filters.maxRank && playerData.rank > filters.maxRank) return false;

            return true;
        },

        // Enhanced export all pages with better error handling
        exportAllPages: function(useFilters) {
            if (this.isScrapingInProgress) {
                new UserMessage('Scraping already in progress...', UserMessage.TYPE_ERROR).show();
                return;
            }

            // Check if we're on the rankings page
            if (!this.isOnRankingsPage()) {
                new UserMessage('Please navigate to the rankings page first', UserMessage.TYPE_ERROR).show();
                return;
            }

            if (!this.validateGameStructure()) {
                return;
            }

            this.isScrapingInProgress = true;
            this.playersData = [];
            this.currentPage = 1;
            this.retryCount = 0;
            this.startTime = Date.now();
            this.performance.pageLoadTimes = [];
            this.performance.errors = [];

            this.getTotalPages();

            var filterText = useFilters ? ' with filters' : '';
            new UserMessage('Starting scraping of ' + this.totalPages + ' pages' + filterText + '...', UserMessage.TYPE_HINT).show();
            this.log('Starting export of ' + this.totalPages + ' pages', 'info');

            this.createEnhancedProgressDialog();
            this.scrapePage(1);
        },

        // Enhanced progress dialog
        createEnhancedProgressDialog: function() {
            var self = this;
            var content = $('<div></div>');

            content.append('<p id="scrapingStatus">Scraping in progress...</p>');
            content.append('<p id="timeInfo">Estimated time remaining: Calculating...</p>');

            this.progressBar = new west.gui.Progressbar(0, this.totalPages);
            content.append(this.progressBar.getMainDiv());

            content.append('<div id="statsInfo" style="font-size: 12px; margin-top: 10px;"></div>');

            this.progressDialog = new west.gui.Dialog("Enhanced Export Progress", content);
            this.progressDialog.addButton("Pause", function() {
                self.pauseScraping();
            });
            this.progressDialog.addButton("Cancel", function() {
                self.stopScraping();
                self.progressDialog.hide();
            });
            this.progressDialog.show();
        },

        // Enhanced page scraping with retry logic
        scrapePage: function(pageNumber) {
            var self = this;
            var pageStartTime = Date.now();

            if (!this.isScrapingInProgress || pageNumber > this.totalPages) {
                this.finalizeScraping();
                return;
            }

            this.updateProgress(pageNumber, pageStartTime);

            this.log('Scraping page ' + pageNumber + '/' + this.totalPages, 'debug');

            this.goToPageWithRetry(pageNumber, function(success) {
                if (!success) {
                    self.log('Failed to navigate to page ' + pageNumber + ' after retries', 'error');
                    if (self.retryCount < self.config.maxRetries) {
                        self.retryCount++;
                        setTimeout(function() {
                            self.scrapePage(pageNumber);
                        }, self.getAdaptiveDelay() * 2);
                        return;
                    } else {
                        new UserMessage('Too many failures, stopping export', UserMessage.TYPE_ERROR).show();
                        self.stopScraping();
                        return;
                    }
                }

                self.retryCount = 0;
                var delay = self.getAdaptiveDelay();

                setTimeout(function() {
                    self.waitForPageLoad(function() {
                        var pageData = self.extractCurrentPageData();
                        self.playersData = self.playersData.concat(pageData);

                        var pageTime = Date.now() - pageStartTime;
                        self.performance.pageLoadTimes.push(pageTime);
                        self.updatePerformanceStats();

                        self.log('Page ' + pageNumber + ' completed: ' + pageData.length + ' players in ' + pageTime + 'ms', 'info');

                        // Auto-save progress
                        if (self.config.autoSave && pageNumber % 5 === 0) {
                            self.saveProgress();
                        }

                        setTimeout(function() {
                            self.scrapePage(pageNumber + 1);
                        }, delay);
                    });
                }, Math.min(delay, 1000));
            });
        },

        // Navigate to page with retry logic
        goToPageWithRetry: function(pageNumber, callback, attempt) {
            attempt = attempt || 1;
            var self = this;

            if (attempt > this.config.maxRetries) {
                callback(false);
                return;
            }

            this.log('Navigation attempt ' + attempt + ' to page ' + pageNumber, 'debug');

            this.goToPage(pageNumber, function(success) {
                if (success) {
                    callback(true);
                } else {
                    self.log('Navigation attempt ' + attempt + ' failed, retrying...', 'warn');
                    setTimeout(function() {
                        self.goToPageWithRetry(pageNumber, callback, attempt + 1);
                    }, self.getAdaptiveDelay());
                }
            });
        },

        // Enhanced navigation with success callback
        goToPage: function(pageNumber, callback) {
            var self = this;
            var success = false;

            try {
                // Method 1: Direct input field
                var pageInput = $('.rl_pagebar_ranking .tw2gui_textfield input[type="text"]');
                if (pageInput.length > 0) {
                    pageInput.val(pageNumber);
                    pageInput.trigger('input').trigger('change');

                    var enterEvent = jQuery.Event('keypress');
                    enterEvent.which = 13;
                    enterEvent.keyCode = 13;
                    pageInput.trigger(enterEvent);

                    success = true;
                    setTimeout(function() {
                        callback(success);
                    }, 800);
                    return;
                }

                // Method 2: Ajax call
                if (typeof Ajax !== 'undefined' && Ajax.remoteCall) {
                    Ajax.remoteCall("ranking", "get_ranking_page", {
                        page: pageNumber,
                        type: "experience"
                    }, function(response) {
                        success = true;
                        setTimeout(function() {
                            callback(success);
                        }, 500);
                    }, function(error) {
                        self.log('Ajax call failed: ' + error, 'warn');
                        callback(false);
                    });
                    return;
                }

                // Method 3: Click pagination link
                var pageLink = $('.rl_pagebar_ranking .pagebar_page').filter(function() {
                    return $(this).text() == pageNumber;
                });

                if (pageLink.length > 0) {
                    pageLink.click();
                    success = true;
                    setTimeout(function() {
                        callback(success);
                    }, 800);
                    return;
                }

                callback(false);

            } catch (e) {
                this.log('Error in goToPage: ' + e.message, 'error');
                callback(false);
            }
        },

        // Get adaptive delay based on performance
        getAdaptiveDelay: function() {
            if (!this.config.adaptiveDelay) {
                return this.config.baseDelay;
            }

            var avgTime = this.performance.averagePageTime;
            var errorRate = this.performance.errors.length / Math.max(this.performance.pageLoadTimes.length, 1);

            // Increase delay if there are errors or slow performance
            var adaptiveMultiplier = 1 + (errorRate * 2) + (avgTime > 2000 ? 0.5 : 0);

            return Math.min(this.config.baseDelay * adaptiveMultiplier, 5000);
        },

        // Update performance statistics
        updatePerformanceStats: function() {
            if (this.performance.pageLoadTimes.length > 0) {
                var sum = this.performance.pageLoadTimes.reduce(function(a, b) { return a + b; }, 0);
                this.performance.averagePageTime = sum / this.performance.pageLoadTimes.length;
            }
        },

        // Update progress dialog
        updateProgress: function(pageNumber, pageStartTime) {
            if (this.progressBar) {
                this.progressBar.setValue(pageNumber - 1);
            }

            if (this.progressDialog) {
                var elapsed = Date.now() - this.startTime;
                var avgPageTime = this.performance.averagePageTime;
                var remainingPages = this.totalPages - pageNumber + 1;
                var estimatedRemaining = (remainingPages * avgPageTime) / 1000;

                $('#scrapingStatus').text('Scraping page ' + pageNumber + '/' + this.totalPages + '...');
                $('#timeInfo').text('Estimated time remaining: ' + Math.round(estimatedRemaining) + 's');
                $('#statsInfo').html(
                    'Players collected: ' + this.playersData.length + '<br>' +
                    'Average page time: ' + Math.round(avgPageTime) + 'ms<br>' +
                    'Errors: ' + this.performance.errors.length
                );
            }
        },

        // Save progress to localStorage
        saveProgress: function() {
            try {
                var progressData = {
                    playersData: this.playersData,
                    currentPage: this.currentPage,
                    totalPages: this.totalPages,
                    timestamp: Date.now()
                };

                if (typeof GM_setValue !== 'undefined') {
                    GM_setValue('exportProgress', JSON.stringify(progressData));
                }
                this.log('Progress saved at page ' + this.currentPage, 'debug');
            } catch (e) {
                this.log('Error saving progress: ' + e.message, 'warn');
            }
        },

        // Resume export from saved progress
        resumeExport: function() {
            try {
                if (typeof GM_getValue !== 'undefined') {
                    var progressData = GM_getValue('exportProgress', null);
                    if (progressData) {
                        progressData = JSON.parse(progressData);

                        // Check if progress is recent (within 24 hours)
                        if (Date.now() - progressData.timestamp < 24 * 60 * 60 * 1000) {
                            this.playersData = progressData.playersData;
                            this.currentPage = progressData.currentPage;
                            this.totalPages = progressData.totalPages;

                            new UserMessage('Resuming from page ' + this.currentPage + ' (' + this.playersData.length + ' players already collected)', UserMessage.TYPE_SUCCESS).show();

                            this.isScrapingInProgress = true;
                            this.createEnhancedProgressDialog();
                            this.scrapePage(this.currentPage);
                            return;
                        }
                    }
                }

                new UserMessage('No recent progress found to resume', UserMessage.TYPE_ERROR).show();
            } catch (e) {
                this.log('Error resuming export: ' + e.message, 'error');
                new UserMessage('Error resuming export', UserMessage.TYPE_ERROR).show();
            }
        },

        // Enhanced CSV generation with multiple formats
        generateExport: function(data, filename) {
            if (data.length === 0) {
                new UserMessage('No data to export', UserMessage.TYPE_ERROR).show();
                return;
            }

            switch (this.config.exportFormat) {
                case 'json':
                    this.generateJSON(data, filename.replace('.csv', '.json'));
                    break;
                case 'csv':
                default:
                    this.generateCSV(data, filename);
                    break;
            }
        },

        // Generate JSON export
        generateJSON: function(data, filename) {
            var exportData = {
                metadata: {
                    exportDate: new Date().toISOString(),
                    totalPlayers: data.length,
                    version: this.version,
                    filters: this.config.filters
                },
                players: data
            };

            var jsonContent = JSON.stringify(exportData, null, 2);
            this.downloadFile(jsonContent, filename, 'application/json');
        },

        // Enhanced CSV generation
        generateCSV: function(data, filename) {
            var csv = 'Rank,Player Name,Level,Experience,Raw Experience\n';

            data.forEach(function(player) {
                var name = '"' + player.name.replace(/"/g, '""') + '"';
                var experience = '"' + player.experience.replace(/"/g, '""') + '"';

                csv += player.rank + ',' + name + ',' + player.level + ',' + experience + ',' + player.rawExperience + '\n';
            });

            this.downloadFile(csv, filename, 'text/csv');
        },

        // Enhanced file download
        downloadFile: function(content, filename, mimeType) {
            try {
                var blob = new Blob([content], { type: mimeType + ';charset=utf-8;' });
                var link = document.createElement('a');

                if (link.download !== undefined) {
                    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);
                    URL.revokeObjectURL(url);

                    this.log('File downloaded: ' + filename, 'info');
                }
            } catch (e) {
                this.log('Error downloading file: ' + e.message, 'error');
                new UserMessage('Error downloading file', UserMessage.TYPE_ERROR).show();
            }
        },

        // Enhanced finalization with statistics
        finalizeScraping: function() {
            this.isScrapingInProgress = false;

            if (this.progressDialog) {
                this.progressDialog.hide();
            }

            if (this.playersData.length === 0) {
                new UserMessage('No data collected', UserMessage.TYPE_ERROR).show();
                return;
            }

            // Clean up progress save
            if (typeof GM_deleteValue !== 'undefined') {
                GM_deleteValue('exportProgress');
            }

            // Sort and deduplicate
            this.playersData.sort(function(a, b) {
                return parseInt(a.rank) - parseInt(b.rank);
            });

            var uniqueData = this.removeDuplicates(this.playersData);
            var totalTime = Date.now() - this.startTime;

            this.generateExport(uniqueData, 'rankings_enhanced_export.' + this.config.exportFormat);

            var stats = 'Export completed!\n' +
                       'Players: ' + uniqueData.length + '\n' +
                       'Time: ' + Math.round(totalTime / 1000) + 's\n' +
                       'Avg page time: ' + Math.round(this.performance.averagePageTime) + 'ms\n' +
                       'Errors: ' + this.performance.errors.length;

            new UserMessage(stats, UserMessage.TYPE_SUCCESS).show();
            this.log('Export finalized: ' + uniqueData.length + ' players in ' + totalTime + 'ms', 'info');
        },

        // Remove duplicates more efficiently
        removeDuplicates: function(data) {
            var seen = {};
            return data.filter(function(player) {
                var key = player.rank + '_' + player.name;
                if (seen[key]) {
                    return false;
                }
                seen[key] = true;
                return true;
            });
        },

        // Pause scraping
        pauseScraping: function() {
            this.isScrapingInProgress = false;
            this.saveProgress();
            new UserMessage('Scraping paused. Use "Resume Export" to continue.', UserMessage.TYPE_HINT).show();
            if (this.progressDialog) {
                this.progressDialog.hide();
            }
        },

        // Show/hide select box
        toggleSelectbox: function() {
            var pos = $('div#RankingExportermenu').offset();
            pos = {
                clientX: pos.left,
                clientY: pos.top
            };
            this.selectBox.show(pos);
        },

        // Export current page with enhanced features
        exportCurrentPage: function() {
            var data = this.extractCurrentPageData();

            if (data.length === 0) {
                new UserMessage('No data found on this page', UserMessage.TYPE_ERROR).show();
                return;
            }

            var filename = 'rankings_current_page_' + Date.now() + '.' + this.config.exportFormat;
            this.generateExport(data, filename);
            new UserMessage('Current page exported (' + data.length + ' players)', UserMessage.TYPE_SUCCESS).show();
        },

        // Show range dialog for partial exports
        showRangeDialog: function() {
            var self = this;
            var content = $('<div></div>');

            content.append('<h3>Export Page Range</h3>');
            content.append('<p>Select the range of pages to export:</p>');

            var startPage = $('<input type="number" id="startPage" min="1" max="' + this.totalPages + '" value="1">');
            var endPage = $('<input type="number" id="endPage" min="1" max="' + this.totalPages + '" value="' + Math.min(5, this.totalPages) + '">');

            content.append($('<p>From page: </p>').append(startPage));
            content.append($('<p>To page: </p>').append(endPage));
            content.append('<p><small>Note: Large ranges may take several minutes</small></p>');

            var dialog = new west.gui.Dialog("Export Page Range", content);
            dialog.addButton("Export", function() {
                var start = parseInt(startPage.val()) || 1;
                var end = parseInt(endPage.val()) || 1;

                if (start > end) {
                    new UserMessage('Start page must be less than or equal to end page', UserMessage.TYPE_ERROR).show();
                    return;
                }

                if (start < 1 || end > self.totalPages) {
                    new UserMessage('Page numbers must be between 1 and ' + self.totalPages, UserMessage.TYPE_ERROR).show();
                    return;
                }

                dialog.hide();
                self.exportPageRange(start, end);
            });
            dialog.addButton("Cancel", function() {
                dialog.hide();
            });
            dialog.show();
        },

        // Export specific page range
        exportPageRange: function(startPage, endPage) {
            if (this.isScrapingInProgress) {
                new UserMessage('Scraping already in progress...', UserMessage.TYPE_ERROR).show();
                return;
            }

            this.isScrapingInProgress = true;
            this.playersData = [];
            this.currentPage = startPage;
            this.totalPages = endPage;
            this.retryCount = 0;
            this.startTime = Date.now();

            new UserMessage('Starting range export: pages ' + startPage + '-' + endPage, UserMessage.TYPE_HINT).show();
            this.log('Starting range export: pages ' + startPage + '-' + endPage, 'info');

            this.createEnhancedProgressDialog();
            this.scrapePage(startPage);
        },

        // Enhanced total pages detection
        getTotalPages: function() {
            try {
                // Method 1: Look for pagination text
                var paginationText = $('.rl_pagebar_ranking .maxpages').text();
                if (paginationText) {
                    var match = paginationText.match(/\/\s*(\d+)/);
                    if (match) {
                        this.totalPages = parseInt(match[1]);
                        this.log('Total pages detected (method 1): ' + this.totalPages, 'debug');
                        return;
                    }
                }

                // Method 2: Count pagination links
                var pageLinks = $('.rl_pagebar_ranking .pagebar_page');
                if (pageLinks.length > 0) {
                    var lastPage = 1;
                    pageLinks.each(function() {
                        var pageNum = parseInt($(this).text());
                        if (!isNaN(pageNum) && pageNum > lastPage) {
                            lastPage = pageNum;
                        }
                    });
                    this.totalPages = lastPage;
                    this.log('Total pages detected (method 2): ' + this.totalPages, 'debug');
                    return;
                }

                // Method 3: Try to calculate from total players (if available)
                var totalPlayersText = $('.ranking-experience .ranking_header').text();
                var totalPlayersMatch = totalPlayersText.match(/(\d+)\s*players?/i);
                if (totalPlayersMatch) {
                    var totalPlayers = parseInt(totalPlayersMatch[1]);
                    var playersPerPage = $('.ranking-experience .tbody .row').length || 25;
                    this.totalPages = Math.ceil(totalPlayers / playersPerPage);
                    this.log('Total pages calculated (method 3): ' + this.totalPages + ' (from ' + totalPlayers + ' players)', 'debug');
                    return;
                }

                // Default fallback
                this.totalPages = 16;
                this.log('Using default total pages: ' + this.totalPages, 'warn');

            } catch (e) {
                this.log('Error detecting total pages: ' + e.message, 'error');
                this.totalPages = 16;
            }
        },

        // Enhanced page load waiting with timeout
        waitForPageLoad: function(callback) {
            var self = this;
            var attempts = 0;
            var maxAttempts = 20; // Increased for better reliability
            var startTime = Date.now();

            var checkLoad = function() {
                attempts++;
                var rows = $('.ranking-experience .tbody .tw2gui_scrollpane_clipper_contentpane .row');
                var loadTime = Date.now() - startTime;

                // Check if page is loaded or timeout reached
                if (rows.length > 0) {
                    self.log('Page loaded successfully in ' + loadTime + 'ms (' + rows.length + ' rows)', 'debug');
                    callback();
                } else if (attempts >= maxAttempts || loadTime > 10000) {
                    self.log('Page load timeout after ' + loadTime + 'ms (' + attempts + ' attempts)', 'warn');
                    callback(); // Continue anyway
                } else {
                    setTimeout(checkLoad, 500);
                }
            };

            checkLoad();
        },

        // Stop scraping with cleanup
        stopScraping: function() {
            this.isScrapingInProgress = false;

            if (this.progressDialog) {
                this.progressDialog.hide();
            }

            // Save partial data if any
            if (this.playersData.length > 0 && this.config.autoSave) {
                var partialFilename = 'rankings_partial_' + Date.now() + '.' + this.config.exportFormat;
                this.generateExport(this.playersData, partialFilename);
                new UserMessage('Scraping stopped. Partial data saved (' + this.playersData.length + ' players)', UserMessage.TYPE_HINT).show();
            } else {
                new UserMessage('Scraping stopped', UserMessage.TYPE_ERROR).show();
            }

            this.log('Scraping stopped by user', 'info');
        },

        // Check if on rankings page
        isOnRankingsPage: function() {
            return $('.ranking-experience').length > 0 && $('.rl_pagebar_ranking').length > 0;
        },

        // Check if ranking elements exist (more flexible check)
        canAccessRankings: function() {
            // Check if we're in the game interface
            return $('#ui_menubar').length > 0 && (
                $('.ranking-experience').length > 0 ||
                $('a[href*="ranking"]').length > 0 ||
                $('.menulink').length > 0
            );
        },

        // Cleanup function
        cleanup: function() {
            this.isScrapingInProgress = false;

            if (this.progressDialog) {
                this.progressDialog.hide();
            }

            // Clear any remaining timeouts
            if (this.scrapingTimeout) {
                clearTimeout(this.scrapingTimeout);
            }

            this.log('Cleanup completed', 'debug');
        }
    };

    // Initialize when document and game objects are ready
    $(document).ready(function() {
        try {
            var initInterval = setInterval(function() {
                if (typeof west !== 'undefined' &&
                    typeof west.gui !== 'undefined' &&
                    typeof west.gui.Selectbox !== 'undefined' &&
                    typeof west.gui.Dialog !== 'undefined' &&
                    typeof west.gui.Progressbar !== 'undefined' &&
                    typeof UserMessage !== 'undefined') {

                    clearInterval(initInterval);

                    // Initialize if we can access the game interface
                    if (RankingExporter.canAccessRankings()) {
                        RankingExporter.init();
                        console.log('Rankings Exporter Enhanced initialized');
                    } else {
                        console.log('Game interface not detected, retrying...');
                        // Try again after a longer delay
                        setTimeout(function() {
                            if (RankingExporter.canAccessRankings()) {
                                RankingExporter.init();
                                console.log('Rankings Exporter Enhanced initialized (delayed)');
                            }
                        }, 5000);
                    }
                }
            }, 1000);

            // Cleanup on page unload
            $(window).on('beforeunload', function() {
                if (RankingExporter.isScrapingInProgress) {
                    RankingExporter.cleanup();
                    return 'Export in progress. Are you sure you want to leave?';
                }
            });

        } catch (e) {
            console.error('Error initializing Rankings Exporter Enhanced:', e);
        }
    });

    // Global error handler
    window.onerror = function(msg, url, lineNo, columnNo, error) {
        if (msg.indexOf('RankingExporter') !== -1) {
            RankingExporter.log('Global error: ' + msg + ' at line ' + lineNo, 'error');
            return true;
        }
        return false;
    };

    // Debug info
    console.log('Rankings CSV Exporter Enhanced v' + RankingExporter.version + ' loaded');
    console.log('Features: Adaptive delays, retry logic, multiple formats, filters, progress saving');

});