USB-Meter / USB Meter / Views / ChargedDevices / ChargedDeviceEditorSheetView.swift
1 contributor
355 lines | 14.74kb
//
//  ChargedDeviceEditorSheetView.swift
//  USB Meter
//
//  Created by Codex on 10/04/2026.
//

import SwiftUI

struct ChargedDeviceEditorSheetView: View {
    @EnvironmentObject private var appData: AppData
    @Environment(\.dismiss) private var dismiss

    let meterMACAddress: String?
    let chargedDevice: ChargedDeviceSummary?
    let suggestedDeviceClass: ChargedDeviceClass?

    @State private var name: String
    @State private var deviceClass: ChargedDeviceClass
    @State private var chargingStateAvailability: ChargingStateAvailability
    @State private var supportsWiredCharging: Bool
    @State private var supportsWirelessCharging: Bool
    @State private var preferredChargingTransportMode: ChargingTransportMode
    @State private var wirelessChargingProfile: WirelessChargingProfile
    @State private var completionCurrentTexts: [ChargeSessionKind: String]
    @State private var notes: String

    init(
        meterMACAddress: String?,
        chargedDevice: ChargedDeviceSummary? = nil,
        suggestedDeviceClass: ChargedDeviceClass? = nil
    ) {
        self.meterMACAddress = meterMACAddress
        self.chargedDevice = chargedDevice
        self.suggestedDeviceClass = suggestedDeviceClass

        let initialDeviceClass = chargedDevice?.deviceClass ?? suggestedDeviceClass ?? .iphone
        _name = State(initialValue: chargedDevice?.name ?? "")
        _deviceClass = State(initialValue: initialDeviceClass)
        _chargingStateAvailability = State(initialValue: chargedDevice?.chargingStateAvailability ?? Self.suggestedChargingStateAvailability(for: initialDeviceClass))
        _supportsWiredCharging = State(initialValue: chargedDevice?.supportsWiredCharging ?? true)
        _supportsWirelessCharging = State(initialValue: chargedDevice?.supportsWirelessCharging ?? true)
        _preferredChargingTransportMode = State(initialValue: chargedDevice?.preferredChargingTransportMode ?? .wired)
        _wirelessChargingProfile = State(initialValue: chargedDevice?.wirelessChargingProfile ?? .genericQi)
        _completionCurrentTexts = State(
            initialValue: Self.makeCompletionCurrentTexts(for: chargedDevice)
        )
        _notes = State(initialValue: chargedDevice?.notes ?? "")
    }

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Identity")) {
                    TextField("Name", text: $name)

                    Picker("Class", selection: $deviceClass) {
                        ForEach(ChargedDeviceClass.allCases) { deviceClass in
                            Label(deviceClass.title, systemImage: deviceClass.symbolName)
                                .tag(deviceClass)
                        }
                    }

                    if let chargedDevice {
                        Text(chargedDevice.qrIdentifier)
                            .font(.caption.monospaced())
                            .foregroundColor(.secondary)
                            .textSelection(.enabled)
                    }
                }

                Section(header: Text("Charge Behaviour")) {
                    Picker("Session Modes", selection: $chargingStateAvailability) {
                        ForEach(ChargingStateAvailability.allCases) { availability in
                            Text(availability.title)
                                .tag(availability)
                        }
                    }

                    Text(chargingStateAvailability.description)
                        .font(.footnote)
                        .foregroundColor(.secondary)
                }

                Section(header: Text("Charging Support")) {
                    Toggle("Supports wired charging", isOn: $supportsWiredCharging)
                    Toggle("Supports wireless charging", isOn: $supportsWirelessCharging)

                    if supportsWirelessCharging {
                        Picker("Wireless profile", selection: $wirelessChargingProfile) {
                            ForEach(WirelessChargingProfile.allCases) { profile in
                                Text(profile.title)
                                    .tag(profile)
                            }
                        }

                        Text(wirelessChargingProfile.description)
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }

                    if !supportedChargingModes.isEmpty {
                        Picker("Default session type", selection: preferredChargingTransportBinding) {
                            ForEach(supportedChargingModes) { chargingTransportMode in
                                Label(chargingTransportMode.title, systemImage: chargingTransportMode.symbolName)
                                    .tag(chargingTransportMode)
                            }
                        }
                    } else {
                        Text("Enable at least one charging method.")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }
                }

                Section(header: Text("Charge Completion")) {
                    if applicableSessionKinds.isEmpty {
                        Text("Enable at least one charging method to configure stop currents.")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    } else {
                        ForEach(applicableSessionKinds) { sessionKind in
                            VStack(alignment: .leading, spacing: 6) {
                                TextField(
                                    "\(sessionKind.shortTitle) completion current (A)",
                                    text: completionCurrentTextBinding(for: sessionKind)
                                )
                                .keyboardType(.decimalPad)

                                Text("Leave empty to keep learning this threshold from sessions of the same type.")
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                            }
                            .padding(.vertical, 2)
                        }
                    }
                }

                Section(header: Text("Notes")) {
                    TextField("Optional notes", text: $notes)
                }
            }
            .navigationTitle(editorTitle)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button(saveButtonTitle) {
                        let configuredCompletionCurrents = parsedCompletionCurrents
                        let didSave: Bool
                        if let chargedDevice {
                            didSave = appData.updateChargedDevice(
                                id: chargedDevice.id,
                                name: name,
                                deviceClass: deviceClass,
                                chargingStateAvailability: chargingStateAvailability,
                                supportsWiredCharging: supportsWiredCharging,
                                supportsWirelessCharging: supportsWirelessCharging,
                                preferredChargingTransportMode: resolvedPreferredChargingTransportMode,
                                wirelessChargingProfile: wirelessChargingProfile,
                                configuredCompletionCurrents: configuredCompletionCurrents,
                                notes: notes
                            )
                        } else {
                            didSave = appData.createChargedDevice(
                                name: name,
                                deviceClass: deviceClass,
                                chargingStateAvailability: chargingStateAvailability,
                                supportsWiredCharging: supportsWiredCharging,
                                supportsWirelessCharging: supportsWirelessCharging,
                                preferredChargingTransportMode: resolvedPreferredChargingTransportMode,
                                wirelessChargingProfile: wirelessChargingProfile,
                                configuredCompletionCurrents: configuredCompletionCurrents,
                                notes: notes,
                                meterMACAddress: meterMACAddress
                            )
                        }

                        if didSave {
                            dismiss()
                        }
                    }
                    .disabled(
                        name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                            || (!supportsWiredCharging && !supportsWirelessCharging)
                            || hasInvalidCompletionCurrentEntry
                    )
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
        .onChange(of: deviceClass) { newValue in
            applySuggestedChargingSupport(for: newValue)
        }
        .onAppear {
            guard chargedDevice == nil else {
                return
            }
            applySuggestedChargingSupport(for: deviceClass)
        }
    }

    private var editorTitle: String {
        if chargedDevice == nil {
            return deviceClass == .charger ? "New Charger" : "New Device"
        }
        return chargedDevice?.isCharger == true ? "Edit Charger" : "Edit Device"
    }

    private var saveButtonTitle: String {
        chargedDevice == nil ? "Save" : "Update"
    }

    private var supportedChargingModes: [ChargingTransportMode] {
        var modes: [ChargingTransportMode] = []
        if supportsWiredCharging {
            modes.append(.wired)
        }
        if supportsWirelessCharging {
            modes.append(.wireless)
        }
        return modes
    }

    private var applicableSessionKinds: [ChargeSessionKind] {
        supportedChargingModes.flatMap { chargingTransportMode in
            chargingStateAvailability.supportedModes.map { chargingStateMode in
                ChargeSessionKind(
                    chargingTransportMode: chargingTransportMode,
                    chargingStateMode: chargingStateMode
                )
            }
        }
    }

    private var parsedCompletionCurrents: [ChargeSessionKind: Double] {
        applicableSessionKinds.reduce(into: [ChargeSessionKind: Double]()) { result, sessionKind in
            guard let value = parsedOptionalCurrent(completionCurrentTexts[sessionKind] ?? "") else {
                return
            }
            result[sessionKind] = value
        }
    }

    private var hasInvalidCompletionCurrentEntry: Bool {
        applicableSessionKinds.contains { sessionKind in
            let text = completionCurrentTexts[sessionKind] ?? ""
            let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines)
            return !normalized.isEmpty && parsedOptionalCurrent(text) == nil
        }
    }

    private var resolvedPreferredChargingTransportMode: ChargingTransportMode {
        if supportedChargingModes.contains(preferredChargingTransportMode) {
            return preferredChargingTransportMode
        }
        return supportsWiredCharging ? .wired : .wireless
    }

    private var preferredChargingTransportBinding: Binding<ChargingTransportMode> {
        Binding(
            get: { resolvedPreferredChargingTransportMode },
            set: { preferredChargingTransportMode = $0 }
        )
    }

    private func completionCurrentTextBinding(for sessionKind: ChargeSessionKind) -> Binding<String> {
        Binding(
            get: { completionCurrentTexts[sessionKind] ?? "" },
            set: { completionCurrentTexts[sessionKind] = $0 }
        )
    }

    private func applySuggestedChargingSupport(for deviceClass: ChargedDeviceClass) {
        if chargedDevice != nil {
            return
        }

        chargingStateAvailability = Self.suggestedChargingStateAvailability(for: deviceClass)

        switch deviceClass {
        case .iphone:
            supportsWiredCharging = true
            supportsWirelessCharging = true
            preferredChargingTransportMode = .wired
        case .watch:
            supportsWiredCharging = false
            supportsWirelessCharging = true
            preferredChargingTransportMode = .wireless
        case .powerbank:
            supportsWiredCharging = true
            supportsWirelessCharging = false
            preferredChargingTransportMode = .wired
        case .charger:
            supportsWiredCharging = true
            supportsWirelessCharging = true
            preferredChargingTransportMode = .wireless
        case .other:
            supportsWiredCharging = true
            supportsWirelessCharging = false
            preferredChargingTransportMode = .wired
        }
    }

    private func parsedOptionalCurrent(_ text: String) -> Double? {
        let normalized = text
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .replacingOccurrences(of: ",", with: ".")
        guard !normalized.isEmpty else {
            return nil
        }
        guard let value = Double(normalized), value > 0 else {
            return nil
        }
        return value
    }

    private static func makeCompletionCurrentTexts(for chargedDevice: ChargedDeviceSummary?) -> [ChargeSessionKind: String] {
        guard let chargedDevice else {
            return [:]
        }

        return ChargeSessionKind.allCases.reduce(into: [ChargeSessionKind: String]()) { result, sessionKind in
            result[sessionKind] = optionalCurrentText(
                chargedDevice.configuredCompletionCurrentAmps(for: sessionKind)
            )
        }
    }

    private static func optionalCurrentText(_ value: Double?) -> String {
        guard let value else {
            return ""
        }
        return value.format(decimalDigits: 2)
    }

    private static func suggestedChargingStateAvailability(for deviceClass: ChargedDeviceClass) -> ChargingStateAvailability {
        switch deviceClass {
        case .iphone:
            return .onOrOff
        case .watch:
            return .onOnly
        case .powerbank:
            return .offOnly
        case .charger:
            return .onOnly
        case .other:
            return .onOnly
        }
    }
}