//
//  ContentView.swift
//  Cue LIght
//
//  Created by Jay H on 8/27/26.
//
import AVFoundation
import Combine
import SwiftUI
import UIKit

enum Dest: String, CaseIterable, Identifiable {
    case youtube = "YouTube"
    case twitch = "Twitch"
    case tiktok = "TikTok"
    case x = "X"
    var id: String { rawValue }
    var hint: String {
        switch self {
        case .youtube: return "Long-form + Shorts"
        case .twitch: return "Chat-first"
        case .tiktok: return "Vertical by default"
        case .x: return "Live + clips"
        }
    }
}

enum Picture: String, CaseIterable {
    case walk = "Walk"
    case high = "High"
    case smooth = "Smooth"
    var line: String {
        switch self {
        case .walk: return "720p · 30 · longest walk"
        case .high: return "720p · 30 · YouTube IRL"
        case .smooth: return "720p · 30 · cap stays 30 so YouTube stays calm"
        }
    }
    var bitRate: Int {
        switch self {
        case .walk: return 1_800_000
        case .high, .smooth: return 2_000_000
        }
    }
    var byteLimit: Int {
        switch self {
        case .walk: return 225_000
        case .high, .smooth: return 250_000
        }
    }
}

private let shows: [(String, String)] = [
    ("Talking head", "Desk. You, full frame."),
    ("Game + cam", "Play fills the shot. You in the corner."),
    ("Just chat", "You and the room, side by side."),
    ("IRL", "The walk. Time and place on screen."),
    ("Be right back", "Hold the audience. No dead air."),
]

private let formats: [(String, String)] = [
    ("Landscape", "16:9 for YouTube and Twitch"),
    ("Vertical", "9:16 for TikTok and Shorts"),
    ("Dual output", "One show, both frames."),
]

private let glances: [(String, String, String)] = [
    ("SUPER", "for the next block", "nina"),
    ("ASK", "where's the next stop?", "devon"),
    ("NAME", "hey it's marcus from last week", "marcus"),
]

struct ContentView: View {
    @State private var step = 0
    @State private var dest: Set<Dest> = [.youtube]
    @State private var format = 0
    @State private var layout = 3
    @State private var picture = Picture.high
    @State private var onLens = false
    @StateObject private var ingest = Ingest()
    @StateObject private var chat = YouTubeChat()
    @AppStorage("cuelight.yt.key") private var ytKey = ""
    @AppStorage("cuelight.tw.key") private var twKey = ""
    @AppStorage("cuelight.x.key") private var xKey = ""
    @AppStorage("cuelight.tt.key") private var ttKey = ""
    @AppStorage("cuelight.x.url") private var xUrl = ""
    @AppStorage("cuelight.tt.url") private var ttUrl = ""
    @AppStorage("cuelight.panic") private var panicCopy = "I need a moment. Stay with the live."

    var body: some View {
        if onLens {
            LensView(
                format: format,
                layout: layout,
                picture: $picture,
                panicCopy: $panicCopy,
                ingest: ingest,
                chat: chat,
                targets: [
                    dest.contains(.youtube) ? IngestTarget(id: "youtube", name: "YouTube", url: IngestDefaults.youtube, key: ytKey) : nil,
                    dest.contains(.twitch) ? IngestTarget(id: "twitch", name: "Twitch", url: IngestDefaults.twitch, key: twKey) : nil,
                    dest.contains(.x) ? IngestTarget(id: "x", name: "X", url: xUrl, key: xKey) : nil,
                    dest.contains(.tiktok) ? IngestTarget(id: "tiktok", name: "TikTok", url: ttUrl, key: ttKey) : nil,
                ].compactMap { $0 },
                onExit: {
                    onLens = false
                    step = 0
                }
            )
        } else {
            setup
        }
    }

    private var setup: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text("Keys empty = practice. Filled keys go live.")
                .font(.caption)
                .foregroundStyle(.white.opacity(0.55))
                .frame(maxWidth: .infinity)
                .padding(.vertical, 8)
                .background(Color.white.opacity(0.04))

            HStack(alignment: .top, spacing: 12) {
                LampMark()
                VStack(alignment: .leading, spacing: 4) {
                    Text("CUE LIGHT")
                        .font(.caption)
                        .tracking(3)
                        .foregroundStyle(.white.opacity(0.45))
                    Text("Bringing light to the desk.")
                        .font(.title.bold())
                        .foregroundStyle(.white)
                }
            }
            .padding(.horizontal, 20)
            .padding(.top, 18)

            ScrollView {
                Group {
                    if step == 0 { whereStep }
                    if step == 1 { lookStep }
                    if step == 2 { showStep }
                    if step == 3 { walkStep }
                }
                .padding(20)
            }

            HStack {
                Button("Back") { step = max(0, step - 1) }
                    .disabled(step == 0)
                    .foregroundStyle(.white.opacity(step == 0 ? 0.3 : 0.8))
                Spacer()
                Button(step < 3 ? "Continue" : "Next") {
                    if step < 3 { step += 1 } else { onLens = true }
                }
                .font(.headline)
                .padding(.horizontal, 22)
                .padding(.vertical, 12)
                .background(Color(red: 0.77, green: 0.36, blue: 0.29))
                .foregroundStyle(.white)
                .clipShape(Capsule())
            }
            .padding(20)
        }
        .background(Color(red: 0.04, green: 0.04, blue: 0.04).ignoresSafeArea())
    }

    private var whereStep: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Where should this go?")
                .font(.title2.bold())
                .foregroundStyle(.white)
            Text("YouTube first. Other chips stay optional.")
                .font(.subheadline)
                .foregroundStyle(.white.opacity(0.55))
            LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
                ForEach(Dest.allCases) { d in
                    let on = dest.contains(d)
                    Button {
                        if on {
                            if dest.count > 1 { dest.remove(d) }
                        } else {
                            dest.insert(d)
                        }
                    } label: {
                        VStack(alignment: .leading, spacing: 8) {
                            ZStack {
                                Circle()
                                    .fill(on ? Color.white : Color.white.opacity(0.12))
                                    .frame(width: 22, height: 22)
                                if on {
                                    Image(systemName: "checkmark")
                                        .font(.system(size: 10, weight: .bold))
                                        .foregroundStyle(.black)
                                }
                            }
                            Text(d.rawValue)
                                .font(.headline)
                                .foregroundStyle(.white)
                            Text(d.hint)
                                .font(.caption)
                                .foregroundStyle(.white.opacity(0.5))
                        }
                        .padding(14)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .background(Color.white.opacity(on ? 0.12 : 0.05))
                        .clipShape(RoundedRectangle(cornerRadius: 14))
                    }
                }
            }
        }
    }

    private var lookStep: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("How should it look?")
                .font(.title2.bold())
                .foregroundStyle(.white)
            Text("Landscape is the walk. Dual is one program, two frames.")
                .font(.subheadline)
                .foregroundStyle(.white.opacity(0.55))
            ForEach(Array(formats.enumerated()), id: \.offset) { i, item in
                Button { format = i } label: {
                    HStack(alignment: .center, spacing: 16) {
                        FormatGlyph(id: i)
                            .frame(width: 92, height: 52, alignment: .bottomLeading)
                        VStack(alignment: .leading, spacing: 4) {
                            Text(item.0)
                                .font(.headline)
                                .foregroundStyle(.white)
                            Text(item.1)
                                .font(.caption)
                                .foregroundStyle(.white.opacity(0.5))
                        }
                        Spacer()
                    }
                    .padding(16)
                    .background(Color.white.opacity(format == i ? 0.14 : 0.05))
                    .clipShape(RoundedRectangle(cornerRadius: 14))
                }
            }
        }
    }

    private var showStep: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("What is the show?")
                .font(.title2.bold())
                .foregroundStyle(.white)
            Text("IRL is the walk. BRB keeps sending so YouTube does not quit.")
                .font(.subheadline)
                .foregroundStyle(.white.opacity(0.55))
            ForEach(Array(shows.enumerated()), id: \.offset) { i, item in
                Button { layout = i } label: {
                    VStack(alignment: .leading, spacing: 4) {
                        Text(item.0)
                            .font(.headline)
                            .foregroundStyle(.white)
                        Text(item.1)
                            .font(.caption)
                            .foregroundStyle(.white.opacity(0.5))
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .padding(14)
                    .background(Color.white.opacity(layout == i ? 0.14 : 0.05))
                    .clipShape(RoundedRectangle(cornerRadius: 14))
                }
            }
        }
    }

    private var walkStep: some View {
        VStack(alignment: .leading, spacing: 14) {
            Text("This phone is the walk.")
                .font(.title2.bold())
                .foregroundStyle(.white)
            Text("Walk saves battery. High is the IRL default. Smooth stays 30fps.")
                .foregroundStyle(.white.opacity(0.55))
            ForEach(Picture.allCases, id: \.self) { p in
                Button { picture = p } label: {
                    VStack(alignment: .leading, spacing: 4) {
                        Text(p.rawValue)
                            .font(.headline)
                            .foregroundStyle(.white)
                        Text(p.line)
                            .font(.caption)
                            .foregroundStyle(.white.opacity(0.5))
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .padding(14)
                    .background(Color.white.opacity(picture == p ? 0.14 : 0.05))
                    .clipShape(RoundedRectangle(cornerRadius: 14))
                }
            }
            Text("Panic line. Hold Panic. Tap OK to clear. Posts to YouTube live chat.")
                .font(.caption)
                .foregroundStyle(.white.opacity(0.45))
            TextField("Panic line", text: $panicCopy)
                .padding(10)
                .background(Color.white.opacity(0.08))
                .clipShape(RoundedRectangle(cornerRadius: 10))
                .foregroundStyle(.white)
            Text("Mute cuts the mic. YouTube claims are theirs, not Cue Light's.")
                .font(.caption)
                .foregroundStyle(.white.opacity(0.45))
            Button(chat.signedIn ? "YouTube chat connected" : "Connect YouTube chat") {
                Task { await chat.connect() }
            }
            .buttonStyle(CueButton(live: chat.signedIn))
            Text(chat.status)
                .font(.caption)
                .foregroundStyle(.white.opacity(0.45))
            keyRow("YouTube key", text: $ytKey)
            keyRow("Twitch key", text: $twKey)
            keyRow("X server URL", text: $xUrl)
            keyRow("X key", text: $xKey)
            keyRow("TikTok server URL", text: $ttUrl)
            keyRow("TikTok key", text: $ttKey)
        }
    }

    private func keyRow(_ title: String, text: Binding<String>) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.caption)
                .foregroundStyle(.white.opacity(0.45))
            SecureField(title, text: text)
                .textInputAutocapitalization(.never)
                .autocorrectionDisabled()
                .padding(10)
                .background(Color.white.opacity(0.08))
                .clipShape(RoundedRectangle(cornerRadius: 10))
                .foregroundStyle(.white)
        }
    }
}

struct FormatGlyph: View {
    var id: Int
    var body: some View {
        HStack(alignment: .bottom, spacing: 8) {
            if id != 1 {
                RoundedRectangle(cornerRadius: 3)
                    .fill(Color.white.opacity(0.22))
                    .frame(width: 56, height: 32)
            }
            if id != 0 {
                RoundedRectangle(cornerRadius: 3)
                    .fill(Color.white.opacity(id == 2 ? 0.4 : 0.22))
                    .frame(width: 22, height: 40)
            }
        }
    }
}

struct LampMark: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .stroke(Color.white.opacity(0.28), lineWidth: 1)
                .frame(width: 36, height: 36)
            VStack(spacing: 0) {
                Circle()
                    .fill(Color(red: 0.77, green: 0.36, blue: 0.29))
                    .frame(width: 8, height: 8)
                Rectangle()
                    .fill(Color.white.opacity(0.7))
                    .frame(width: 2, height: 12)
                RoundedRectangle(cornerRadius: 1)
                    .fill(Color.white.opacity(0.7))
                    .frame(width: 10, height: 2)
            }
            .offset(y: 2)
        }
    }
}

final class WalkPower: ObservableObject {
    private var savedBrightness: CGFloat?
    private var dimmed = false

    func live(_ on: Bool) {
        UIApplication.shared.isIdleTimerDisabled = on
        if !on { restoreScreen() }
    }

    func ear(_ on: Bool) {
        if on {
            if savedBrightness == nil {
                savedBrightness = UIScreen.main.brightness
            }
            UIScreen.main.brightness = min(savedBrightness ?? 0.3, 0.22)
            dimmed = true
        } else {
            restoreScreen()
        }
    }

    func restoreScreen() {
        if dimmed, let saved = savedBrightness {
            UIScreen.main.brightness = saved
        }
        dimmed = false
        savedBrightness = nil
        UIApplication.shared.isIdleTimerDisabled = false
    }
}

final class Lens: ObservableObject {
    @Published var world = true
    @Published var live = false
    @Published var muted = false
    @Published var message: String?
    var picture: Picture = .high
    let session = AVCaptureSession()
    private var audioConnection: AVCaptureConnection?
    private let queue = DispatchQueue(label: "live.cuelight.lens")

    func start() {
        queue.async { [weak self] in self?.configure() }
    }

    func stop() {
        queue.async { [weak self] in
            self?.session.stopRunning()
        }
    }

    func flip() {
        world.toggle()
        queue.async { [weak self] in self?.configure() }
    }

    func setLive(_ on: Bool) {
        live = on
        queue.async { [weak self] in self?.configure() }
    }

    func setPicture(_ picture: Picture) {
        self.picture = picture
        queue.async { [weak self] in self?.configure() }
    }

    func setMuted(_ on: Bool) {
        muted = on
        queue.async { [weak self] in
            self?.audioConnection?.isEnabled = !on
        }
    }

    private func configure() {
        session.beginConfiguration()
        session.inputs.forEach { session.removeInput($0) }
        session.outputs.forEach { session.removeOutput($0) }

        let preset: AVCaptureSession.Preset = .hd1280x720
        let fps: Int32 = 30
        if session.canSetSessionPreset(preset) {
            session.sessionPreset = preset
        }

        let position: AVCaptureDevice.Position = world ? .back : .front
        guard
            let device = AVCaptureDevice.default(
                .builtInWideAngleCamera,
                for: .video,
                position: position
            ) ?? AVCaptureDevice.default(for: .video),
            let input = try? AVCaptureDeviceInput(device: device)
        else {
            DispatchQueue.main.async { self.message = "No camera" }
            session.commitConfiguration()
            return
        }
        if session.canAddInput(input) {
            session.addInput(input)
        }

        if let mic = AVCaptureDevice.default(for: .audio),
           let micInput = try? AVCaptureDeviceInput(device: mic),
           session.canAddInput(micInput) {
            session.addInput(micInput)
        }

        try? device.lockForConfiguration()
        let duration = CMTime(value: 1, timescale: fps)
        device.activeVideoMinFrameDuration = duration
        device.activeVideoMaxFrameDuration = duration
        device.unlockForConfiguration()

        session.commitConfiguration()
        audioConnection = session.connections.first { $0.audioChannels.isEmpty == false }
        audioConnection?.isEnabled = !muted
        if let connection = session.connections.first(where: { $0.isVideoStabilizationSupported }) {
            connection.preferredVideoStabilizationMode = .off
        }
        if !session.isRunning {
            session.startRunning()
        }
        DispatchQueue.main.async { self.message = nil }
    }
}

struct PreviewView: UIViewRepresentable {
    let session: AVCaptureSession
    var mirror: Bool

    func makeUIView(context: Context) -> Preview {
        let v = Preview()
        v.previewLayer.session = session
        v.previewLayer.videoGravity = .resizeAspectFill
        return v
    }

    func updateUIView(_ uiView: Preview, context: Context) {
        uiView.previewLayer.connection?.automaticallyAdjustsVideoMirroring = false
        uiView.previewLayer.connection?.isVideoMirrored = mirror
    }

    final class Preview: UIView {
        override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
        var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
    }
}

final class Voice: ObservableObject {
    let synth = AVSpeechSynthesizer()
    func speak(_ text: String) {
        synth.stopSpeaking(at: .immediate)
        let utterance = AVSpeechUtterance(string: text)
        utterance.rate = 0.48
        synth.speak(utterance)
    }
    func stop() {
        synth.stopSpeaking(at: .immediate)
    }
}

struct LensView: View {
    var format: Int
    var layout: Int
    @Binding var picture: Picture
    @Binding var panicCopy: String
    @ObservedObject var ingest: Ingest
    @ObservedObject var chat: YouTubeChat
    var targets: [IngestTarget]
    var onExit: () -> Void
    @StateObject private var lens = Lens()
    @StateObject private var voice = Voice()
    @StateObject private var power = WalkPower()
    @State private var ended = false
    @State private var ear = false
    @State private var feed: [(String, String, String)] = []
    @State private var incoming = 0
    @State private var timer: Timer?

    private let cadence: TimeInterval = 4
    private let maxVisible = 3
    private let maxQueue = 8

    var body: some View {
        if ended {
            walkComplete
        } else {
            liveCamera
        }
    }

    private var visible: [(String, String, String)] {
        Array(feed.suffix(maxVisible).reversed())
    }

    private var walkComplete: some View {
        VStack(spacing: 18) {
            Spacer()
            Text("Walk complete.")
                .font(.largeTitle.bold())
                .foregroundStyle(.white)
            Text("If a key was live, YouTube has the replay.")
                .foregroundStyle(.white.opacity(0.55))
            Button("New walk") { onExit() }
                .buttonStyle(CueButton(live: true))
            Button("Open desk") {
                if let url = URL(string: "https://cuelight.live") {
                    UIApplication.shared.open(url)
                }
            }
            .buttonStyle(CueButton())
            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(red: 0.04, green: 0.04, blue: 0.04).ignoresSafeArea())
        .onAppear {
            timer?.invalidate()
            lens.stop()
            voice.stop()
            power.restoreScreen()
            chat.stopListening()
            Task { await ingest.stop() }
        }
    }

    private var liveCamera: some View {
        ZStack {
            Color(red: 0.04, green: 0.04, blue: 0.04).ignoresSafeArea()
            if ingest.pushing {
                MixerPreview(ingest: ingest)
                    .ignoresSafeArea()
            } else {
                PreviewView(session: lens.session, mirror: !lens.world)
                    .ignoresSafeArea()
            }

            if format == 2 {
                DualCropGuide()
            }

            if layout == 4 {
                Color.black.opacity(0.72).ignoresSafeArea()
                VStack(spacing: 8) {
                    Text("Back in a moment")
                        .font(.largeTitle.bold())
                        .foregroundStyle(.white)
                    Text("Be right back")
                        .foregroundStyle(.white.opacity(0.5))
                }
            }

            if ingest.panicking {
                Color.red.opacity(0.5).ignoresSafeArea()
                VStack(spacing: 12) {
                    Text(panicCopy)
                        .font(.title.bold())
                        .foregroundStyle(.white)
                        .multilineTextAlignment(.center)
                    Text(chat.status)
                        .font(.caption.bold())
                        .foregroundStyle(.white.opacity(0.85))
                }
                .padding(28)
            }

            VStack(alignment: .leading, spacing: 6) {
                Text(ingest.pushing
                    ? "Live. End to stop the pipe."
                    : "Practice. Empty keys stay off the internet.")
                    .font(.caption)
                    .foregroundStyle(.white.opacity(0.7))
                    .frame(maxWidth: .infinity)
                    .padding(.top, 8)
                if layout == 3 {
                    Text(timeStamp() + " · ON LOCATION · \(picture.rawValue.uppercased())")
                        .font(.caption2)
                        .tracking(1.4)
                        .foregroundStyle(.white.opacity(0.7))
                        .padding(.horizontal, 16)
                }
                if lens.muted {
                    Text("MIC MUTED")
                        .font(.caption.bold())
                        .foregroundStyle(Color(red: 0.77, green: 0.36, blue: 0.29))
                        .padding(.horizontal, 16)
                }
                if ingest.pushing {
                    Text(
                        ingest.reconnecting || ingest.signalLost
                            ? "RECONNECTING…"
                            : (ingest.liveNames.isEmpty ? "CONNECTING…" : ingest.liveNames.joined(separator: " · ").uppercased())
                    )
                    .font(.caption.bold())
                    .foregroundStyle(ingest.reconnecting || ingest.signalLost ? Color.red : .white)
                    .padding(.horizontal, 16)
                }
                if chat.signedIn, !ingest.reconnecting, !ingest.signalLost {
                    Text(chat.status)
                        .font(.caption)
                        .foregroundStyle(.white.opacity(0.55))
                        .padding(.horizontal, 16)
                }
                Spacer()
                if lens.live && layout != 4 && !visible.isEmpty {
                    VStack(alignment: .leading, spacing: 12) {
                        ForEach(Array(visible.enumerated()), id: \.offset) { i, line in
                            VStack(alignment: .leading, spacing: 2) {
                                Text(line.1)
                                    .font(i == 0 ? .title2.bold() : .subheadline.bold())
                                    .foregroundStyle(.white.opacity(i == 0 ? 1 : 0.7))
                                Text(line.2)
                                    .font(.caption)
                                    .foregroundStyle(.white.opacity(0.55))
                            }
                        }
                    }
                    .padding(.horizontal, 16)
                    .padding(.bottom, 8)
                }
                HStack(spacing: 8) {
                    if ingest.panicking {
                        Button("OK") {
                            Task { await ingest.panic(false, message: panicCopy) }
                        }
                        .buttonStyle(CueButton(danger: true))
                    } else {
                        Text("Panic")
                            .font(.subheadline.weight(.semibold))
                            .padding(.horizontal, 14)
                            .padding(.vertical, 12)
                            .background(Color.white.opacity(0.15))
                            .foregroundStyle(.white)
                            .clipShape(Capsule())
                            .onLongPressGesture(minimumDuration: 0.55) {
                                Task {
                                    await ingest.panic(true, message: panicCopy)
                                    await chat.shout(panicCopy)
                                }
                            }
                    }
                    Button(lens.world ? "World" : "You") {
                        lens.flip()
                        Task { if ingest.pushing { await ingest.flip(world: lens.world) } }
                    }
                    .buttonStyle(CueButton())
                    Button(picture.rawValue) { cyclePicture() }
                        .buttonStyle(CueButton())
                }
                .padding(.horizontal, 12)
                HStack(spacing: 10) {
                    Button(lens.muted ? "Unmute" : "Mute") {
                        lens.setMuted(!lens.muted)
                        Task { await ingest.mute(lens.muted) }
                    }
                    .buttonStyle(CueButton(danger: lens.muted))
                    Button(ear ? "Ear on" : "Ear") {
                        ear.toggle()
                        power.ear(ear)
                        if !ear {
                            voice.stop()
                        } else {
                            speakGlance()
                        }
                    }
                    .buttonStyle(CueButton(live: ear))
                    if lens.live {
                        Button("End") {
                            lens.setLive(false)
                            power.live(false)
                            chat.stopListening()
                            Task { await ingest.stop() }
                            ended = true
                        }
                        .buttonStyle(CueButton(danger: true))
                    } else {
                        Button("Go live") {
                            lens.setLive(true)
                            power.live(true)
                            Task {
                                let ready = targets.filter(\.ready)
                                if !ready.isEmpty {
                                    lens.stop()
                                    try? await Task.sleep(nanoseconds: 700_000_000)
                                    await ingest.start(
                                        targets: ready,
                                        world: lens.world,
                                        bitRate: picture.bitRate,
                                        byteLimit: picture.byteLimit
                                    )
                                    chat.startListening()
                                }
                            }
                        }
                        .buttonStyle(CueButton(live: true))
                    }
                }
                .padding(.bottom, 28)
                .padding(.horizontal, 12)
            }

            if lens.live && layout != 4 && !ingest.reconnecting && !ingest.signalLost {
                VStack {
                    HStack {
                        Circle()
                            .fill(Color(red: 0.77, green: 0.36, blue: 0.29))
                            .frame(width: 8, height: 8)
                        Text("LIVE")
                            .font(.caption.bold())
                            .foregroundStyle(.white)
                        Spacer()
                    }
                    .padding()
                    Spacer()
                }
            }
        }
        .onAppear {
            lens.picture = picture
            lens.start()
        }
        .onDisappear {
            timer?.invalidate()
            power.restoreScreen()
        }
        .onChange(of: lens.live) { _, live in
            timer?.invalidate()
            if live {
                takeNext()
                timer = Timer.scheduledTimer(withTimeInterval: cadence, repeats: true) { _ in
                    takeNext()
                }
            }
        }
    }

    private func cyclePicture() {
        switch picture {
        case .walk: picture = .high
        case .high: picture = .smooth
        case .smooth: picture = .walk
        }
        lens.setPicture(picture)
    }

    private func takeNext() {
        let next: (String, String, String)
        if let live = chat.take() {
            next = live
        } else if chat.signedIn {
            return
        } else {
            next = glances[incoming % glances.count]
            incoming += 1
        }
        var nextFeed = feed
        nextFeed.append(next)
        if nextFeed.count > maxQueue {
            nextFeed.removeFirst(nextFeed.count - maxQueue)
        }
        feed = nextFeed
        if ear {
            voice.speak("\(next.2). \(next.1)")
        }
    }

    private func speakGlance() {
        guard let newest = feed.last else { return }
        voice.speak("\(newest.2). \(newest.1)")
    }

    private func timeStamp() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "h:mm a"
        return formatter.string(from: Date()).uppercased()
    }
}

struct DualCropGuide: View {
    var body: some View {
        GeometryReader { geo in
            let h = geo.size.height * 0.86
            let w = h * 9 / 16
            RoundedRectangle(cornerRadius: 10)
                .stroke(Color.white.opacity(0.35), lineWidth: 1)
                .frame(width: min(w, geo.size.width * 0.42), height: h)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
                .padding(.trailing, 12)
        }
        .allowsHitTesting(false)
    }
}

struct CueButton: ButtonStyle {
    var live = false
    var danger = false

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.subheadline.weight(.semibold))
            .padding(.horizontal, 14)
            .padding(.vertical, 12)
            .background(
                danger
                    ? Color.red.opacity(0.85)
                    : live
                    ? Color(red: 0.77, green: 0.36, blue: 0.29)
                    : Color.white.opacity(0.15)
            )
            .foregroundStyle(.white)
            .clipShape(Capsule())
            .opacity(configuration.isPressed ? 0.7 : 1)
    }
}
