import UserData;
import Game;
import Math;

namespace Gamepad {

    static class GamepadManager {

        static field _devices = null;
        static field _isGamepadSupported = Game.GamepadImpl_isSupported();
        static field _platformRequiresRefresh = Game.GamepadImpl_platformRequiresRefresh();
        static field _device_count_checked = false;
        static field _device_by_id_ints = null;
        static field _device_by_id_strings = null;

        static constructor() {
            GameWindow._eventPumpHandlers.add(GamepadManager._eventPump);
        }

        static function isGamepadSupported() {
            return GamepadManager._isGamepadSupported;
        }

        static function platformRequiresRefresh() {
            return GamepadManager._platformRequiresRefresh;
        }

        static function refreshDevices() {
            if (GamepadManager._platformRequiresRefresh) {
                devices = GamepadManager._devices;
                if (devices == null) {
                    devices = [];
                    GamepadManager._devices = devices;
                }

                Game.GamepadImpl_refreshDevices();
                newCount = Game.GamepadImpl_getDeviceCount();
                while (newCount > devices.length) {
                    devices.add(new GamepadDevice(devices.length));
                }
            } else if (GamepadManager._devices == null) {
                // For platforms that don't require a refresh, it's okay to ignore successive calls to this function.
                devices = [];
                deviceCount = Game.GamepadImpl_getDeviceCount();
                for (i = 0; i < deviceCount; ++i) {
                    devices.add(new GamepadDevice(i));
                }
                GamepadManager._devices = devices;
            }
        }

        static function getDeviceCount() {
            Core.assert(GamepadManager._devices != null, "Cannot get device count without calling refreshDevices.");
            GamepadManager._device_count_checked = true;
            return GamepadManager._devices.length;
        }

        static function getDeviceByIndex(index) {
            Core.assert(GamepadManager._device_count_checked, "Cannot get device by index without checking device count.");
            Core.assert(index >= 0 && index < GamepadManager._devices.length, "Device index out of range.");
            return GamepadManager._devices[index];
        }

        static function getDeviceById(id) {
            Core.assert(GamepadManager._devices != null, "Cannot get device by ID without initializing.");
            sLookup = GamepadManager._device_by_id_strings;
            iLookup = GamepadManager._device_by_id_ints;
            if (sLookup == null) {
                sLookup = {};
                iLookup = {};
                for (device : GamepadManager._devices) {
                    id = device._id;
                    if (id != null) {
                        if (Core.typeof(id) == Core.Type.INTEGER) {
                            iLookup[id] = device;
                        } else {
                            sLookup[id] = device;
                        }
                    }
                }
                GamepadManager._device_by_id_strings = sLookup;
                GamepadManager._device_by_id_ints = iLookup;
            }

            t = Core.typeof(id);
            if (t == Type.STRING) return sLookup.get(id);
            if (t == Type.INTEGER) return iLookup.get(id);
            return null;
        }

        /*
            Config file format:
            Each line is an entry.
            Each line has columns delimited by a comma.
            Columns:
            - hardware fingerprint (which includes a axis/button/hat count)
            - the number of mashables that are bound
            the following values are repeated N times where N is the number above...
            - mashy hardware ID
            - + or - indicating which direction of the mashy (defaults to ignorable + for buttons)
            - S or I indicating that the button ID is a string or integer
            - button ID, hex encoded if it's a string
            - 1 or 0 -> isDigital
            - 1, 2, or 4 -> dimensionality
            - 0 to 3 -> index in the binding
        */

        static field _settingsCacheDeviceOrder = null;
        static field _settingsCacheDeviceConfig = null;

        // Returns the number of IDs that were successfully assigned.
        // This can be called repeatedly
        static function restoreSettingsFromUserData(deviceIdList) {

            if (Core.typeof(deviceIdList) != Core.Type.LIST) deviceIdList = [deviceIdList];

            if (!GamepadManager._isGamepadSupported) return 0;

            // Read the file from disk and cache it to make successive or frequent reads
            // not painful for performance.
            if (GamepadManager._settingsCacheDeviceOrder == null) {
                ordering = [];
                config = {};
                file = Game.GamepadImpl_getSaveFilePath();
                if (file == null || !UserData.fileExists(file)) return 0;
                for (line : UserData.fileReadText(file).split('\n')) {
                    cols = line.trim().split(',');
                    if (cols.length > 2) {
                        fingerprint = cols[0].trim();
                        mashablesCount = Core.parseInt(cols[1]);

                        if (fingerprint.length > 0 &&
                            mashablesCount != null &&
                            mashablesCount == cols.length - 2) {
                            mashablesCount = mashablesCount / 7;
                            // row is valid-ish.
                            hardwareMashyLookup = {};
                            for (mashyIndex = 0; mashyIndex < mashablesCount; ++mashyIndex) {
                                iStart = mashyIndex * 7 + 2;
                                mashyId = cols[iStart];
                                attributes = cols[iStart + 1:iStart + 7];
                                hardwareMashyLookup[mashyId] = attributes;
                            }
                            ordering.add(fingerprint);
                            config[fingerprint] = hardwareMashyLookup;
                        }
                    }
                }
                GamepadManager._settingsCacheDeviceOrder = ordering;
                GamepadManager._settingsCacheDeviceConfig = config;
            }

            GamepadManager.refreshDevices();
            unassignedDevices = {};
            availableIds = deviceIdList.clone(); // do not mutate incoming list.
            count = 0;
            for (device : GamepadManager._devices) {
                // only modify devices that haven't been touched.
                if (device._id == null) {
                    fingerprint = device._hardware_fingerprint;
                    unassignedDevices[fingerprint] = unassignedDevices.get(fingerprint) ?? [];
                    unassignedDevices[fingerprint].add(device);
                    count++;
                }
            }

            if (count == 0) return 0;

            devicesConfigured = 0;

            for (fingerprint : GamepadManager._settingsCacheDeviceOrder) {
                if (availableIds.length > 0) {
                    devicesWithFingerprint = unassignedDevices.get(fingerprint);
                    if (devicesWithFingerprint != null && devicesWithFingerprint.length > 0) {
                        device = devicesWithFingerprint[0];
                        playerId = availableIds[0];
                        config = GamepadManager._settingsCacheDeviceConfig[fingerprint];
                        configurationSuccesses = false;
                        for (mashyHardwareId : config.keys()) {
                            configlies = config[mashyHardwareId];
                            isPositive = configlies[0] == '+';
                            isButtonIdInteger = configlies[1] == 'I';
                            buttonId = isButtonIdInteger ? Core.parseInt(configlies[2]) : GamepadDevice._hexToString(configlies[2].trim().lower());
                            isDigital = configlies[3] == '1';
                            dimensionality = Core.parseInt(configlies[4]);
                            dimIndex = Core.parseInt(configlies[5]);
                            mashyIndex = device._mashy_fingerprint_to_index.get(mashyHardwareId);

                            // Not the world's most perfect consistency check but should prevent corrupted data
                            // from bringing the game down.
                            if (mashyIndex != null &&
                                buttonId != null && dimensionality != null && dimIndex != null &&
                                (isButtonIdInteger || buttonId.length > 0) &&
                                (dimensionality == 1 || dimensionality == 2 || dimensionality == 4) &&
                                dimIndex >= 0 && dimIndex <= 3) {

                                if (!configurationSuccesses) {
                                    device._config_stack.add([{}, {}]);
                                    configurationSuccesses = true;
                                }
                                bindingLookup = device._config_stack[-1][isButtonIdInteger ? 0 : 1];
                                binding = bindingLookup.get(buttonId);
                                if (binding == null) {
                                    binding = [isDigital, dimensionality, null, null, null, null];
                                    bindingLookup[buttonId] = binding;
                                }

                                binding[2 + dimIndex] = mashyIndex * (isPositive ? 1 : -1);
                            }
                        }

                        if (configurationSuccesses) {
                            device.setId(playerId);
                            devicesConfigured++;
                            availableIds.remove(0);
                            devicesWithFingerprint.remove(0);
                        }
                    }
                }
            }
            return devicesConfigured;
        }

        static function saveSettingsToUserData() {
            // TODO: might be nice to introduce a dirty bit to indicate whether anything has actually changed since the last time
            // this has been called.
            GamepadManager._settingsCacheDeviceOrder = null; // clear the cache
            GamepadManager._settingsCacheDeviceConfig = null;

            file = Game.GamepadImpl_getSaveFilePath();
            if (file == null) return;
            exists = UserData.fileExists(file);
            if (exists) return;

            existingOrder = [];
            deviceDataById = {};

            if (exists) {
                for (line : UserData.fileReadText(file).split('\n')) {
                    parts = line.split(':');
                    if (parts.length > 2) {
                        id = parts[0].trim();
                        bindings = Core.parseInt(parts[1].trim());
                        if (bindings != null && bindings > 0 && bindings.length * 7 == (parts.length - 2)) {
                            if (!deviceDataById.contains(id)) {
                            existingOrder.add(id);
                            }
                            // data might be totally bogus but weren't not making it worse
                            deviceDataById[id] = parts[2:];
                        }
                    }
                }
            }

            if (GamepadManager._devices != null) {
                for (device : GamepadManager._devices) {
                    if (device._config_stack.length > 0) {
                        config = device._config_stack[-1];
                        saved = [];
                        if (config[0].length > 0 || config[1].length > 0) {
                            for (lookup : config) {
                                for (buttonId : lookup.keys()) {
                                    binding = lookup[buttonId];
                                    for (i = 0; i < binding[1]; ++i) {
                                        signedIndex = binding[2 + i];
                                        if (signedIndex != null) {
                                            mashy = device._hardware_mashies[Math.abs(signedIndex)];
                                            isPositive = signedIndex > 0;
                                            saved.add(
                                                mashy[2], // mashy hardware ID
                                                isPositive ? '+' : '-',
                                                Core.typeof(buttonId) == Core.Type.INTEGER ? 'I' : 'S',
                                                Core.typeof(buttonId) == Core.Type.INTEGER ? ('' + buttonId) : GamepadDevice._stringToHex(buttonId),
                                                binding[0] ? '1' : '0', // isDigital
                                                '' + binding[1], // dimensionality
                                                '' + i); // 0 to 3 for index in the binding
                                        }
                                    }
                                }
                            }
                        }
                        if (saved.length > 0) {
                            id = device._hardware_fingerprint;
                            if (deviceDataById.contains(id)) {
                                for (i = 0; i < existingOrder.length; ++i) {
                                    if (existingOrder[i] == id) {
                                        existingOrder.remove(i);
                                        break;
                                    }
                                }
                            }
                            existingOrder.insert(0, id);
                            deviceDataById[id] = saved;
                        }
                    }
                }
            }

            if (existingOrder.length > 0) {
                lines = [];
                for (id : existingOrder) {
                    data = deviceDataById[id];
                    data = [id, data.length] + data;
                    lines.add(data.join(','));
                }
                UserData.fileWriteText(file, lines.join('\n'));
            }
        }

        static function clearAllIds() {
            Core.assert(GamepadManager._devices != null, "Cannot clear IDs without initializing.");
            for (d : GamepadManager._devices) {
                d._id = null;
            }
        }

        static function _eventPump(eventsOut) {
            if (GamepadManager._devices != null) {
                for (device : GamepadManager._devices) {
                    device._updatePass(eventsOut);
                }
            }
        }

        // TODO: Determine if a dead zone filter should be global or set on individual gamepads.
    }
}
