USB-Meter / USB Meter / Views / Meter / Tabs / ChargeRecord / MeterChargeRecordTabView.swift
1 contributor
515 lines | 25.003kb
//
//  MeterChargeRecordTabView.swift
//  USB Meter
//

import SwiftUI

struct MeterChargeRecordTabView: View {
    @EnvironmentObject private var appData: AppData
    @EnvironmentObject private var usbMeter: Meter
    @State private var chargedDeviceLibraryVisibility = false
    @State private var chargerLibraryVisibility = false
    @State private var checkpointEditorVisibility = false
    @State private var editingChargedDevice: ChargedDeviceSummary?
    @State private var targetNotificationEditorVisibility = false

    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                VStack(alignment: .leading, spacing: 8) {
                    HStack {
                        Text("Charge Record")
                            .font(.system(.title3, design: .rounded).weight(.bold))
                        Spacer()
                        Text(usbMeter.chargeRecordStatusText)
                            .font(.caption.weight(.bold))
                            .foregroundColor(usbMeter.chargeRecordStatusColor)
                            .padding(.horizontal, 10)
                            .padding(.vertical, 6)
                            .meterCard(
                                tint: usbMeter.chargeRecordStatusColor,
                                fillOpacity: 0.18,
                                strokeOpacity: 0.24,
                                cornerRadius: 999
                            )
                    }
                    Text("App-side charge accumulation based on the stop-threshold workflow.")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                }
                .frame(maxWidth: .infinity)
                .padding(18)
                .meterCard(tint: .pink, fillOpacity: 0.18, strokeOpacity: 0.24)

                chargedDeviceSection

                if let activeChargeSession {
                    chargeMonitorSection(activeChargeSession)
                }

                ChargeRecordMetricsTableView(
                    labels: ["Capacity", "Energy", "Duration", "Stop Threshold"],
                    values: [
                        "\(usbMeter.chargeRecordAH.format(decimalDigits: 3)) Ah",
                        "\(usbMeter.chargeRecordWH.format(decimalDigits: 3)) Wh",
                        usbMeter.chargeRecordDurationDescription,
                        "\(usbMeter.chargeRecordStopThreshold.format(decimalDigits: 2)) A"
                    ]
                )
                .padding(18)
                .meterCard(tint: .pink, fillOpacity: 0.14, strokeOpacity: 0.20)

                if usbMeter.chargeRecordTimeRange != nil {
                    VStack(alignment: .leading, spacing: 12) {
                        HStack {
                            Text("Charge Curve")
                                .font(.headline)
                            Spacer()
                            Button("Reset Graph") {
                                usbMeter.resetChargeRecordGraph()
                            }
                            .foregroundColor(.red)
                        }
                        MeasurementChartView(timeRange: usbMeter.chargeRecordTimeRange)
                            .environmentObject(usbMeter.measurements)
                            .frame(minHeight: 220)
                        Text("Reset Graph clears the current charge-record session and removes older shared samples that are no longer needed for this curve.")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }
                    .padding(18)
                    .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20)
                }

                VStack(alignment: .leading, spacing: 12) {
                    Text("Stop Threshold")
                        .font(.headline)
                    Slider(value: $usbMeter.chargeRecordStopThreshold, in: 0...0.30, step: 0.01)
                    Text("The app starts accumulating when current rises above this threshold and stops when it falls back to or below it.")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                    Button("Reset") {
                        usbMeter.resetChargeRecord()
                    }
                    .frame(maxWidth: .infinity)
                    .padding(.vertical, 10)
                    .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                    .buttonStyle(.plain)
                }
                .padding(18)
                .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)

                if usbMeter.supportsDataGroupCommands || usbMeter.recordedAH > 0 || usbMeter.recordedWH > 0 || usbMeter.recordingDuration > 0 {
                    VStack(alignment: .leading, spacing: 12) {
                        Text("Meter Totals")
                            .font(.headline)
                        ChargeRecordMetricsTableView(
                            labels: ["Capacity", "Energy", "Duration", "Meter Threshold"],
                            values: [
                                "\(usbMeter.recordedAH.format(decimalDigits: 3)) Ah",
                                "\(usbMeter.recordedWH.format(decimalDigits: 3)) Wh",
                                usbMeter.recordingDurationDescription,
                                usbMeter.supportsRecordingThreshold ? "\(usbMeter.recordingTreshold.format(decimalDigits: 2)) A" : "Read-only"
                            ]
                        )
                        Text("These values are reported by the meter for the active data group.")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                        if usbMeter.supportsDataGroupCommands {
                            Button("Reset Active Group") {
                                usbMeter.clear()
                            }
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 10)
                            .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                            .buttonStyle(.plain)
                        }
                    }
                    .padding(18)
                    .meterCard(tint: .teal, fillOpacity: 0.14, strokeOpacity: 0.20)
                }
            }
            .padding()
        }
        .background(
            LinearGradient(
                colors: [.pink.opacity(0.14), Color.clear],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()
        )
        .sheet(isPresented: $chargedDeviceLibraryVisibility) {
            ChargedDeviceLibrarySheetView(
                visibility: $chargedDeviceLibraryVisibility,
                meterMACAddress: usbMeter.btSerial.macAddress.description,
                meterTint: usbMeter.color,
                mode: .device
            )
            .environmentObject(appData)
        }
        .sheet(isPresented: $chargerLibraryVisibility) {
            ChargedDeviceLibrarySheetView(
                visibility: $chargerLibraryVisibility,
                meterMACAddress: usbMeter.btSerial.macAddress.description,
                meterTint: usbMeter.color,
                mode: .charger
            )
            .environmentObject(appData)
        }
        .sheet(isPresented: $checkpointEditorVisibility) {
            BatteryCheckpointEditorSheetView()
                .environmentObject(appData)
                .environmentObject(usbMeter)
        }
        .sheet(item: $editingChargedDevice) { chargedDevice in
            ChargedDeviceEditorSheetView(
                meterMACAddress: nil,
                chargedDevice: chargedDevice
            )
            .environmentObject(appData)
        }
        .sheet(isPresented: $targetNotificationEditorVisibility) {
            if let activeChargeSession {
                BatteryTargetNotificationEditorSheetView(
                    sessionID: activeChargeSession.id,
                    initialTargetPercent: activeChargeSession.targetBatteryPercent
                )
                .environmentObject(appData)
            }
        }
    }

    private var selectedChargedDevice: ChargedDeviceSummary? {
        appData.currentChargedDeviceSummary(for: usbMeter.btSerial.macAddress.description)
    }

    private var activeChargeSession: ChargeSessionSummary? {
        appData.activeChargeSessionSummary(for: usbMeter.btSerial.macAddress.description)
    }

    private var selectedCharger: ChargedDeviceSummary? {
        appData.currentChargerSummary(for: usbMeter.btSerial.macAddress.description)
    }

    private var chargedDeviceSection: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text("Device")
                    .font(.headline)
                Spacer()
                Button("Library") {
                    chargedDeviceLibraryVisibility = true
                }
            }

            if let selectedChargedDevice {
                HStack(alignment: .top, spacing: 14) {
                    ChargedDeviceQRCodeView(
                        qrIdentifier: selectedChargedDevice.qrIdentifier,
                        side: 88
                    )

                    VStack(alignment: .leading, spacing: 8) {
                        Label(selectedChargedDevice.name, systemImage: selectedChargedDevice.deviceClass.symbolName)
                            .font(.headline)

                        Text(selectedChargedDevice.deviceClass.title)
                            .font(.caption.weight(.semibold))
                            .foregroundColor(.secondary)

                        Text(selectedChargedDevice.supportsChargingWhileOff ? "Can finish charging while off" : "Needs on-state sessions to estimate capacity carefully")
                            .font(.caption2)
                            .foregroundColor(.secondary)

                        if selectedChargedDevice.supportedChargingModes.count == 1 {
                            Label(
                                "Charging via \(selectedChargedDevice.preferredChargingTransportMode.title)",
                                systemImage: selectedChargedDevice.preferredChargingTransportMode.symbolName
                            )
                            .font(.caption2)
                            .foregroundColor(.secondary)
                        } else {
                            Picker("Charging Type", selection: chargingTransportModeBinding(for: selectedChargedDevice)) {
                                ForEach(selectedChargedDevice.supportedChargingModes) { chargingTransportMode in
                                    Label(chargingTransportMode.title, systemImage: chargingTransportMode.symbolName)
                                        .tag(chargingTransportMode)
                                }
                            }
                            .pickerStyle(.segmented)
                        }

                        if let capacity = selectedChargedDevice.estimatedBatteryCapacityWh(for: effectiveChargingTransportMode(for: selectedChargedDevice)) {
                            Text("Estimated \(effectiveChargingTransportMode(for: selectedChargedDevice).title.lowercased()) capacity: \(capacity.format(decimalDigits: 2)) Wh")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        } else if let capacity = selectedChargedDevice.estimatedBatteryCapacityWh {
                            Text("Estimated capacity: \(capacity.format(decimalDigits: 2)) Wh")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }

                        if let minimumCurrent = selectedChargedDevice.resolvedCompletionCurrentAmps(for: effectiveChargingTransportMode(for: selectedChargedDevice)) {
                            Text("\(effectiveChargingTransportMode(for: selectedChargedDevice).title) completion current: \(minimumCurrent.format(decimalDigits: 2)) A")
                                .font(.caption2)
                                .foregroundColor(.secondary)
                        } else if let minimumCurrent = selectedChargedDevice.minimumCurrentAmps {
                            Text("Completion current: \(minimumCurrent.format(decimalDigits: 2)) A")
                                .font(.caption2)
                                .foregroundColor(.secondary)
                        }
                    }

                    Spacer(minLength: 0)
                }

                if shouldShowWirelessChargerSection(for: selectedChargedDevice) {
                    Divider()

                    VStack(alignment: .leading, spacing: 10) {
                        HStack {
                            Text("Wireless Charger")
                                .font(.subheadline.weight(.semibold))
                            Spacer()
                            Button(selectedCharger == nil ? "Select" : "Change") {
                                chargerLibraryVisibility = true
                            }
                        }

                        if let selectedCharger {
                            HStack(alignment: .top, spacing: 12) {
                                ChargedDeviceQRCodeView(
                                    qrIdentifier: selectedCharger.qrIdentifier,
                                    side: 62
                                )

                                VStack(alignment: .leading, spacing: 6) {
                                    Label(selectedCharger.name, systemImage: selectedCharger.deviceClass.symbolName)
                                        .font(.subheadline.weight(.semibold))

                                    if let chargerMaximumPowerWatts = selectedCharger.chargerMaximumPowerWatts {
                                        Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
                                            .font(.caption)
                                            .foregroundColor(.secondary)
                                    }

                                    if !selectedCharger.chargerObservedVoltageSelections.isEmpty {
                                        Text(
                                            "Observed voltages: " + selectedCharger.chargerObservedVoltageSelections
                                                .map { "\($0.format(decimalDigits: 1)) V" }
                                                .joined(separator: ", ")
                                        )
                                        .font(.caption2)
                                        .foregroundColor(.secondary)
                                    }
                                }
                            }
                        } else {
                            Text("Wireless sessions need a selected charger in addition to the charged device.")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                    }
                }

                Button("Add Battery Checkpoint") {
                    checkpointEditorVisibility = true
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .green, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)

                Button("Edit Device") {
                    editingChargedDevice = selectedChargedDevice
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)
            } else {
                Text("Select or create the device you are charging. New sessions, checkpoints, QR identity, capacity tracking, and curve learning are all anchored to that device.")
                    .font(.footnote)
                    .foregroundColor(.secondary)
            }
        }
        .padding(18)
        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    private func chargeMonitorSection(_ activeChargeSession: ChargeSessionSummary) -> some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Charging Monitor")
                .font(.headline)

            ChargeRecordMetricsTableView(
                labels: ["Source", "Energy", "Charge", "Stop Threshold"],
                values: [
                    activeChargeSession.sourceMode.title,
                    "\(activeChargeSession.measuredEnergyWh.format(decimalDigits: 3)) Wh",
                    "\(activeChargeSession.measuredChargeAh.format(decimalDigits: 3)) Ah",
                    "\(activeChargeSession.stopThresholdAmps.format(decimalDigits: 2)) A"
                ]
            )

            if let selectedChargedDevice,
               let batteryPrediction = selectedChargedDevice.batteryLevelPrediction(for: activeChargeSession) {
                VStack(alignment: .leading, spacing: 4) {
                    Text("Predicted battery level: \(batteryPrediction.predictedPercent.format(decimalDigits: 0))%")
                        .font(.caption.weight(.semibold))
                    Text(
                        "Anchored to \(batteryPrediction.anchorDescription) at \(batteryPrediction.anchorPercent.format(decimalDigits: 0))% using \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh estimated capacity."
                    )
                    .font(.caption2)
                    .foregroundColor(.secondary)
                }
            }

            if let targetBatteryPercent = activeChargeSession.targetBatteryPercent {
                Text("Target notification: \(targetBatteryPercent.format(decimalDigits: 0))%")
                    .font(.caption.weight(.semibold))
            } else {
                Text("No target battery notification configured.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            Button(activeChargeSession.targetBatteryPercent == nil ? "Set Target Notification" : "Change Target Notification") {
                targetNotificationEditorVisibility = true
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: .indigo, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)

            if activeChargeSession.targetBatteryPercent != nil {
                Button("Clear Target Notification") {
                    _ = appData.setTargetBatteryPercent(nil, for: activeChargeSession.id)
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14)
                .buttonStyle(.plain)
            }

            if activeChargeSession.requiresCompletionConfirmation {
                completionConfirmationCard(activeChargeSession)
            }

            if let capacityEstimateWh = activeChargeSession.capacityEstimateWh {
                Text("Current \(activeChargeSession.chargingTransportMode.title.lowercased()) capacity estimate: \(capacityEstimateWh.format(decimalDigits: 2)) Wh")
                    .font(.caption.weight(.semibold))
            }

            Label(
                "Session charging type: \(activeChargeSession.chargingTransportMode.title)",
                systemImage: activeChargeSession.chargingTransportMode.symbolName
            )
            .font(.caption)
            .foregroundColor(.secondary)

            if activeChargeSession.chargingTransportMode == .wireless {
                if let chargerID = activeChargeSession.chargerID,
                   let charger = appData.chargedDeviceSummary(id: chargerID) {
                    Label("Wireless charger: \(charger.name)", systemImage: "bolt.badge.clock")
                        .font(.caption)
                        .foregroundColor(.secondary)
                } else {
                    Text("No wireless charger is currently selected for this session.")
                        .font(.caption)
                        .foregroundColor(.orange)
                }
            }

            if activeChargeSession.checkpoints.isEmpty == false {
                VStack(alignment: .leading, spacing: 8) {
                    Text("Battery Checkpoints")
                        .font(.subheadline.weight(.semibold))

                    ForEach(activeChargeSession.checkpoints.suffix(4), id: \.id) { checkpoint in
                        HStack {
                            Text(checkpoint.timestamp.format())
                                .font(.caption2)
                                .foregroundColor(.secondary)
                            Spacer()
                            Text("\(checkpoint.batteryPercent.format(decimalDigits: 0))%")
                                .font(.caption.weight(.semibold))
                            Text("•")
                                .foregroundColor(.secondary)
                            Text("\(checkpoint.measuredEnergyWh.format(decimalDigits: 2)) Wh")
                                .font(.caption2)
                                .foregroundColor(.secondary)
                        }
                    }
                }
            }

            Text("The monitor prefers the meter's offline counters when available, then blends them with live samples so reconnects do not lose the real transferred energy.")
                .font(.footnote)
                .foregroundColor(.secondary)
        }
        .padding(18)
        .meterCard(tint: .green, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    private func completionConfirmationCard(_ activeChargeSession: ChargeSessionSummary) -> some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Completion Needs Confirmation")
                .font(.subheadline.weight(.semibold))

            if let contradictionPercent = activeChargeSession.completionContradictionPercent {
                Text("Current dropped below the stop threshold, but the estimated battery level is only \(contradictionPercent.format(decimalDigits: 0))%.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            } else {
                Text("Current dropped below the stop threshold, but the estimated battery level does not match a normal charge end.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            if let targetBatteryPercent = activeChargeSession.targetBatteryPercent {
                Text("The active target is \(targetBatteryPercent.format(decimalDigits: 0))%.")
                    .font(.caption2)
                    .foregroundColor(.secondary)
            }

            Button("Finish Session") {
                _ = appData.confirmChargeSessionCompletion(sessionID: activeChargeSession.id)
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)

            Button("Keep Monitoring") {
                _ = appData.continueChargeSessionMonitoring(sessionID: activeChargeSession.id)
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)
        }
        .padding(14)
        .meterCard(tint: .orange, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 16)
    }

    private func chargingTransportModeBinding(for chargedDevice: ChargedDeviceSummary) -> Binding<ChargingTransportMode> {
        Binding(
            get: {
                effectiveChargingTransportMode(for: chargedDevice)
            },
            set: { newValue in
                _ = appData.setChargingTransportMode(newValue, for: usbMeter)
            }
        )
    }

    private func effectiveChargingTransportMode(for chargedDevice: ChargedDeviceSummary) -> ChargingTransportMode {
        activeChargeSession?.chargingTransportMode ?? chargedDevice.preferredChargingTransportMode
    }

    private func shouldShowWirelessChargerSection(for chargedDevice: ChargedDeviceSummary) -> Bool {
        effectiveChargingTransportMode(for: chargedDevice) == .wireless
    }
}