import SwiftUI
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif

// MARK: - App-Icon-Loading (cross-platform, kein Extra-Asset)

extension Image {
    /// Lädt das echte AppIcon zur Laufzeit:
    /// iOS via `CFBundleIcons` aus der Info.plist, macOS via
    /// `NSApp.applicationIconImage`. Fällt auf ein SF-Symbol zurück wenn die
    /// Plattform-spezifische Auflösung leer ist (z.B. Preview ohne Bundle).
    static var appIcon: Image {
        #if os(iOS)
        if let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
           let primary = icons["CFBundlePrimaryIcon"] as? [String: Any],
           let files = primary["CFBundleIconFiles"] as? [String],
           let name = files.last,
           let ui = UIImage(named: name) {
            return Image(uiImage: ui)
        }
        return Image(systemName: "books.vertical.fill")
        #else
        if let ns = NSApp?.applicationIconImage {
            return Image(nsImage: ns)
        }
        return Image(systemName: "books.vertical.fill")
        #endif
    }
}

// MARK: - Color-Helper für hellere/dunklere Akzent-Töne

private extension Color {
    func lighter(by amount: CGFloat = 0.2) -> Color { adjustBrightness(by: amount) }
    func darker(by amount: CGFloat = 0.2) -> Color { adjustBrightness(by: -amount) }

    private func adjustBrightness(by amount: CGFloat) -> Color {
        var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
        #if os(iOS)
        UIColor(self).getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        #else
        (NSColor(self).usingColorSpace(.sRGB) ?? NSColor(self))
            .getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        #endif
        return Color(hue: Double(h),
                     saturation: Double(s),
                     brightness: Double(min(1.0, max(0.0, b + amount))),
                     opacity: Double(a))
    }
}

// MARK: - Partikel-Konfiguration

private struct ParticleConfig: Identifiable {
    let id: Int
    let startAngle: Double         // 0…2π — Position auf der Umlaufbahn bei t=0
    let orbitRadius: CGFloat       // 80…150pt — Radius der Umlaufbahn
    let size: CGFloat              // 4 / 7 / 10pt
    let appearDelay: Double        // 0…0.9s — Stagger-Fade-In
    let angularVelocity: Double    // rad/s — Umlaufgeschwindigkeit, Vorzeichen = Richtung
    let radialWobble: CGFloat      // 0…5pt — sanftes Atmen des Radius
    let wobbleFrequency: Double    // 0.4…0.8 Hz
    let wobblePhase: Double        // 0…2π
    let shade: ParticleShade
}

private enum ParticleShade {
    case light, accent, dark

    func color(base: Color) -> Color {
        switch self {
        case .light:  return base.lighter(by: 0.25)
        case .accent: return base
        case .dark:   return base.darker(by: 0.20)
        }
    }
}

/// Acht Partikel auf unterschiedlichen Umlaufbahnen. Reproduzierbar bei
/// jedem App-Start (kein Random). Drei Bahnen-Schichten (innen/mitte/außen)
/// plus gemischte Drehrichtungen (Vorzeichen von `angularVelocity`) damit
/// das Bild nicht wie ein gleichförmiges Karussell wirkt.
private let particleConfigs: [ParticleConfig] = [
    .init(id: 0, startAngle: 0.45, orbitRadius: 88,  size: 7,  appearDelay: 0.05, angularVelocity:  0.32, radialWobble: 3, wobbleFrequency: 0.7, wobblePhase: 0.3, shade: .light),
    .init(id: 1, startAngle: 1.20, orbitRadius: 130, size: 4,  appearDelay: 0.30, angularVelocity: -0.22, radialWobble: 4, wobbleFrequency: 0.5, wobblePhase: 1.1, shade: .accent),
    .init(id: 2, startAngle: 2.00, orbitRadius: 85,  size: 10, appearDelay: 0.55, angularVelocity:  0.18, radialWobble: 2, wobbleFrequency: 0.4, wobblePhase: 2.4, shade: .dark),
    .init(id: 3, startAngle: 2.80, orbitRadius: 115, size: 7,  appearDelay: 0.20, angularVelocity:  0.28, radialWobble: 5, wobbleFrequency: 0.6, wobblePhase: 0.8, shade: .accent),
    .init(id: 4, startAngle: 3.65, orbitRadius: 95,  size: 4,  appearDelay: 0.70, angularVelocity: -0.35, radialWobble: 3, wobbleFrequency: 0.55, wobblePhase: 3.2, shade: .light),
    .init(id: 5, startAngle: 4.40, orbitRadius: 140, size: 10, appearDelay: 0.10, angularVelocity: -0.16, radialWobble: 4, wobbleFrequency: 0.35, wobblePhase: 4.0, shade: .dark),
    .init(id: 6, startAngle: 5.20, orbitRadius: 100, size: 7,  appearDelay: 0.45, angularVelocity:  0.24, radialWobble: 3, wobbleFrequency: 0.65, wobblePhase: 5.1, shade: .light),
    .init(id: 7, startAngle: 5.80, orbitRadius: 120, size: 4,  appearDelay: 0.85, angularVelocity: -0.30, radialWobble: 5, wobbleFrequency: 0.45, wobblePhase: 1.6, shade: .accent),
]

// MARK: - SplashScreenView

/// Vierphasiger Splash:
/// 1. Partikel erscheinen gestaffelt um das Icon (~1.2s)
/// 2. Partikel driften organisch in Loop bis `isReady` true wird (min 0.5s)
/// 3. Alle Partikel werden ins Icon gesogen (0.6s) mit kleinem Icon-Wobble
/// 4. Flash-Ring + Icon-Scale-Pulse (0.4s, iOS auch Haptic)
/// 5. Fade-Out (0.4s) → `onComplete()`
struct SplashScreenView: View {
    let isReady: Bool
    let onComplete: () -> Void

    private enum Phase { case appearing, idle, attracting, flash, fading }

    @State private var phase: Phase = .appearing
    @State private var particlesShown: Bool = false
    @State private var particlesAttracted: Bool = false
    @State private var iconScale: CGFloat = 1.0
    @State private var iconWobble: CGFloat = 1.0
    @State private var iconBrightness: Double = 0
    @State private var ringScale: CGFloat = 1.0
    @State private var ringOpacity: Double = 0
    @State private var splashOpacity: Double = 1.0
    /// Spiegel von `isReady` mit `@State`-Storage. Nötig weil die `.task`-
    /// Closure die View-Struct beim ersten Erscheinen capturet und das
    /// `let isReady` aus dem alten Snapshot liest — das bleibt forever false
    /// auch wenn der Parent neu rendert. `@State`-Storage dagegen ist
    /// shared über Reconstructions hinweg, also sieht der laufende Task die
    /// Aktualisierung sobald `onChange` sie hier reinpiped.
    @State private var readySignaled: Bool = false

    /// Einmalig festgelegt — Basis für die TimelineView-Zeit. Als `@State`,
    /// damit der Wert bei View-Re-Renders aus ContentView nicht versehentlich
    /// auf „jetzt" zurückgesetzt wird und die Drift-Bewegung dadurch springt.
    @State private var referenceDate: Date = .init()

    var body: some View {
        ZStack {
            background
            ZStack {
                particleLayer
                iconLayer
                flashRing
            }
            .frame(width: 320, height: 320)
        }
        .opacity(splashOpacity)
        .task { await runSplashAnimation() }
        .onAppear {
            // Falls der Parent isReady bereits beim ersten Render gesetzt hat
            // (z.B. weil bootstrap super-schnell durchlief), spiegeln wir den
            // Anfangswert direkt — ohne dass eine onChange-Flanke nötig wäre.
            if isReady { readySignaled = true }
        }
        .onChange(of: isReady) { _, newValue in
            if newValue { readySignaled = true }
        }
    }

    // MARK: Background

    @ViewBuilder
    private var background: some View {
        #if os(iOS)
        Color(.systemBackground).ignoresSafeArea()
        #else
        Color(NSColor.windowBackgroundColor).ignoresSafeArea()
        #endif
    }

    // MARK: Icon

    private var iconLayer: some View {
        Image.appIcon
            .resizable()
            .frame(width: 96, height: 96)
            .clipShape(RoundedRectangle(cornerRadius: 21, style: .continuous))
            .shadow(color: .black.opacity(0.15), radius: 8, y: 3)
            .scaleEffect(iconScale * iconWobble)
            .brightness(iconBrightness)
    }

    // MARK: Flash-Ring

    private var flashRing: some View {
        Circle()
            .stroke(Color.white, lineWidth: 4)
            .frame(width: 96, height: 96)
            .scaleEffect(ringScale)
            .opacity(ringOpacity)
            .allowsHitTesting(false)
    }

    // MARK: Partikel

    @ViewBuilder
    private var particleLayer: some View {
        // TimelineView läuft während `.appearing` und `.idle`, damit die
        // Partikel schon beim Stagger-Fade-In leicht driften (sonst gäb's
        // einen sichtbaren Positions-Sprung beim Übergang zu `.idle`).
        // Pausiert ab `.attracting` — die gefrorene Position dient als
        // Startpunkt für die Attract-Animation Richtung .zero.
        let paused = (phase == .attracting || phase == .flash || phase == .fading)
        TimelineView(.animation(paused: paused)) { context in
            let t = context.date.timeIntervalSince(referenceDate)
            ZStack {
                ForEach(particleConfigs) { cfg in
                    particleView(cfg: cfg, t: t)
                }
            }
        }
    }

    @ViewBuilder
    private func particleView(cfg: ParticleConfig, t: TimeInterval) -> some View {
        let drifted = idlePosition(for: cfg, t: t)
        let position = particlesAttracted ? CGPoint.zero : drifted
        let scale: CGFloat = particlesAttracted ? 0.0 : 1.0

        // Wichtig: NICHT `Color.accentColor` (semantisch — konvertiert via
        // UIColor unzuverlässig zum echten Asset-Wert, gibt manchmal den
        // System-Tint = blau zurück). Direkt aus dem Asset-Catalog lesen.
        Circle()
            .fill(cfg.shade.color(base: Color("AccentColor")))
            .frame(width: cfg.size, height: cfg.size)
            .opacity(particlesShown ? 1.0 : 0.0)
            .scaleEffect(scale)
            .offset(x: position.x, y: position.y)
            // Stagger-Fade-In: jeder Partikel mit eigener Verzögerung
            .animation(.easeOut(duration: 0.5).delay(cfg.appearDelay), value: particlesShown)
            // Attract: 0.6s easeIn ins Zentrum, gleichzeitig auf scale=0
            .animation(.easeIn(duration: 0.6), value: particlesAttracted)
    }

    private func idlePosition(for cfg: ParticleConfig, t: TimeInterval) -> CGPoint {
        // Echte Umlaufbahn: Winkel wächst mit t (Vorzeichen = Drehrichtung).
        // Radius "atmet" leicht über sinusförmiges radialWobble — damit die
        // Bahn nicht maschinell perfekt-kreisförmig wirkt.
        let angle = cfg.startAngle + t * cfg.angularVelocity
        let wobble = sin(t * cfg.wobbleFrequency + cfg.wobblePhase) * Double(cfg.radialWobble)
        let radius = Double(cfg.orbitRadius) + wobble
        return CGPoint(x: cos(angle) * radius, y: sin(angle) * radius)
    }

    // MARK: Animation-Orchestrierung

    private func runSplashAnimation() async {
        // Phase 1a — Partikel erscheinen gestaffelt
        particlesShown = true
        // Warten bis alle Partikel sichtbar sind (max appearDelay 0.85 + fade 0.5)
        try? await Task.sleep(for: .milliseconds(1400))
        if Task.isCancelled { return }
        phase = .idle

        // Phase 1b — Drift-Loop bis readySignaled UND min 500ms in idle.
        // Wichtig: NICHT `isReady` lesen — siehe Doc-Kommentar an readySignaled.
        let idleStart = Date()
        let minIdleDuration: TimeInterval = 0.5
        while !readySignaled || Date().timeIntervalSince(idleStart) < minIdleDuration {
            try? await Task.sleep(for: .milliseconds(80))
            if Task.isCancelled { return }
        }

        // Phase 2 — Sog ins Icon-Zentrum
        phase = .attracting
        withAnimation(.easeIn(duration: 0.6)) {
            particlesAttracted = true
        }
        // Bei ~400ms in die Attract-Phase ein kleiner Icon-Wobble als die
        // Partikel ankommen — fühlt sich an wie der Impact.
        try? await Task.sleep(for: .milliseconds(400))
        if Task.isCancelled { return }
        withAnimation(.easeOut(duration: 0.1)) { iconWobble = 1.06 }
        try? await Task.sleep(for: .milliseconds(100))
        withAnimation(.easeIn(duration: 0.1)) { iconWobble = 1.0 }
        try? await Task.sleep(for: .milliseconds(100))
        if Task.isCancelled { return }

        // Phase 3 — Flash + Scale-Pulse + Haptic
        phase = .flash
        #if os(iOS)
        UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
        #endif
        // Ring: 1.0 → 2.5×, opacity 1 → 0 (0.4s easeOut)
        ringScale = 1.0
        ringOpacity = 1.0
        withAnimation(.easeOut(duration: 0.4)) {
            ringScale = 2.5
            ringOpacity = 0
        }
        // Icon-Brightness: schnell hoch, langsamer runter
        withAnimation(.easeOut(duration: 0.15)) { iconBrightness = 0.6 }
        // Parallel: Icon-Scale-Pulse 1.0 → 1.15 → 1.0
        withAnimation(.easeOut(duration: 0.2)) { iconScale = 1.15 }
        try? await Task.sleep(for: .milliseconds(200))
        if Task.isCancelled { return }
        withAnimation(.easeIn(duration: 0.25)) { iconBrightness = 0 }
        withAnimation(.easeIn(duration: 0.2)) { iconScale = 1.0 }
        try? await Task.sleep(for: .milliseconds(200))
        if Task.isCancelled { return }

        // Phase 4 — Fade
        phase = .fading
        withAnimation(.easeOut(duration: 0.4)) {
            splashOpacity = 0
        }
        try? await Task.sleep(for: .milliseconds(420))
        onComplete()
    }
}
