Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
folium-app
GitHub Repository: folium-app/Folium
Path: blob/a-new-beginning/Folium-iOS/Controllers/SettingsControllers/CytrusSettingsController.swift
2 views
//
//  CytrusSettingsController.swift
//  Folium-iOS
//
//  Created by Jarrod Norwell on 26/7/2025.
//

import Cytrus
import Foundation
import SettingsKit
import UIKit

enum CytrusSettingsHeaders : String, CaseIterable {
    case core = "Core"
    case dataStorage = "Data Storage"
    case debugging = "Debugging"
    case system = "System"
    case systemSaveGame = "System Save Game"
    case renderer = "Renderer"
    case defaultLayout = "Default Layout"
    case customLayout = "Custom Layout"
    case audio = "Audio"
    case miscellaneous = "Miscellaneous"
    
    case destructive = "Destructive"
    
    var header: SettingHeader {
        switch self {
        case .core,
                .dataStorage,
                .debugging,
                .system,
                .systemSaveGame,
                .renderer,
                .defaultLayout,
                .audio,
                .miscellaneous:
            SettingHeader(text: rawValue)
        case .customLayout:
            SettingHeader(text: rawValue,
                          secondaryText: "Set Default Layout to Custom Layout")
        case .destructive:
            SettingHeader(text: "")
        }
    }
    
    static var allHeaders: [SettingHeader] { allCases.map { $0.header } }
}

enum CytrusSettingsItems : String, CaseIterable {
    // Destructive
    case resetSettings = "cytrus.v1.38.resetSettings"
    
    // Core
    case mode = "cytrus.v1.38.mode"
    case cpuJIT = "cytrus.v1.38.cpuJIT"
    case cpuClockPercentage = "cytrus.v1.38.cpuClockPercentage"
    case new3DS = "cytrus.v1.38.new3DS"
    case lleApplets = "cytrus.v1.38.lleApplets"
    case deterministicAsyncOperations = "cytrus.v1.38.deterministicAsyncOperations"
    case enableRequiredOnlineLLEModules = "cytrus.v1.38.enableRequiredOnlineLLEModules"
    
    // Data Storage
    case compressCIAInstalls = "cytrus.v1.38.compressCIAInstalls"
    
    // System
    case regionValue = "cytrus.v1.38.regionValue"
    case pluginLoader = "cytrus.v1.38.pluginLoader"
    case allowPluginLoader = "cytrus.v1.38.allowPluginLoader"
    case stepsPerHour = "cytrus.v1.38.stepsPerHour"
    case applyRegionFreePatch = "cytrus.v1.38.applyRegionFreePatch"
    
    // Renderer
    case spirvShaderGeneration = "cytrus.v1.38.spirvShaderGeneration"
    case disableSpirvOptimizer = "cytrus.v1.38.disableSpirvOptimizer"
    case useAsyncShaderCompilation = "cytrus.v1.38.useAsyncShaderCompilation"
    case useAsyncPresentation = "cytrus.v1.38.useAsyncPresentation"
    case useHardwareShaders = "cytrus.v1.38.useHardwareShaders"
    case useDiskShaderCache = "cytrus.v1.38.useDiskShaderCache"
    case useShadersAccurateMul = "cytrus.v1.38.useShadersAccurateMul"
    case useNewVSync = "cytrus.v1.38.useNewVSync"
    case useShaderJIT = "cytrus.v1.38.useShaderJIT"
    case resolutionFactor = "cytrus.v1.38.resolutionFactor"
    case textureFilter = "cytrus.v1.38.textureFilter"
    case textureSampling = "cytrus.v1.38.textureSampling"
    case delayGameRenderThreadUS = "cytrus.v1.38.delayGameRenderThreadUS"
    case layoutOption = "cytrus.v1.38.layoutOption"
    case customTopX = "cytrus.v1.38.customTopX"
    case customTopY = "cytrus.v1.38.customTopY"
    case customTopWidth = "cytrus.v1.38.customTopWidth"
    case customTopHeight = "cytrus.v1.38.customTopHeight"
    case customBottomX = "cytrus.v1.38.customBottomX"
    case customBottomY = "cytrus.v1.38.customBottomY"
    case customBottomWidth = "cytrus.v1.38.customBottomWidth"
    case customBottomHeight = "cytrus.v1.38.customBottomHeight"
    case customSecondLayerOpacity = "cytrus.v1.38.customSecondLayerOpacity"
    case aspectRatio = "cytrus.v1.38.aspectRatio"
    case render3D = "cytrus.v1.38.render3D"
    case factor3D = "cytrus.v1.38.factor3D"
    case monoRender = "cytrus.v1.38.monoRender"
    case filterMode = "cytrus.v1.38.filterMode"
    case ppShaderName = "cytrus.v1.38.ppShaderName"
    case anaglyphShaderName = "cytrus.v1.38.anaglyphShaderName"
    case dumpTextures = "cytrus.v1.38.dumpTextures"
    case customTextures = "cytrus.v1.38.customTextures"
    case preloadTextures = "cytrus.v1.38.preloadTextures"
    case asyncCustomLoading = "cytrus.v1.38.asyncCustomLoading"
    case disableRightEyeRender = "cytrus.v1.38.disableRightEyeRender"
    
    // Audio
    case audioMuted = "cytrus.v1.38.audioMuted"
    case audioEmulation = "cytrus.v1.38.audioEmulation"
    case audioStretching = "cytrus.v1.38.audioStretching"
    case realtimeAudio = "cytrus.v1.38.realtimeAudio"
    case volume = "cytrus.v1.38.volume"
    case outputType = "cytrus.v1.38.outputType"
    case inputType = "cytrus.v1.38.inputType"
    
    // Miscellaneous
    case logLevel = "cytrus.v1.38.logLevel"
    case webAPIURL = "cytrus.v1.38.webAPIURL"
    
    case systemLanguage = "cytrus.v1.38.systemLanguage"
    case username = "cytrus.v1.38.username"
    
    var title: String {
        switch self {
        case .resetSettings:
            "Reset Settings"
        case .mode:
            "Emulation Mode"
        case .cpuJIT:
            "CPU JIT"
        case .cpuClockPercentage:
            "CPU Clock Percentage"
        case .new3DS:
            "New 3DS"
        case .lleApplets:
            "LLE Applets"
        case .deterministicAsyncOperations:
            "Deterministic Async Operations"
        case .enableRequiredOnlineLLEModules:
            "Required Online LLE Modules"
        case .compressCIAInstalls:
            "Compress CIA Installs"
        case .regionValue:
            "Region Value"
        case .pluginLoader:
            "Plugin Loader"
        case .allowPluginLoader:
            "Plugin Loader (Homebrew)"
        case .stepsPerHour:
            "Steps Per Hour"
        case .applyRegionFreePatch:
            "Apply Region-Free Patch"
        case .spirvShaderGeneration:
            "SPIR-V Shader Generation"
        case .disableSpirvOptimizer:
            "Disable SPIR-V Optimizer"
        case .useAsyncShaderCompilation:
            "Async Shader Compilation"
        case .useAsyncPresentation:
            "Async Presentation"
        case .useHardwareShaders:
            "Hardware Shaders"
        case .useDiskShaderCache:
            "Disk Shader Cache"
        case .useShadersAccurateMul:
            "Shaders Accurate Mul"
        case .useNewVSync:
            "New VSync"
        case .useShaderJIT:
            "Shader JIT"
        case .resolutionFactor:
            "Resolution Factor"
        case .textureFilter:
            "Texture Filter"
        case .textureSampling:
            "Texture Sampling"
        case .delayGameRenderThreadUS:
            "Delay Game Render Thread US"
        case .layoutOption:
            "Layout Option"
        case .customTopX:
            "Custom Top X"
        case .customTopY:
            "Custom Top Y"
        case .customTopWidth:
            "Custom Top Width"
        case .customTopHeight:
            "Custom Top Height"
        case .customBottomX:
            "Custom Bottom X"
        case .customBottomY:
            "Custom Bottom Y"
        case .customBottomWidth:
            "Custom Bottom Width"
        case .customBottomHeight:
            "Custom Bottom Height"
        case .customSecondLayerOpacity:
            "Custom Second Layer Opacity"
        case .aspectRatio:
            "Aspect Ratio"
        case .render3D:
            "Render 3D"
        case .factor3D:
            "Factor 3D"
        case .monoRender:
            "Mono Render"
        case .filterMode:
            "Filter Mode"
        case .ppShaderName:
            "PP Shader Name"
        case .anaglyphShaderName:
            "Anaglyph Shader Name"
        case .dumpTextures:
            "Dump Textures"
        case .customTextures:
            "Custom Textures"
        case .preloadTextures:
            "Preload Textures"
        case .asyncCustomLoading:
            "Async Custom Loading"
        case .disableRightEyeRender:
            "Disable Right Eye Render"
        case .audioMuted:
            "Audio Muted"
        case .audioEmulation:
            "Audio Emulation"
        case .audioStretching:
            "Audio Stretching"
        case .realtimeAudio:
            "Realtime Audio"
        case .volume:
            "Volume"
        case .outputType:
            "Output Type"
        case .inputType:
            "Input Type"
        case .logLevel:
            "Log Level"
        case .webAPIURL:
            "Web API URL"
        case .systemLanguage:
            "System Language"
        case .username:
            "Username"
        }
    }
    
    var secondaryTitle: String? {
        if #available(iOS 26, *) {
            switch self {
            case .cpuJIT,
                    .useShaderJIT:
                "Unavailable on iOS 26"
            default:
                nil
            }
        } else {
            switch self {
            default:
                nil
            }
        }
    }
    
    var details: String? {
        switch self {
        case .resetSettings:
            "Resets all settings to their default values"
        case .mode:
            "Automatically selects the best settings to achieve the selected mode"
        case .cpuJIT:
            "Enables the use of the Just-In-Time (JIT) compiler for CPU emulation significantly improving performance"
        case .cpuClockPercentage:
            "Changes the clock frequency of the 3DS CPU\n\nUnderclocking can increase performance at the risk of freezing\n\nOverclocking can fix lag at the risk of freezing"
        case .new3DS:
            "Changes the system model of the 3DS that Cytrus will try to emulate"
        case .lleApplets:
            "Enables the use of LLE system applets, if installed"
        case .deterministicAsyncOperations:
            "Forces deterministic asynchronous operations for debugging, reducing performance"
        case .enableRequiredOnlineLLEModules:
            "Enables the LLE modules required for online play, if installed"
        case .compressCIAInstalls:
            "Enables the compression of installed CIA contents, reducing storage usage"
        case .regionValue:
            "Changes the system region of the 3DS that Cytrus will use when emulating"
        case .pluginLoader:
            "Enables plugins to be loaded from the SD card"
        case .allowPluginLoader:
            "Enables plugins to be loaded from the SD card for homebrew apps"
        case .stepsPerHour:
            "Sets the number of steps reported to the pedometer"
        case .applyRegionFreePatch:
            "Patches the region of installed applications allowing them to always be displayed on the 3DS home menu"
        case .spirvShaderGeneration:
            ""
        case .disableSpirvOptimizer:
            ""
        case .useAsyncShaderCompilation:
            ""
        case .useAsyncPresentation:
            ""
        case .useHardwareShaders:
            ""
        case .useDiskShaderCache:
            ""
        case .useShadersAccurateMul:
            ""
        case .useNewVSync:
            ""
        case .useShaderJIT:
            "Enables the use of the Just-In-Time (JIT) compiler for shader emulation significantly improving performance"
        case .resolutionFactor:
            ""
        case .textureFilter:
            ""
        case .textureSampling:
            ""
        case .delayGameRenderThreadUS:
            ""
        case .layoutOption:
            ""
        case .customTopX:
            ""
        case .customTopY:
            ""
        case .customTopWidth:
            ""
        case .customTopHeight:
            ""
        case .customBottomX:
            ""
        case .customBottomY:
            ""
        case .customBottomWidth:
            ""
        case .customBottomHeight:
            ""
        case .customSecondLayerOpacity:
            ""
        case .aspectRatio:
            ""
        case .render3D:
            ""
        case .factor3D:
            ""
        case .monoRender:
            ""
        case .filterMode:
            ""
        case .ppShaderName:
            ""
        case .anaglyphShaderName:
            ""
        case .dumpTextures:
            ""
        case .customTextures:
            ""
        case .preloadTextures:
            ""
        case .asyncCustomLoading:
            ""
        case .disableRightEyeRender:
            "Disables the rendering of the right eye image significantly increasing performance in some games at the cost of causing flickers in others"
        case .audioMuted:
            ""
        case .audioEmulation:
            ""
        case .audioStretching:
            "Enables the audio stretching post-processing effect adjusting audio speed to match emulation speed to help prevent audio stutter at the cost of increasing audio latency"
        case .realtimeAudio:
            "Enables the scaling of audio playback speed to account for drops in emulation speed"
        case .volume:
            ""
        case .outputType:
            ""
        case .inputType:
            ""
        case .logLevel:
            ""
        case .webAPIURL:
            ""
        case .systemLanguage:
            ""
        case .username:
            ""
        }
    }
    
    var isEnabled: Bool {
        if #available(iOS 26, *) {
            switch self {
            case .cpuJIT,
                    .useShaderJIT:
                false
            default:
                true
            }
        } else {
            true
        }
    }
    
    func setting(_ delegate: SettingDelegate? = nil) -> BaseSetting {
        switch self {
        case .resetSettings:
            TapSetting(key: rawValue,
                       title: title,
                       details: details,
                       color: .systemRed,
                       handler: { controller in
                guard let controller: CytrusSettingsController = controller as? CytrusSettingsController else {
                    return
                }
                
                if #available(iOS 26, *) {
                    UserDefaults.standard.set(false, forKey: "cytrus.v1.38.cpuJIT")
                    UserDefaults.standard.set(false, forKey: "cytrus.v1.38.useShaderJIT")
                } else {
                    UserDefaults.standard.set(false, forKey: "cytrus.v1.38.cpuJIT")
                    UserDefaults.standard.set(true, forKey: "cytrus.v1.38.useShaderJIT")
                }
                
                let defaults: [String : [String : Any]] = [
                    "Cytrus" : [
                        "mode" : 0,
                        "cpuClockPercentage" : 100,
                        "new3DS" : true,
                        "lleApplets" : true,
                        "deterministicAsyncOperations" : false,
                        "enableRequiredOnlineLLEModules" : false,
                        
                        "compressCIAInstalls" : false,
                        
                        "regionValue" : -1,
                        "pluginLoader" : false,
                        "allowPluginLoader" : true,
                        "stepsPerHour" : 0,
                        "applyRegionFreePatch" : true,
                        
                        "spirvShaderGeneration" : true,
                        "disableSpirvOptimizer" : true,
                        "useAsyncShaderCompilation" : false,
                        "useAsyncPresentation" : true,
                        "useHardwareShaders" : true,
                        "useDiskShaderCache" : true,
                        "useShadersAccurateMul" : true,
                        "useNewVSync" : true,
                        "resolutionFactor" : 1,
                        "textureFilter" : 0,
                        "textureSampling" : 0,
                        "delayGameRenderThreadUS" : 0,
                        "layoutOption" : 0,
                        "customTopX" : 0,
                        "customTopY" : 0,
                        "customTopWidth" : 800,
                        "customTopHeight" : 480,
                        "customBottomX" : 80,
                        "customBottomY" : 500,
                        "customBottomWidth" : 640,
                        "customBottomHeight" : 480,
                        "customSecondLayerOpacity" : 100,
                        "aspectRatio" : 0,
                        "render3D" : 0,
                        "factor3D" : 0,
                        "monoRender" : 0,
                        "filterMode" : true,
                        "ppShaderName" : "none (builtin)",
                        "anaglyphShaderName" : "dubois (builtin)",
                        "dumpTextures" : false,
                        "customTextures" : false,
                        "preloadTextures" : false,
                        "asyncCustomLoading" : true,
                        "disableRightEyeRender" : false,
                        
                        "audioMuted" : false,
                        "audioEmulation" : 0,
                        "audioStretching" : true,
                        "realtimeAudio" : false,
                        "volume" : 1,
                        "outputType" : 0,
                        "inputType" : 0,
                        
                        "logLevel" : 2,
                        "webAPIURL" : "http://88.198.47.46:5000",
                        
                        "systemLanguage" : 1,
                        "username" : "Cytrus"
                    ]
                ]
                
                defaults.forEach { core, values in
                    values.forEach { key, value in
                        UserDefaults.standard.set(value, forKey: "\(core.lowercased()).v1.38.\(key)")
                    }
                }
                
                controller.populateSettings()
            },
                       delegate: delegate)
        case .mode:
            SegmentedSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Speed" : 0,
                                "Looks" : 1
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: { controller in
                guard let controller: CytrusSettingsController = controller as? CytrusSettingsController else {
                    return
                }
                
                switch UserDefaults.standard.integer(forKey: rawValue) {
                case 0:
                    let defaults: [String : [String : Any]] = [
                        "Cytrus" : [
                            "cpuClockPercentage" : 30,
                            
                            "disableRightEyeRender" : true,
                            
                            "realtimeAudio" : true
                        ]
                    ]
                    
                    defaults.forEach { core, values in
                        values.forEach { key, value in
                            UserDefaults.standard.set(value, forKey: "\(core.lowercased()).v1.38.\(key)")
                        }
                    }
                case 1:
                    let defaults: [String : [String : Any]] = [
                        "Cytrus" : [
                            "cpuClockPercentage" : 100,
                            
                            "disableRightEyeRender" : false,
                            
                            "realtimeAudio" : false
                        ]
                    ]
                    
                    defaults.forEach { core, values in
                        values.forEach { key, value in
                            UserDefaults.standard.set(value, forKey: "\(core.lowercased()).v1.38.\(key)")
                        }
                    }
                default:
                    break
                }
                
                controller.populateSettings()
            },
                             delegate: delegate)
        case .cpuJIT,
                .new3DS,
                .lleApplets,
                .deterministicAsyncOperations,
                .enableRequiredOnlineLLEModules,
                .compressCIAInstalls,
                .pluginLoader,
                .allowPluginLoader,
                .applyRegionFreePatch,
                .spirvShaderGeneration,
                .disableSpirvOptimizer,
                .useAsyncShaderCompilation,
                .useAsyncPresentation,
                .useHardwareShaders,
                .useDiskShaderCache,
                .useShadersAccurateMul,
                .useNewVSync,
                .useShaderJIT,
                .filterMode,
                .dumpTextures,
                .customTextures,
                .preloadTextures,
                .asyncCustomLoading,
                .disableRightEyeRender,
                .audioMuted,
                .audioStretching,
                .realtimeAudio:
            BoolSetting(key: rawValue,
                        title: title,
                        details: details,
                        secondaryTitle: secondaryTitle,
                        isEnabled: isEnabled,
                        value: UserDefaults.standard.bool(forKey: rawValue),
                        delegate: delegate)
        case .cpuClockPercentage:
            InputNumberSetting(key: rawValue,
                               title: title,
                               details: details,
                               min: 5,
                               max: 400,
                               value: UserDefaults.standard.double(forKey: rawValue),
                               delegate: delegate)
        case .regionValue:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Automatic" : -1,
                                "Japan" : 0,
                                "USA" : 1,
                                "Europe" : 2,
                                "Australia" : 3,
                                "China" : 4,
                                "Korea" : 5,
                                "Taiwan" : 6
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .stepsPerHour:
            InputNumberSetting(key: rawValue,
                               title: title,
                               details: details,
                               min: 0,
                               max: 9999,
                               value: UserDefaults.standard.double(forKey: rawValue),
                               delegate: delegate)
        case .resolutionFactor:
            StepperSetting(key: rawValue,
                           title: title,
                           details: details,
                           min: 0,
                           max: 10,
                           value: UserDefaults.standard.double(forKey: rawValue),
                           delegate: delegate)
        case .textureFilter:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "None" : 0,
                                "Anime4K" : 1,
                                "Bicubic" : 2,
                                "ScaleForce" : 3,
                                "xBRZ" : 4,
                                "MMPX" : 5
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .textureSampling:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Game Controlled" : 0,
                                "Nearest Neighbor" : 1,
                                "Linear" : 2
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .delayGameRenderThreadUS:
            InputNumberSetting(key: rawValue,
                               title: title,
                               details: details,
                               min: 0,
                               max: 16000,
                               value: UserDefaults.standard.double(forKey: rawValue),
                               delegate: delegate)
        case .layoutOption:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Default" : 0,
                                "Single Screen" : 1,
                                "Large Screen" : 2,
                                "Side Screen" : 3,
                                "Hybrid Screen" : 5,
                                "Custom Layout" : 6
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .customTopX,
                .customTopY,
                .customTopWidth,
                .customTopHeight,
                .customBottomX,
                .customBottomY,
                .customBottomWidth,
                .customBottomHeight:
            InputNumberSetting(key: rawValue,
                               title: title,
                               details: details,
                               min: 0,
                               max: 9999,
                               value: UserDefaults.standard.double(forKey: rawValue),
                               delegate: delegate)
        case .customSecondLayerOpacity:
            InputNumberSetting(key: rawValue,
                               title: title,
                               details: details,
                               min: 0,
                               max: 100,
                               value: UserDefaults.standard.double(forKey: rawValue),
                               delegate: delegate)
        case .aspectRatio:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Default" : 0,
                                "16:9" : 1,
                                "4:3" : 2,
                                "21:9" : 3,
                                "16:10" : 4,
                                "Stretch" : 5
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .render3D:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Off" : 0,
                                "Side by Side" : 1,
                                "Anaglyph" : 2,
                                "Interlaced" : 3,
                                "ReverseInterlaced" : 4,
                                "CardboardVR" : 5
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .factor3D:
            InputNumberSetting(key: rawValue,
                               title: title,
                               details: details,
                               min: 0,
                               max: 100,
                               value: UserDefaults.standard.double(forKey: rawValue),
                               delegate: delegate)
        case .monoRender:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Left Eye" : 0,
                                "Right Eye" : 1
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .ppShaderName:
            InputStringSetting(key: rawValue,
                               title: title,
                               details: details,
                               placeholder: "None (builtin)",
                               value: UserDefaults.standard.string(forKey: rawValue),
                               action: {},
                               delegate: delegate)
        case .anaglyphShaderName:
            InputStringSetting(key: rawValue,
                               title: title,
                               details: details,
                               placeholder: "Dubois (builtin)",
                               value: UserDefaults.standard.string(forKey: rawValue),
                               action: {},
                               delegate: delegate)
        case .audioEmulation:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "HLE" : 0,
                                "LLE" : 1,
                                "LLE (Multithreaded)" : 2
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .volume:
            StepperSetting(key: rawValue,
                           title: title,
                           details: details,
                           min: 0,
                           max: 1,
                           value: UserDefaults.standard.double(forKey: rawValue),
                           delegate: delegate)
        case .outputType:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Automatic" : 0,
                                "None" : 1,
                                "OpenAL" : 3,
                                // "SDL3" : 5,
                                "CoreAudio" : 6
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .inputType:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Automatic" : 0,
                                "None" : 1,
                                "Static" : 2,
                                "OpenAL" : 3
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .logLevel:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Trace" : 0,
                                "Debug" : 1,
                                "Info" : 2,
                                "Warning" : 3,
                                "Error" : 4,
                                "Critical" : 5
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {},
                             delegate: delegate)
        case .webAPIURL:
            InputStringSetting(key: rawValue,
                               title: title,
                               details: details,
                               placeholder: "http(s)://address:port",
                               value: UserDefaults.standard.string(forKey: rawValue),
                               action: {
                CytrusMultiplayerManager.shared().updateWebAPIURL()
            },
                               delegate: delegate)
        case .systemLanguage:
            SelectionSetting(key: rawValue,
                             title: title,
                             details: details,
                             values: [
                                "Japanese" : 0,
                                "English" : 1,
                                "French" : 2,
                                "German" : 3,
                                "Italian" : 4,
                                "Spanish" : 5,
                                "Simplified Chinese" : 6,
                                "Korean" : 7,
                                "Dutch" : 8,
                                "Portuguese" : 9,
                                "Russian" : 10,
                                "Traditional Chinese" : 11
                             ],
                             selectedValue: UserDefaults.standard.value(forKey: rawValue),
                             action: {
                // guard let systemLanguage = UserDefaults.standard.value(forKey: rawValue) as? Int else { return }
                // SystemSaveGame.shared.set(systemLanguage)
            },
                             delegate: delegate)
        case .username:
            InputStringSetting(key: rawValue,
                               title: title,
                               details: details,
                               placeholder: "Cytrus",
                               value: UserDefaults.standard.string(forKey: rawValue),
                               action: {
                // guard let username = UserDefaults.standard.string(forKey: rawValue) else { return }
                // SystemSaveGame.shared.set(username)
            },
                               delegate: delegate)
        }
    }
    
    static func settings(_ header: CytrusSettingsHeaders) -> [CytrusSettingsItems] {
        switch header {
        case .core:
            [
                .mode,
                .cpuClockPercentage,
                .new3DS,
                .lleApplets,
                .deterministicAsyncOperations,
                .enableRequiredOnlineLLEModules
            ]
        case .dataStorage:
            [
                .compressCIAInstalls
            ]
        case .debugging:
            [
                .cpuJIT,
                .logLevel
            ]
        case .system:
            [
                .regionValue,
                .pluginLoader,
                .allowPluginLoader,
                .stepsPerHour,
                .applyRegionFreePatch
            ]
        case .systemSaveGame:
            [
                .systemLanguage,
                .username
            ]
        case .renderer:
            [
                .spirvShaderGeneration,
                .disableSpirvOptimizer,
                .useAsyncShaderCompilation,
                .useAsyncPresentation,
                .useHardwareShaders,
                .useDiskShaderCache,
                .useShadersAccurateMul,
                .useNewVSync,
                .useShaderJIT,
                .aspectRatio,
                .resolutionFactor,
                .textureFilter,
                .textureSampling,
                .delayGameRenderThreadUS,
                .render3D,
                .factor3D,
                .monoRender,
                .filterMode,
                .ppShaderName,
                .anaglyphShaderName,
                .dumpTextures,
                .customTextures,
                .preloadTextures,
                .asyncCustomLoading,
                .disableRightEyeRender
            ]
        case .defaultLayout:
            [
                .layoutOption
            ]
        case .customLayout:
            [
                .customTopX,
                .customTopY,
                .customTopWidth,
                .customTopHeight,
                .customBottomX,
                .customBottomY,
                .customBottomWidth,
                .customBottomHeight,
                .customSecondLayerOpacity,
            ]
        case .audio:
            [
                .audioMuted,
                .audioEmulation,
                .audioStretching,
                .realtimeAudio,
                .volume,
                .outputType,
                .inputType
            ]
        case .miscellaneous:
            [
                .webAPIURL
            ]
        case .destructive:
            [
                .resetSettings
            ]
        }
    }
}

class CytrusSettingsController : UIViewController, UICollectionViewDelegate {
    var dataSource: UICollectionViewDiffableDataSource<CytrusSettingsHeaders, AnyHashableSendable>! = nil
    var snapshot: NSDiffableDataSourceSnapshot<CytrusSettingsHeaders, AnyHashableSendable>! = nil
    
    var cytrus: Cytrus
    init(_ cytrus: Cytrus) {
        self.cytrus = cytrus
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        if let navigationController {
            navigationController.navigationBar.prefersLargeTitles = true
        }
        navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark"), primaryAction: UIAction { _ in
            self.dismiss(animated: true)
        })
        if #available(iOS 26, *) {
            navigationItem.title = "Cytrus"
            navigationItem.largeTitle = navigationItem.title
            navigationItem.subtitle = "Nintendo 3DS (Azahar)"
            navigationItem.largeSubtitle = navigationItem.subtitle
        } else {
            title = "Cytrus"
        }
        navigationItem.style = .browser
        
        var configuration: UICollectionLayoutListConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        configuration.backgroundColor = .clear
        configuration.headerMode = .supplementary
        configuration.trailingSwipeActionsConfigurationProvider = { indexPath in
            guard let dataSource = self.dataSource, let item: BaseSetting = dataSource.itemIdentifier(for: indexPath) as? BaseSetting else {
                return UISwipeActionsConfiguration()
            }
            
            let informationContextualAction: UIContextualAction = UIContextualAction(style: .normal, title: nil, handler: { action, view, performed in
                let alertController: UIAlertController = UIAlertController(title: item.title, message: item.details, preferredStyle: .alert)
                alertController.addAction(UIAlertAction(title: "Dismiss", style: .default) { _ in
                    performed(true)
                })
                // alertController.preferredAction = alertController.actions.first
                self.present(alertController, animated: true)
            })
            informationContextualAction.image = UIImage(systemName: "info")
            
            return UISwipeActionsConfiguration(actions: [
                informationContextualAction
            ])
        }
        
        let collectionView: UICollectionView = UICollectionView(frame: .zero,
                                                                collectionViewLayout: UICollectionViewCompositionalLayout.list(using: configuration))
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.delegate = self
        view.addSubview(collectionView)
        
        collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        
        if #available(iOS 26, *) {
            let backgroundView: UIVisualEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular))
            backgroundView.cornerConfiguration = .corners(radius: .containerConcentric())
            
            collectionView.backgroundColor = .clear
            collectionView.backgroundView = backgroundView
            view.backgroundColor = .clear
        } else {
            view.backgroundColor = .systemBackground
        }
        
        let selectionCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SelectionSetting> { cell, indexPath, itemIdentifier in
            if #available(iOS 26, *) {
                var backgroundConfiguration: UIBackgroundConfiguration = .clear()
                
                let effect: UIGlassEffect = UIGlassEffect(style: .regular)
                
                let visualEffectView: UIVisualEffectView = UIVisualEffectView(effect: effect)
                visualEffectView.cornerConfiguration = .corners(topLeftRadius: .fixed(cell.effectiveRadius(corner: .topLeft)),
                                                                topRightRadius: .fixed(cell.effectiveRadius(corner: .topRight)),
                                                                bottomLeftRadius: .fixed(cell.effectiveRadius(corner: .bottomLeft)),
                                                                bottomRightRadius: .fixed(cell.effectiveRadius(corner: .bottomRight)))
                backgroundConfiguration.customView = visualEffectView
                cell.backgroundConfiguration = backgroundConfiguration
            }
            
            var contentConfiguration = UIListContentConfiguration.cell()
            contentConfiguration.text = itemIdentifier.title
            cell.contentConfiguration = contentConfiguration
            
            let children: [UIMenuElement] = switch itemIdentifier.values {
            case let stringInt as [String : Int]:
                stringInt.reduce(into: [UIAction](), { partialResult, element in
                    var state: UIMenuElement.State = .off
                    if let selectedValue = itemIdentifier.selectedValue as? Int {
                        state = element.value == selectedValue ? .on : .off
                    }
                    
                    partialResult.append(.init(title: element.key, state: state, handler: { _ in
                        UserDefaults.standard.set(element.value, forKey: itemIdentifier.key)
                        if let delegate = itemIdentifier.delegate {
                            delegate.didChangeSetting(at: indexPath)
                        }
                    }))
                })
            case let stringString as [String : String]:
                stringString.reduce(into: [UIAction](), { partialResult, element in
                    var state: UIMenuElement.State = .off
                    if let selectedValue = itemIdentifier.selectedValue as? String {
                        state = element.value == selectedValue ? .on : .off
                    }
                    
                    partialResult.append(.init(title: element.key, state: state, handler: { _ in
                        UserDefaults.standard.set(element.value, forKey: itemIdentifier.key)
                        if let delegate = itemIdentifier.delegate {
                            delegate.didChangeSetting(at: indexPath)
                        }
                    }))
                })
            default:
                []
            }
            
            var title = "Automatic"
            if let selectedValue = itemIdentifier.selectedValue {
                switch selectedValue {
                case let intValue as Int:
                    if let values = itemIdentifier.values as? [String : Int] {
                        title = values.first(where: { $0.value == intValue })?.key ?? title
                    }
                case let stringValue as String:
                    if let values = itemIdentifier.values as? [String : String] {
                        title = values.first(where: { $0.value == stringValue })?.key ?? title
                    }
                default:
                    break
                }
            }
            
            cell.accessories = [
                UICellAccessory.label(text: title),
                UICellAccessory.popUpMenu(UIMenu(children: children.sorted(by: { $0.title < $1.title })))
            ]
        }
        
        let blankSettingsCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, BlankSetting> { cell, indexPath, itemIdentifier in
            cell.contentConfiguration = UIListContentConfiguration.cell()
        }
        
        let headerCellRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
            var contentConfiguration = UIListContentConfiguration.extraProminentInsetGroupedHeader()
            contentConfiguration.text = self.snapshot.sectionIdentifiers[indexPath.section].header.text
            contentConfiguration.secondaryText = self.snapshot.sectionIdentifiers[indexPath.section].header.secondaryText
            contentConfiguration.secondaryTextProperties.color = .secondaryLabel
            supplementaryView.contentConfiguration = contentConfiguration
        }
        
        let boolCell: UICollectionView.CellRegistration<UICollectionViewListCell, BoolSetting> = CellManager.Settings.boolCell
        let inputNumberCell: UICollectionView.CellRegistration<UICollectionViewListCell, InputNumberSetting> = CellManager.Settings.inputNumberCell
        let inputStringCell: UICollectionView.CellRegistration<UICollectionViewListCell, InputStringSetting> = CellManager.Settings.inputStringCell
        let segmentedCell: UICollectionView.CellRegistration<UICollectionViewListCell, SegmentedSetting> = CellManager.Settings.segmentedCell(self)
        let stepperCell: UICollectionView.CellRegistration<UICollectionViewListCell, StepperSetting> = CellManager.Settings.stepperCell
        let tapCell: UICollectionView.CellRegistration<UICollectionViewListCell, TapSetting> = CellManager.Settings.tapCell
        
        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            switch itemIdentifier {
            case let blankSetting as BlankSetting:
                collectionView.dequeueConfiguredReusableCell(using: blankSettingsCellRegistration, for: indexPath, item: blankSetting)
            case let boolSetting as BoolSetting:
                collectionView.dequeueConfiguredReusableCell(using: boolCell, for: indexPath, item: boolSetting)
            case let inputNumberSetting as InputNumberSetting:
                collectionView.dequeueConfiguredReusableCell(using: inputNumberCell, for: indexPath, item: inputNumberSetting)
            case let inputStringSetting as InputStringSetting:
                collectionView.dequeueConfiguredReusableCell(using: inputStringCell, for: indexPath, item: inputStringSetting)
            case let segmentedSetting as SegmentedSetting:
                collectionView.dequeueConfiguredReusableCell(using: segmentedCell, for: indexPath, item: segmentedSetting)
            case let stepperSetting as StepperSetting:
                collectionView.dequeueConfiguredReusableCell(using: stepperCell, for: indexPath, item: stepperSetting)
            case let selectionSetting as SelectionSetting:
                collectionView.dequeueConfiguredReusableCell(using: selectionCellRegistration, for: indexPath, item: selectionSetting)
            case let tapSetting as TapSetting:
                collectionView.dequeueConfiguredReusableCell(using: tapCell, for: indexPath, item: tapSetting)
            default:
                nil
            }
        }
        
        dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
            collectionView.dequeueConfiguredReusableSupplementary(using: headerCellRegistration, for: indexPath)
        }
        
        populateSettings()
    }
    
    func populateSettings() {
        snapshot = NSDiffableDataSourceSnapshot()
        snapshot.appendSections(CytrusSettingsHeaders.allCases)
        snapshot.sectionIdentifiers.forEach { header in
            snapshot.appendItems(CytrusSettingsItems.settings(header).map { item in
                item.setting(self)
            }, toSection: header)
        }
        
        Task {
            await dataSource.apply(snapshot)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.deselectItem(at: indexPath, animated: true)
        
        switch dataSource.itemIdentifier(for: indexPath) {
        case let inputSetting as InputNumberSetting:
            let alertController = UIAlertController(title: inputSetting.title,
                                                    message: "Min: \(Int(inputSetting.min)), Max: \(Int(inputSetting.max))",
                                                    preferredStyle: .alert)
            alertController.addTextField {
                $0.keyboardType = .numberPad
            }
            alertController.addAction(.init(title: "Cancel", style: .cancel))
            alertController.addAction(.init(title: "Save", style: .default, handler: { _ in
                guard let textFields = alertController.textFields, let textField = textFields.first, let value = textField.text as? NSString else {
                    return
                }
                
                UserDefaults.standard.set(value.doubleValue, forKey: inputSetting.key)
                if let delegate = inputSetting.delegate {
                    delegate.didChangeSetting(at: indexPath)
                }
            }))
            present(alertController, animated: true)
        case let inputSetting as InputStringSetting:
            let alertController = UIAlertController(title: inputSetting.title,
                                                    message: inputSetting.details,
                                                    preferredStyle: .alert)
            alertController.addTextField {
                $0.placeholder = inputSetting.placeholder
            }
            
            alertController.addAction(.init(title: "Cancel", style: .cancel))
            alertController.addAction(.init(title: "Save", style: .default, handler: { _ in
                guard let textFields = alertController.textFields, let textField = textFields.first, let value = textField.text else {
                    return
                }
                
                UserDefaults.standard.set(value, forKey: inputSetting.key)
                if let delegate = inputSetting.delegate {
                    inputSetting.action()
                    delegate.didChangeSetting(at: indexPath)
                }
            }))
            present(alertController, animated: true)
        case let tapSetting as TapSetting:
            tapSetting.handler(self)
        default:
            break
        }
    }
}

extension CytrusSettingsController : SettingDelegate {
    func didChangeSetting(at indexPath: IndexPath) {
        cytrus.updateSettings()
        
        guard let sectionIdentifier = dataSource.sectionIdentifier(for: indexPath.section) else {
            return
        }
        
        var snapshot = dataSource.snapshot()
        let item = snapshot.itemIdentifiers(inSection: sectionIdentifier)[indexPath.item]
        
        switch item {
        case let boolSetting as BoolSetting:
            boolSetting.value = UserDefaults.standard.bool(forKey: boolSetting.key)
        case let inputNumberSetting as InputNumberSetting:
            inputNumberSetting.value = UserDefaults.standard.double(forKey: inputNumberSetting.key)
        case let inputStringSetting as InputStringSetting:
            inputStringSetting.value = UserDefaults.standard.string(forKey: inputStringSetting.key)
        case let segmentedSetting as SegmentedSetting:
            segmentedSetting.selectedValue = UserDefaults.standard.value(forKey: segmentedSetting.key)
        case let stepperSetting as StepperSetting:
            stepperSetting.value = UserDefaults.standard.double(forKey: stepperSetting.key)
        case let selectionSetting as SelectionSetting:
            selectionSetting.selectedValue = UserDefaults.standard.value(forKey: selectionSetting.key)
        default:
            break
        }
        
        snapshot.reloadItems([item])
        Task {
            await dataSource.apply(snapshot, animatingDifferences: false)
        }
    }
}