//
//  LevelGenerator.swift
//  DashSmash
//
//  Builds a deterministic Level from a song + band string.
//
//  The seed first picks a LevelStyle, which controls density, section count,
//  section length, mode preferences, and which obstacle "phrases" can appear.
//  Two songs with the same style will still look different — within the style,
//  every concrete choice (which phrase, which mode, how long each section,
//  where rests go) is drawn from the seeded RNG.
//

import Foundation
import CoreGraphics

// MARK: - Seeded RNG

struct SeededRNG: RandomNumberGenerator {
    private var state: UInt64

    init(seed: UInt64) {
        self.state = seed == 0 ? 0x9E3779B97F4A7C15 : seed
    }

    init(string: String) {
        var h: UInt64 = 0xcbf29ce484222325 // FNV-1a 64
        for byte in string.lowercased().utf8 {
            h ^= UInt64(byte)
            h = h &* 0x100000001b3
        }
        self.init(seed: h)
    }

    mutating func next() -> UInt64 {
        state = state &+ 0x9E3779B97F4A7C15
        var z = state
        z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9
        z = (z ^ (z >> 27)) &* 0x94D049BB133111EB
        return z ^ (z >> 31)
    }

    mutating func intInRange(_ range: ClosedRange<Int>) -> Int {
        Int.random(in: range, using: &self)
    }

    mutating func doubleInRange(_ range: ClosedRange<Double>) -> Double {
        Double.random(in: range, using: &self)
    }

    mutating func coin(_ p: Double) -> Bool {
        Double.random(in: 0...1, using: &self) < p
    }

    mutating func pick<T>(_ array: [T]) -> T {
        array[Int.random(in: 0..<array.count, using: &self)]
    }

    mutating func weightedPick<T>(_ items: [(T, Double)]) -> T {
        let total = items.reduce(0.0) { $0 + $1.1 }
        var r = Double.random(in: 0..<total, using: &self)
        for (val, w) in items {
            if r < w { return val }
            r -= w
        }
        return items.last!.0
    }
}

// MARK: - Phrases

/// One obstacle inside a phrase template, in phrase-local beat space.
struct PhraseObstacle {
    let kind: ObstacleKind
    let beatOffset: Double
    let yOffset: CGFloat
    let width: CGFloat
    let height: CGFloat
}

/// A reusable rhythmic obstacle pattern for a specific game mode.
struct ObstaclePhrase {
    let name: String
    /// Modes this phrase is valid in.
    let modes: Set<GameMode>
    /// Total length of the phrase in beats (including trailing breathing room).
    let beats: Double
    let obstacles: [PhraseObstacle]
}

enum PhraseLibrary {
    static let all: [ObstaclePhrase] = [
        // Cube / Mini — spike combos
        ObstaclePhrase(name: "single_spike", modes: [.cube, .mini], beats: 2, obstacles: [
            PhraseObstacle(kind: .spike, beatOffset: 0, yOffset: 0, width: 30, height: 30)
        ]),
        ObstaclePhrase(name: "double_spike", modes: [.cube, .mini], beats: 2, obstacles: [
            PhraseObstacle(kind: .spike, beatOffset: 0,   yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .spike, beatOffset: 0.4, yOffset: 0, width: 30, height: 30),
        ]),
        // Name retained for phraseBias compatibility, but trimmed to two
        // spikes — three-in-a-row was unfair in playtesting because the second
        // jump apex always landed on the third spike.
        ObstaclePhrase(name: "triple_spike", modes: [.cube, .mini], beats: 3, obstacles: [
            PhraseObstacle(kind: .spike, beatOffset: 0,   yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .spike, beatOffset: 0.5, yOffset: 0, width: 30, height: 30),
        ]),
        ObstaclePhrase(name: "spike_block", modes: [.cube], beats: 3, obstacles: [
            PhraseObstacle(kind: .spike, beatOffset: 0,   yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .block, beatOffset: 1.0, yOffset: 0, width: 90, height: 60),
        ]),
        ObstaclePhrase(name: "stair_up", modes: [.cube], beats: 4, obstacles: [
            PhraseObstacle(kind: .block, beatOffset: 0,   yOffset: 0,   width: 80, height: 40),
            PhraseObstacle(kind: .block, beatOffset: 1.0, yOffset: 0,   width: 80, height: 80),
            PhraseObstacle(kind: .block, beatOffset: 2.0, yOffset: 0,   width: 80, height: 120),
        ]),
        ObstaclePhrase(name: "high_block", modes: [.cube], beats: 3, obstacles: [
            PhraseObstacle(kind: .block, beatOffset: 0, yOffset: 0, width: 120, height: 90),
        ]),
        ObstaclePhrase(name: "kick_wall", modes: [.cube, .mini], beats: 2, obstacles: [
            PhraseObstacle(kind: .block, beatOffset: 0, yOffset: 0, width: 54, height: 50),
        ]),
        ObstaclePhrase(name: "snare_orb", modes: [.cube, .mini], beats: 2, obstacles: [
            PhraseObstacle(kind: .beatOrb, beatOffset: 0, yOffset: 126, width: 38, height: 38),
        ]),
        // A 1-beat gap between the floating spike and the ground spike was
        // tight enough that the jump arc clipped the floater; pushed to 1.8
        // beats so the player has time to land between them.
        ObstaclePhrase(name: "hat_floater", modes: [.cube, .mini], beats: 4, obstacles: [
            PhraseObstacle(kind: .floatingSpike, beatOffset: 0, yOffset: 150, width: 34, height: 34),
            PhraseObstacle(kind: .spike, beatOffset: 1.8, yOffset: 0, width: 26, height: 26),
        ]),
        // Same trim: pairs only, no triples.
        ObstaclePhrase(name: "mini_burst", modes: [.mini], beats: 2, obstacles: [
            PhraseObstacle(kind: .spike, beatOffset: 0,    yOffset: 0, width: 22, height: 22),
            PhraseObstacle(kind: .spike, beatOffset: 0.4,  yOffset: 0, width: 22, height: 22),
        ]),

        // Ship phrases
        ObstaclePhrase(name: "ship_floor_spike", modes: [.ship], beats: 2, obstacles: [
            PhraseObstacle(kind: .spike, beatOffset: 0, yOffset: 0, width: 30, height: 30),
        ]),
        ObstaclePhrase(name: "ship_ceiling_spike", modes: [.ship], beats: 2, obstacles: [
            PhraseObstacle(kind: .ceilingSpike, beatOffset: 0, yOffset: 0, width: 30, height: 30),
        ]),
        ObstaclePhrase(name: "ship_zigzag", modes: [.ship], beats: 4, obstacles: [
            PhraseObstacle(kind: .spike,        beatOffset: 0,   yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .ceilingSpike, beatOffset: 1.0, yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .spike,        beatOffset: 2.0, yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .ceilingSpike, beatOffset: 3.0, yOffset: 0, width: 30, height: 30),
        ]),
        ObstaclePhrase(name: "ship_threader", modes: [.ship], beats: 3, obstacles: [
            PhraseObstacle(kind: .block, beatOffset: 0, yOffset: 120, width: 100, height: 24),
        ]),
        ObstaclePhrase(name: "ship_kick_gate", modes: [.ship], beats: 4, obstacles: [
            PhraseObstacle(kind: .ceilingBlock, beatOffset: 0, yOffset: 0, width: 84, height: 120),
            PhraseObstacle(kind: .block, beatOffset: 2.0, yOffset: 0, width: 84, height: 120),
        ]),
        ObstaclePhrase(name: "ship_snare_orbs", modes: [.ship], beats: 4, obstacles: [
            PhraseObstacle(kind: .beatOrb, beatOffset: 0, yOffset: 112, width: 36, height: 36),
            PhraseObstacle(kind: .beatOrb, beatOffset: 2.0, yOffset: 210, width: 36, height: 36),
        ]),

        // Gravity phrases
        ObstaclePhrase(name: "ceiling_spike", modes: [.gravity], beats: 2, obstacles: [
            PhraseObstacle(kind: .ceilingSpike, beatOffset: 0, yOffset: 0, width: 30, height: 30),
        ]),
        ObstaclePhrase(name: "ceiling_double", modes: [.gravity], beats: 2, obstacles: [
            PhraseObstacle(kind: .ceilingSpike, beatOffset: 0,   yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .ceilingSpike, beatOffset: 0.5, yOffset: 0, width: 30, height: 30),
        ]),
        ObstaclePhrase(name: "gravity_mixed", modes: [.gravity], beats: 3, obstacles: [
            PhraseObstacle(kind: .ceilingSpike, beatOffset: 0,   yOffset: 0, width: 30, height: 30),
            PhraseObstacle(kind: .spike,        beatOffset: 1.0, yOffset: 0, width: 30, height: 30),
        ]),
        ObstaclePhrase(name: "gravity_kick_gate", modes: [.gravity], beats: 3, obstacles: [
            PhraseObstacle(kind: .ceilingBlock, beatOffset: 0, yOffset: 0, width: 70, height: 70),
        ]),
        ObstaclePhrase(name: "gravity_hat_orb", modes: [.gravity], beats: 2, obstacles: [
            PhraseObstacle(kind: .beatOrb, beatOffset: 0, yOffset: 138, width: 36, height: 36),
        ]),
    ]

    static func phrases(for mode: GameMode) -> [ObstaclePhrase] {
        all.filter { $0.modes.contains(mode) }
    }
}

// MARK: - Style profiles

struct StyleProfile {
    /// Number of sections to generate.
    let sectionCountRange: ClosedRange<Int>
    /// Section length (beats).
    let sectionBeatRange: ClosedRange<Double>
    /// Rest beats between phrases.
    let restBeatsRange: ClosedRange<Double>
    /// Bpm range.
    let bpmRange: ClosedRange<Double>
    /// Mode weighting.
    let modeWeights: [(GameMode, Double)]
    /// Whether intensity grows over the level (more phrases, less rest).
    let escalating: Bool
    /// Pointer to a phrase-name pattern preference. Returns weight for a phrase name.
    let phraseBias: (String) -> Double
}

extension LevelStyle {
    var profile: StyleProfile {
        switch self {
        case .chill:
            return StyleProfile(
                sectionCountRange: 3...4,
                sectionBeatRange: 24...40,
                restBeatsRange: 2.0...4.0,
                bpmRange: 70...100,
                modeWeights: [(.cube, 5), (.ship, 1), (.gravity, 0.5), (.mini, 0.3)],
                escalating: false,
                phraseBias: { name in
                    switch name {
                    case "single_spike", "spike_block": return 3
                    case "double_spike", "high_block", "kick_wall", "snare_orb": return 1
                    case "triple_spike", "mini_burst", "hat_floater": return 0.3
                    default: return 1
                    }
                })
        case .dancey:
            return StyleProfile(
                sectionCountRange: 4...6,
                sectionBeatRange: 16...32,
                restBeatsRange: 1.0...2.0,
                bpmRange: 110...130,
                modeWeights: [(.cube, 4), (.mini, 2), (.ship, 1), (.gravity, 1)],
                escalating: false,
                phraseBias: { name in
                    switch name {
                    case "single_spike": return 5
                    case "double_spike", "kick_wall", "snare_orb": return 2
                    case "ship_zigzag", "ship_snare_orbs": return 2
                    default: return 1
                    }
                })
        case .aggressive:
            return StyleProfile(
                sectionCountRange: 4...6,
                sectionBeatRange: 16...28,
                restBeatsRange: 0.5...1.5,
                bpmRange: 140...180,
                modeWeights: [(.cube, 3), (.mini, 3), (.ship, 2), (.gravity, 2)],
                escalating: true,
                phraseBias: { name in
                    switch name {
                    case "triple_spike", "mini_burst", "ceiling_double", "ship_kick_gate": return 4
                    case "double_spike", "ship_zigzag", "hat_floater", "snare_orb": return 3
                    case "single_spike", "kick_wall": return 1
                    case "stair_up": return 0.2
                    default: return 1
                    }
                })
        case .progressive:
            return StyleProfile(
                sectionCountRange: 4...6,
                sectionBeatRange: 20...36,
                restBeatsRange: 1.0...3.0,
                bpmRange: 100...140,
                modeWeights: [(.cube, 3), (.ship, 2), (.gravity, 2), (.mini, 2)],
                escalating: true,
                phraseBias: { _ in 1 })
        case .spacey:
            return StyleProfile(
                sectionCountRange: 3...5,
                sectionBeatRange: 20...36,
                restBeatsRange: 1.5...3.0,
                bpmRange: 80...120,
                modeWeights: [(.ship, 5), (.gravity, 3), (.cube, 1), (.mini, 0.5)],
                escalating: false,
                phraseBias: { name in
                    switch name {
                    case "ship_zigzag", "ship_threader", "ship_kick_gate", "ship_snare_orbs": return 3
                    case "ceiling_spike", "gravity_hat_orb": return 2
                    default: return 1
                    }
                })
        case .stuttery:
            return StyleProfile(
                sectionCountRange: 6...9,
                sectionBeatRange: 8...16,
                restBeatsRange: 0.5...2.0,
                bpmRange: 120...160,
                modeWeights: [(.cube, 2), (.mini, 2), (.ship, 2), (.gravity, 2)],
                escalating: false,
                phraseBias: { _ in 1 })
        case .anthem:
            return StyleProfile(
                sectionCountRange: 4...6,
                sectionBeatRange: 24...48,
                restBeatsRange: 1.0...4.0,
                bpmRange: 110...150,
                modeWeights: [(.cube, 4), (.ship, 2), (.mini, 1), (.gravity, 1)],
                escalating: true,
                phraseBias: { name in
                    switch name {
                    case "single_spike", "stair_up", "spike_block", "kick_wall", "snare_orb": return 2
                    case "triple_spike", "ship_kick_gate": return 2
                    default: return 1
                    }
                })
        }
    }
}

// MARK: - Generator

enum LevelGenerator {

    private static let sectionLabels: [String] = [
        "Intro", "Verse", "Pre-Chorus", "Chorus", "Drop",
        "Guitar Solo", "Drum Break", "Bridge", "Breakdown", "Outro"
    ]

    static func generate(
        songName: String,
        bandName: String,
        fixedBPM: Double? = nil,
        fixedDurationSeconds: Double? = nil,
        trackSourceID: String? = nil
    ) -> Level {
        let seedSource = "\(songName.trimmingCharacters(in: .whitespacesAndNewlines))::\(bandName.trimmingCharacters(in: .whitespacesAndNewlines))"
        var rng = SeededRNG(string: seedSource.isEmpty ? "untitled" : seedSource)

        let style = rng.pick(LevelStyle.allCases)
        let profile = style.profile

        let bpm = (fixedBPM ?? rng.doubleInRange(profile.bpmRange)).rounded()
        let pointsPerBeat = CGFloat(rng.doubleInRange(110...160))

        // If we know the real song duration, target that many beats; otherwise
        // let the style decide section count.
        let targetTotalBeats: Double? = fixedDurationSeconds.map { $0 * bpm / 60.0 }
        let sectionCount = rng.intInRange(profile.sectionCountRange)

        // Build sections.
        var sections: [LevelSection] = []
        var cursorBeat = 0.0

        // Choose labels: first is always Intro, last is Outro when section count >= 3.
        var labelPool = sectionLabels.shuffled(using: &rng)
        labelPool.removeAll(where: { $0 == "Intro" || $0 == "Outro" })
        var labels: [String] = ["Intro"]
        let middleCount = max(0, sectionCount - 2)
        labels.append(contentsOf: labelPool.prefix(middleCount))
        if sectionCount >= 2 { labels.append("Outro") }
        while labels.count < sectionCount { labels.append("Verse") }

        for i in 0..<sectionCount {
            var beatsForSection = rng.doubleInRange(profile.sectionBeatRange)
            // If we have a fixed target, scale the remaining section to fit.
            if let target = targetTotalBeats {
                let remaining = target - cursorBeat
                let sectionsLeft = sectionCount - i
                if remaining <= 0 { break }
                let avgBeats = remaining / Double(sectionsLeft)
                // Bias toward the average but keep some variation.
                beatsForSection = max(8, min(avgBeats * 1.4, max(beatsForSection, avgBeats * 0.7)))
                if i == sectionCount - 1 {
                    beatsForSection = remaining
                }
            }
            // Pick mode — first section is cube for safe onboarding.
            let mode: GameMode = (i == 0) ? .cube : rng.weightedPick(profile.modeWeights)
            let label = labels[i]

            // Intensity grows over the level for "escalating" styles.
            let intensityFactor: Double = {
                guard profile.escalating else { return 1.0 }
                return 0.5 + 0.6 * (Double(i) / max(1.0, Double(sectionCount - 1)))
            }()

            let obstacles = generateSectionObstacles(
                mode: mode,
                sectionStartBeat: cursorBeat,
                sectionBeats: beatsForSection,
                profile: profile,
                intensity: intensityFactor,
                isFirstSection: i == 0,
                rng: &rng
            )

            sections.append(LevelSection(
                mode: mode, startBeat: cursorBeat,
                obstacles: obstacles, label: label
            ))
            cursorBeat += beatsForSection
        }

        let primaryHue = CGFloat(rng.doubleInRange(0...1))
        let secondaryHue = CGFloat((Double(primaryHue) + rng.doubleInRange(0.25...0.55))
            .truncatingRemainder(dividingBy: 1.0))

        return Level(
            id: UUID(),
            songName: songName,
            bandName: bandName,
            bpm: bpm,
            pointsPerBeat: pointsPerBeat,
            totalBeats: cursorBeat,
            sections: sections,
            primaryHue: primaryHue,
            secondaryHue: secondaryHue,
            style: style,
            trackSourceID: trackSourceID
        )
    }

    // MARK: - Section obstacle assembly

    private static func generateSectionObstacles(
        mode: GameMode,
        sectionStartBeat: Double,
        sectionBeats: Double,
        profile: StyleProfile,
        intensity: Double,
        isFirstSection: Bool,
        rng: inout SeededRNG
    ) -> [Obstacle] {
        var obstacles: [Obstacle] = []
        // Leave breathing room at the very start of the level, then sectionwise lead-in.
        var localBeat: Double = isFirstSection ? 8 : 2
        let endBeat = sectionBeats - 1.5

        let pool = PhraseLibrary.phrases(for: mode)
        guard !pool.isEmpty else { return obstacles }

        // Weighted phrase choices using the style's phraseBias.
        let weighted = pool.map { ($0, profile.phraseBias($0.name)) }

        while localBeat < endBeat {
            let phrase = rng.weightedPick(weighted)
            // Snap phrase start to an integer beat for groove.
            let phraseStart = localBeat.rounded()
            if phraseStart + phrase.beats > endBeat { break }

            for po in phrase.obstacles {
                obstacles.append(Obstacle(
                    kind: po.kind,
                    beat: sectionStartBeat + phraseStart + po.beatOffset,
                    yOffset: po.yOffset,
                    width: po.width,
                    height: po.height
                ))
            }
            // Advance past the phrase, plus a style-defined rest scaled by intensity.
            let rest = rng.doubleInRange(profile.restBeatsRange) / max(0.5, intensity)
            localBeat = phraseStart + phrase.beats + rest
        }
        return obstacles
    }
}
