import SpriteKit

// MARK: - Delegate

protocol GameSceneDelegate: AnyObject {
    func gameSceneDidWin(moveCount: Int, stars: Int)
    func gameSceneDidUpdateMoveCount(_ count: Int)
}

// MARK: - GameScene

final class GameScene: SKScene {

    // MARK: State
    var gameState: GameState {
        didSet { syncBlockNodes() }
    }
    private var renderer: GridRenderer!
    private var blockNodes: [Int: BlockNode] = [:]   // keyed by Block.id
    private var selectedBlockIndex: Int? = nil
    private var isAnimating = false
    weak var sceneDelegate: GameSceneDelegate?

    // MARK: Init

    init(state: GameState, size: CGSize) {
        self.gameState = state
        super.init(size: size)
        scaleMode = .resizeFill
        backgroundColor = UIColor(hex: "#1A1A2E")
    }

    required init?(coder aDecoder: NSCoder) { fatalError() }

    // MARK: - Scene lifecycle

    override func didMove(to view: SKView) {
        renderer = GridRenderer(gridSize: gameState.grid, sceneSize: size)
        addChild(renderer.buildBackground())
        addChild(renderer.buildWalls(gameState.walls))
        rebuildAllBlockNodes()
        setupGestures(in: view)
        SFXRunner.shared.attach(to: self)
    }

    // MARK: - Input

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard !isAnimating, let touch = touches.first else { return }
        let loc = touch.location(in: self)

        // Check if a block was tapped
        if let blockNode = nodes(at: loc).compactMap({ $0 as? BlockNode }).first {
            let idx = gameState.blocks.firstIndex(where: { $0.id == blockNode.block.id })
            selectBlock(at: idx)
            SFXManager.shared.play(.select)
        }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        // Handled by swipe detection below
    }

    private var swipeStart: CGPoint?

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        if swipeStart == nil { swipeStart = touch.previousLocation(in: self) }
    }

    // We use a single recognizer-style approach: track start on touchesBegan
    // and compute direction on touchesEnded for the selected block.

    // MARK: - Swipe detection via UISwipeGestureRecognizer (added in didMove)

    func setupGestures(in view: SKView) {
        for dir in [UISwipeGestureRecognizer.Direction.up,
                    .down, .left, .right] {
            let rec = UISwipeGestureRecognizer(target: self,
                                               action: #selector(handleSwipe(_:)))
            rec.direction = dir
            view.addGestureRecognizer(rec)
        }
    }

    @objc private func handleSwipe(_ rec: UISwipeGestureRecognizer) {
        guard !isAnimating, let idx = selectedBlockIndex else { return }
        let direction: Direction
        switch rec.direction {
        case .up:    direction = .up
        case .down:  direction = .down
        case .left:  direction = .left
        case .right: direction = .right
        default:     return
        }
        applyMove(blockIndex: idx, direction: direction)
    }

    // MARK: - Move application

    func applyMove(blockIndex: Int, direction: Direction) {
        guard !isAnimating else { return }
        let result = GameEngine.processMove(state: gameState, blockIndex: blockIndex,
                                            direction: direction)
        guard result.isValid else { return }

        isAnimating = true
        selectedBlockIndex = nil
        deselectAll()

        animateEvents(result.events, newState: result.newState) { [weak self] in
            guard let self else { return }
            self.gameState  = result.newState
            self.isAnimating = false
            self.sceneDelegate?.gameSceneDidUpdateMoveCount(result.newState.moveCount)
            if result.newState.isWon {
                SFXManager.shared.play(.complete)
                self.sceneDelegate?.gameSceneDidWin(
                    moveCount: result.newState.moveCount,
                    stars: result.newState.currentStars
                )
            }
        }
    }

    func applyUndo() {
        guard !isAnimating else { return }
        let prev = GameEngine.undo(state: gameState)
        guard prev.moveCount != gameState.moveCount || prev.blocks.count != gameState.blocks.count else { return }
        gameState = prev
        sceneDelegate?.gameSceneDidUpdateMoveCount(prev.moveCount)
    }

    func applyRestart(initialState: GameState) {
        guard !isAnimating else { return }
        gameState = initialState
        sceneDelegate?.gameSceneDidUpdateMoveCount(0)
    }

    // MARK: - Animation

    private func animateEvents(_ events: [GameEvent], newState: GameState,
                               completion: @escaping () -> Void) {
        var remaining = events.count
        guard remaining > 0 else { completion(); return }

        let done = {
            remaining -= 1
            if remaining == 0 { completion() }
        }

        for event in events {
            switch event {
            case .slid(let blockIndex, _, let to):
                guard let block = gameState.blocks.indices.contains(blockIndex)
                        ? gameState.blocks[blockIndex] : nil,
                      let node = blockNodes[block.id] else { done(); continue }

                let dest = renderer.scenePosition(for: to)
                SFXManager.shared.play(.slide)
                node.run(SKAction.move(to: dest, duration: 0.15).ease(.easeOut)) { done() }

            case .destroyed(let idx1, let idx2, _):
                // Both nodes are still at their pre-move positions; animate the slid one
                // (already handled in .slid), then destroy both
                let ids = [idx1, idx2].compactMap { i -> Int? in
                    guard gameState.blocks.indices.contains(i) else { return nil }
                    return gameState.blocks[i].id
                }
                SFXManager.shared.play(.destroy)
                var destroyRemaining = ids.count
                for id in ids {
                    guard let node = blockNodes[id] else {
                        destroyRemaining -= 1
                        continue
                    }
                    node.animateDestruction {
                        node.removeFromParent()
                        destroyRemaining -= 1
                        if destroyRemaining == 0 { done() }
                    }
                }
                if ids.isEmpty { done() }

            case .merged(let idx1, let idx2, let resultColor, let position):
                // Look up both source block nodes (indices reference the OLD state)
                let ids = [idx1, idx2].compactMap { i -> Int? in
                    guard gameState.blocks.indices.contains(i) else { return nil }
                    return gameState.blocks[i].id
                }

                // The engine assigns the merged block the stationary block's id.
                // Look it up in the new state so the sprite, the model block, and
                // the blockNodes dict all agree on the same id.
                let mergedId = newState.blocks.first(where: { $0.position == position })?.id ?? -1

                // Burst: scale up 1.2×, then shrink + fade out, then remove
                let burstUp   = SKAction.scale(to: 1.2, duration: 0.1)
                let burstOut  = SKAction.group([
                    SKAction.scale(to: 0.0, duration: 0.1),
                    SKAction.fadeOut(withDuration: 0.1)
                ])
                let burstSeq  = SKAction.sequence([burstUp, burstOut, SKAction.removeFromParent()])

                var burstCount = ids.count
                let spawnMergedBlock = { [weak self] in
                    guard let self else { return }
                    // Source nodes were removed from the scene tree by burstSeq;
                    // drop their dict entries so syncBlockNodes/selection stay consistent.
                    for id in ids { self.blockNodes.removeValue(forKey: id) }

                    let mergedBlock = Block(id: mergedId, position: position, color: resultColor)
                    let node = BlockNode(block: mergedBlock, cellSize: self.renderer.cellSize)
                    node.position = self.renderer.scenePosition(for: position)
                    node.setScale(0.0)
                    self.addChild(node)
                    self.blockNodes[mergedId] = node
                    // Spring pop: 0 → 1.15 → 1.0
                    let spring = SKAction.sequence([
                        SKAction.scale(to: 1.15, duration: 0.12),
                        SKAction.scale(to: 1.0, duration: 0.05)
                    ])
                    node.run(spring) { done() }
                }

                SFXManager.shared.play(.merge)

                if ids.isEmpty {
                    spawnMergedBlock()
                } else {
                    for id in ids {
                        guard let node = blockNodes[id] else {
                            burstCount -= 1
                            if burstCount == 0 { spawnMergedBlock() }
                            continue
                        }
                        node.run(burstSeq) {
                            burstCount -= 1
                            if burstCount == 0 { spawnMergedBlock() }
                        }
                    }
                }
            }
        }
    }

    // MARK: - Node management

    private func rebuildAllBlockNodes() {
        blockNodes.values.forEach { $0.removeFromParent() }
        blockNodes = [:]
        for (idx, block) in gameState.blocks.enumerated() {
            addBlockNode(block: block, at: block.position)
            _ = idx
        }
    }

    private func addBlockNode(block: Block, at pos: GridPosition) {
        let node = BlockNode(block: block, cellSize: renderer.cellSize)
        node.position = renderer.scenePosition(for: pos)
        addChild(node)
        blockNodes[block.id] = node
    }

    /// Called after state changes NOT driven by animation (undo/restart)
    private func syncBlockNodes() {
        // Remove nodes for blocks no longer in state
        let liveIds = Set(gameState.blocks.map { $0.id })
        for (id, node) in blockNodes where !liveIds.contains(id) {
            node.removeFromParent()
            blockNodes.removeValue(forKey: id)
        }
        // Update existing or add new
        for block in gameState.blocks {
            if let node = blockNodes[block.id] {
                node.position = renderer.scenePosition(for: block.position)
            } else {
                addBlockNode(block: block, at: block.position)
            }
        }
    }

    // MARK: - Selection

    private func selectBlock(at index: Int?) {
        deselectAll()
        selectedBlockIndex = index
        guard let idx = index,
              gameState.blocks.indices.contains(idx) else { return }
        let block = gameState.blocks[idx]
        blockNodes[block.id]?.setSelected(true, cellSize: renderer.cellSize)
    }

    private func deselectAll() {
        for node in blockNodes.values {
            node.setSelected(false, cellSize: renderer.cellSize)
        }
    }
}

// MARK: - SKAction ease helper

private extension SKAction {
    func ease(_ timing: SKActionTimingMode) -> SKAction {
        timingMode = timing
        return self
    }
}
