import Metal
import MetalKit
import simd
import SwiftUI

/// A SwiftUI wrapper around a full-screen quad running your `clouds_fragment` shader.
public struct CloudySkyView: UIViewRepresentable, Identifiable {
    public let id: String

    // Exposed uniforms:
    public var timeScale: ADFloat
    public var cloudscale: ADFloat
    public var speed: ADFloat
    public var clouddark: ADFloat
    public var cloudlight: ADFloat
    public var cloudcover: ADFloat
    public var cloudalpha: ADFloat
    public var skytint: ADFloat
    public var skycolour1: ADVector3
    public var skycolour2: ADVector3

    // Frame control:
    public var frameRate: Int = 60
    public var isPaused: Bool = false

    @ObservedObject private var coordinator: Coordinator

    public init(
        id: String,
        timeScale: ADFloat = 1.0,
        cloudscale: ADFloat = 1.1,
        speed: ADFloat = 0.03,
        clouddark: ADFloat = 0.5,
        cloudlight: ADFloat = 0.3,
        cloudcover: ADFloat = 0.2,
        cloudalpha: ADFloat = 8.0,
        skytint: ADFloat = 0.5,
        skycolour1: ADVector3 = .init(x: 0.2, y: 0.4, z: 0.6),
        skycolour2: ADVector3 = .init(x: 0.4, y: 0.7, z: 1.0),
        frameRate: Int = 60,
        isPaused: Bool = false
    ) {
        self.id = id
        self.timeScale = timeScale
        self.cloudscale = cloudscale
        self.speed = speed
        self.clouddark = clouddark
        self.cloudlight = cloudlight
        self.cloudcover = cloudcover
        self.cloudalpha = cloudalpha
        self.skytint = skytint
        self.skycolour1 = skycolour1
        self.skycolour2 = skycolour2
        self.frameRate = frameRate
        self.isPaused = isPaused

        let c = Coordinator()
        self.coordinator = c
    }

    public func makeCoordinator() -> Coordinator { coordinator }

    public func makeUIView(context: Context) -> MTKView {
        let mtk = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice())
        mtk.delegate = context.coordinator
        mtk.preferredFramesPerSecond = frameRate
        mtk.framebufferOnly = false
        mtk.clearColor = MTLClearColorMake(0, 0, 0, 0)
        mtk.enableSetNeedsDisplay = true
        mtk.isPaused = isPaused
        return mtk
    }

    public func updateUIView(_ uiView: MTKView, context _: Context) {
        uiView.isPaused = isPaused
        // push updated uniforms down each update
        coordinator.updateTimeScale(timeScale)
        coordinator.updateUniforms(
            cloudscale: cloudscale,
            speed: speed,
            clouddark: clouddark,
            cloudlight: cloudlight,
            cloudcover: cloudcover,
            cloudalpha: cloudalpha,
            skytint: skytint,
            skycolour1: skycolour1,
            skycolour2: skycolour2
        )
    }

    public class Coordinator: NSObject, ObservableObject, MTKViewDelegate {
        private let device: MTLDevice?
        private let queue: MTLCommandQueue?
        private let renderable: CloudySkyRenderable

        // current uniform values
        private var uniforms = CloudySkyRenderable.Config.defaultConfiguration

        // seed time
        private let start = CFAbsoluteTimeGetCurrent()
        private var lastTime: CFAbsoluteTime?
        private var timeScale: ADFloat = 1.0

        override public init() {
            device = Adamantium.sharedDevice
            queue = Adamantium.sharedCommandQueue
            renderable = CloudySkyRenderable()
            super.init()
        }

        func updateTimeScale(_ newTimeScale: ADFloat) {
            timeScale = newTimeScale
        }

        // push new uniform values
        func updateUniforms(
            cloudscale: ADFloat,
            speed: ADFloat,
            clouddark: ADFloat,
            cloudlight: ADFloat,
            cloudcover: ADFloat,
            cloudalpha: ADFloat,
            skytint: ADFloat,
            skycolour1: ADVector3,
            skycolour2: ADVector3
        ) {
            uniforms.cloudscale = cloudscale
            uniforms.speed = speed
            uniforms.clouddark = clouddark
            uniforms.cloudlight = cloudlight
            uniforms.cloudcover = cloudcover
            uniforms.cloudalpha = cloudalpha
            uniforms.skytint = skytint
            uniforms.skycolour1 = skycolour1
            uniforms.skycolour2 = skycolour2
        }

        public func mtkView(_: MTKView, drawableSizeWillChange _: CGSize) {
            // nothing special
        }

        public func draw(in view: MTKView) {
            guard
                let buf = queue?.makeCommandBuffer(),
                let desc = view.currentRenderPassDescriptor,
                let drw = view.currentDrawable
            else { return }

            // compute delta time and absolute time
            let now = CFAbsoluteTimeGetCurrent()
            let delta = lastTime.map { now - $0 } ?? 0
            lastTime = now

            let elapsed = Float(now - start)
            // update per-frame uniforms:
            uniforms.resolution = .init(
                x: Float(view.drawableSize.width),
                y: Float(view.drawableSize.height)
            )

            uniforms.time = elapsed * timeScale

            // apply config and render
            renderable.config = uniforms

            let enc = buf.makeRenderCommandEncoder(descriptor: desc)!
            renderable.render(encoder: enc)
            enc.endEncoding()

            buf.present(drw)
            buf.commit()
        }
    }
}
