/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/

//
//  StackChanRobot.swift
//  Runner
//
// Created by on 2026/1/30.
//
import SceneKit

class StackChanRobot: NSObject, FlutterPlatformView, FlutterStreamHandler {
    
    private let sceneView: SCNView
    private let methodChannel: FlutterMethodChannel
    
    private var currentDanceData: DanceData?
    private let expressionLayer = ExpressionLayer(data: ExpressionData(leftEye: ExpressionItem(weight: 100), rightEye: ExpressionItem(weight: 100), mouth: ExpressionItem()))
    private let planeNodeName = "expressionPlane"
    private let rotateKey = "autoRotate"
    private var topLook: Bool = false
    private let methodChannelName = "com.stackchan.robot.method"
    
    private var defaultCameraNode: SCNNode?
    private var topCameraNode: SCNNode?
    
    init(
        frame: CGRect,
        viewId: Int64,
        messenger: FlutterBinaryMessenger,
        args: Any?
    ) {
        self.sceneView = SCNView(frame: frame)
        self.methodChannel = FlutterMethodChannel(
            name: methodChannelName + "_\(viewId)",
            binaryMessenger: messenger
        )
        super.init()
        methodChannel.setMethodCallHandler(handleMethodCall)
        setupSceneView()
        setupInitialScene()
    }
    
    func view() -> UIView {
        return sceneView
    }
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        return nil
    }
    
    private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "updateDanceData":
            if let json = call.arguments as? String {
                updateDanceData(from: json)
                result(nil)
            } else {
                result(FlutterError(
                    code: "INVALID_ARGS",
                    message: "Expected JSON string",
                    details: nil
                ))
            }
        case "setTopLook":
            if let topLook = call.arguments as? Bool {
                self.topLook = topLook
                setupCamera()
                result(nil)
            } else {
                result(FlutterError(
                    code: "INVALID_ARGS",
                    message: "Expected boolean value",
                    details: nil
                ))
            }
        case "setAllowsCameraControl":
            if let allowsControl = call.arguments as? Bool {
                sceneView.allowsCameraControl = allowsControl
                result(nil)
            } else {
                result(FlutterError(
                    code: "INVALID_ARGS",
                    message: "Expected boolean value",
                    details: nil
                ))
            }
        case "dispose":
            cleanup()
            result(nil)
        default:
            result(FlutterMethodNotImplemented)
        }
    }
    
    private func setupSceneView() {
        sceneView.antialiasingMode = .multisampling4X
        sceneView.autoenablesDefaultLighting = true
        sceneView.allowsCameraControl = false
        sceneView.backgroundColor = .clear
        sceneView.isPlaying = true
    }
    
    private func setupInitialScene() {
        guard let scene = SCNScene(named: "StackChanModel.scn") else {
                        return
        }
        scene.rootNode.eulerAngles = SCNVector3Zero
        scene.rootNode.eulerAngles.x = -Float.pi / 2
        scene.rootNode.position.y = scene.rootNode.position.y + 25
        scene.rootNode.position.z = scene.rootNode.position.z - 35
        
        if let rootNode = scene.rootNode.childNodes.first {
            setupRobotHierarchy(rootNode: rootNode, scene: scene)
            
            if let defaultCamera = scene.rootNode.childNode(withName: "camera", recursively: true) {
                defaultCameraNode = defaultCamera
            } else {
                defaultCameraNode = createDefaultCameraNode(rootNode: rootNode)
                rootNode.addChildNode(defaultCameraNode!)
            }
            sceneView.pointOfView = defaultCameraNode
        }
        sceneView.scene = scene
    }
    
    private func createDefaultCameraNode(rootNode: SCNNode) -> SCNNode {
        let cameraNode = SCNNode()
        cameraNode.name = "defaultCamera"
        let camera = SCNCamera()
        camera.zFar = 200
        cameraNode.camera = camera
        cameraNode.position = SCNVector3(x: 0, y: -100, z: 0)
        let lookAtConstraint = SCNLookAtConstraint(target: rootNode)
        lookAtConstraint.isGimbalLockEnabled = true
        cameraNode.constraints = [lookAtConstraint]
        return cameraNode
    }
    
    private func createTopCameraNode(rootNode: SCNNode) -> SCNNode {
        let cameraNode = SCNNode()
        cameraNode.name = "leftTopCamera"
        let camera = SCNCamera()
        camera.zFar = 300
        cameraNode.camera = camera
        cameraNode.position = SCNVector3(x: 0, y: -100, z: 70)
        let lookAtConstraint = SCNLookAtConstraint(target: rootNode)
        lookAtConstraint.isGimbalLockEnabled = true
        cameraNode.constraints = [lookAtConstraint]
        return cameraNode
    }
    
    private func setupRobotHierarchy(rootNode: SCNNode, scene: SCNScene) {
        guard let foundation = rootNode.childNode(withName: "_00_stackchan450_3", recursively: false),
              let centralComponent = rootNode.childNode(withName: "_00_stackchan450_2", recursively: false),
              let head = rootNode.childNode(withName: "_00_stackchan450_1", recursively: false) else {
            return
        }
        
        let yawAxis = SCNNode()
        yawAxis.name = "yawAxis"
        let centralWorldPos = centralComponent.worldPosition
        yawAxis.worldPosition.z = centralWorldPos.z + 15
        foundation.addChildNode(yawAxis)
        
        let centralWorldTransform = centralComponent.worldTransform
        yawAxis.addChildNode(centralComponent)
        centralComponent.setWorldTransform(centralWorldTransform)
        
        // Setup pitch axis for head movement
        let headWorldTransform = head.worldTransform
        let pitchAxis = SCNNode()
        pitchAxis.name = "pitchAxis"
        pitchAxis.worldPosition.z = pitchAxis.worldPosition.z - 20
        centralComponent.addChildNode(pitchAxis)
        pitchAxis.addChildNode(head)
        head.setWorldTransform(headWorldTransform)
        
        // Add expression plane to head
        addExpressionPlane(to: head)
    }
    
    private func addExpressionPlane(to head: SCNNode) {
        let plane = SCNPlane(width: 42, height: 32)
        let magnification: CGFloat = 5
        let size = CGSize(width: magnification * plane.width, height: magnification * plane.height)
        
        expressionLayer.frame = CGRect(origin: .zero, size: size)
        expressionLayer.setNeedsDisplay()
        let material = SCNMaterial()
        plane.materials = [material]
        
        let planeNode = SCNNode(geometry: plane)
        planeNode.name = planeNodeName
        planeNode.position = head.position
        planeNode.position.z = planeNode.position.z - 4.5
        
        head.addChildNode(planeNode)
    }
    
    private func setupCamera() {
        guard let scene = sceneView.scene,
              let rootNode = scene.rootNode.childNodes.first else {
            return
        }
        rootNode.childNodes.filter { $0.name == "leftTopCamera" }.forEach { $0.removeFromParentNode() }
        if topLook {
            if topCameraNode == nil {
                topCameraNode = createTopCameraNode(rootNode: rootNode)
                rootNode.addChildNode(topCameraNode!)
            }
            sceneView.pointOfView = topCameraNode
        } else {
            if defaultCameraNode == nil {
                defaultCameraNode = createDefaultCameraNode(rootNode: rootNode)
                rootNode.addChildNode(defaultCameraNode!)
            }
            sceneView.pointOfView = defaultCameraNode
        }
    }
    
    private func updateDanceData(from json: String) {
        guard let danceData = DanceData.from(jsonString: json) else {
            return
        }
        
        currentDanceData = danceData
        
        DispatchQueue.main.async {
            self.applyDanceData(danceData)
        }
    }
    
    private func applyDanceData(_ data: DanceData) {
        guard let scene = sceneView.scene,
              let rootNode = scene.rootNode.childNodes.first else {
            return
        }
        
        // Update servo positions
        updateServos(rootNode: rootNode, data: data)
        
        // Update RGB color
        updateRGBColor(rootNode: rootNode, data: data)
        
        // Update expression
        updateExpression(data: data)
    }
    private func updateServos(rootNode: SCNNode, data: DanceData) {
        
        if let yawAxis = rootNode.childNode(withName: "yawAxis", recursively: true),
           let pitchAxis = rootNode.childNode(withName: "pitchAxis", recursively: true) {
            
            yawAxis.removeAction(forKey: rotateKey)
            
            // Update yaw (rotation around Y axis)
            if data.yawServo.rotate == 0 {
                let clampedYaw = max(-128, min(128, data.yawServo.angle / 10))
                let yawRadians = Float(clampedYaw) * Float.pi / 180.0
                yawAxis.rotation = SCNVector4(0, 1, 0, yawRadians)
            } else {
                let rotateSpeed = max(-100, min(100, data.yawServo.rotate / 10))
                let radiansPerSecond = Float(rotateSpeed) / 100.0 * Float.pi * 2
                
                let rotateAction = SCNAction.customAction(duration: .infinity) { node, _ in
                    let deltaTime: Float = 1.0 / 60.0
                    node.eulerAngles.y += radiansPerSecond * deltaTime
                }
                yawAxis.runAction(rotateAction, forKey: rotateKey)
            }
            
            // Update pitch (head tilt)
            let clampedPitch = max(0, min(90, data.pitchServo.angle / 10))
            let pitchRadians = Float(clampedPitch) * Float.pi / 180.0
            pitchAxis.eulerAngles.x = -pitchRadians
        }
    }
    
    private func updateRGBColor(rootNode: SCNNode, data: DanceData) {
        rootNode.enumerateChildNodes { node, _ in
            if let materials = node.geometry?.materials {
                for material in materials {
                    if material.name == "MTL12" {
                        if let color = UIColor(hex: data.leftRgbColor) {
                            material.emission.contents = color
                        }
                        break
                    }
                }
            }
        }
    }
    
    private func updateExpression(data: DanceData) {
        guard let planeNode = sceneView.scene?.rootNode.childNode(withName: planeNodeName, recursively: true),
              let plane = planeNode.geometry as? SCNPlane else {
            return
        }
        
        let expressionData = ExpressionData(
            leftEye: data.leftEye,
            rightEye: data.rightEye,
            mouth: data.mouth
        )
        
        expressionLayer.data = expressionData
        expressionLayer.setNeedsDisplay()
        
        let newImage = expressionRenderer().image { ctx in
            self.expressionLayer.render(in: ctx.cgContext)
        }
        
        plane.firstMaterial?.diffuse.contents = newImage
    }
    
    private func expressionRenderer() -> UIGraphicsImageRenderer {
        let format = UIGraphicsImageRendererFormat.default()
        format.scale = UIScreen.main.scale
        format.opaque = false
        return UIGraphicsImageRenderer(
            size: expressionLayer.bounds.size,
            format: format
        )
    }
    
    private func cleanup() {
        // Stop all animations
        sceneView.scene?.rootNode.childNodes.forEach { node in
            node.removeAllActions()
            node.removeFromParentNode()
        }
        
        // Clean up scene
        sceneView.scene = nil
        sceneView.isPlaying = false
        
        // Remove method call handler
        methodChannel.setMethodCallHandler(nil)
    }
    
    deinit {
        cleanup()
    }
}





class ExpressionLayer: CALayer {
    var data: ExpressionData
    
    let reverse: Bool
    
    init(data: ExpressionData, reverse: Bool = false) {
        self.data = data
        self.reverse = reverse
        super.init()
        self.contentsScale = UIScreen.main.scale
        self.setNeedsDisplay()
    }
    
    override init(layer: Any) {
        if let layer = layer as? ExpressionLayer {
            self.data = layer.data
            self.reverse = layer.reverse
        } else {
            self.data = ExpressionData(leftEye: ExpressionItem(), rightEye: ExpressionItem(), mouth: ExpressionItem())
            self.reverse = false
        }
        super.init(layer: layer)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(in ctx: CGContext) {
        let rect = self.frame
        
        // Background
        ctx.setFillColor(UIColor.black.withAlphaComponent(0.7).cgColor)
        ctx.fill(rect)
        
        let eyeSize = rect.width / 10
        
        func drawEye(_ item: ExpressionItem, at point: CGPoint) {
            
            // Calculate scale based on size (-100 to 100)
            // 0   -> 1.0 (keep current size)
            // -100 -> 0.5 (half normal radius)
            // 100  -> 2.0 (double normal radius)
            let clampedSize = max(-100, min(100, item.size))
            let sizeScale: CGFloat
            if clampedSize >= 0 {
                sizeScale = 1.0 + CGFloat(clampedSize) / 100.0
            } else {
                sizeScale = 1.0 + CGFloat(clampedSize) / 200.0
            }
            
            let scaledEyeSize = eyeSize * sizeScale
            
            let visibleHeight = scaledEyeSize * (CGFloat(item.weight) / 100)
            
            let centerX = point.x + CGFloat(item.x / 10) + eyeSize / 2
            let centerY = point.y + CGFloat(item.y / 10) + eyeSize / 2
            let eyeRect = CGRect(
                x: centerX - scaledEyeSize / 2,
                y: centerY - scaledEyeSize / 2,
                width: scaledEyeSize,
                height: scaledEyeSize
            )
            
            ctx.saveGState()
            
            // Rotation
            let rotationDegrees = CGFloat(item.rotation) / 10.0
            let center = CGPoint(x: eyeRect.midX, y: eyeRect.midY)
            ctx.translateBy(x: center.x, y: center.y)
            ctx.rotate(by: rotationDegrees * .pi / 180)
            ctx.translateBy(x: -center.x, y: -center.y)
            
            // Clip height
            let maskRect = CGRect(
                x: eyeRect.minX,
                y: eyeRect.maxY - visibleHeight,
                width: scaledEyeSize,
                height: visibleHeight
            )
            ctx.addRect(maskRect)
            ctx.clip()
            
            ctx.setFillColor(UIColor.white.cgColor)
            ctx.fillEllipse(in: eyeRect)
            
            ctx.restoreGState()
        }
        
        let eyeY = (rect.height * 0.4) - (eyeSize / 2)
        let leftEyePoint = CGPoint(x: (rect.width / 4) - (eyeSize / 2), y: eyeY)
        let rightEyePoint = CGPoint(x: (rect.width / 4 * 3) - (eyeSize / 2), y: eyeY)
        
        
        if reverse {
            // Temporarily swap rotation angles
            let leftEyeRotation = data.leftEye.rotation
            let rightEyeRotation = data.rightEye.rotation
            
            var leftEye = data.leftEye
            var rightEye = data.rightEye
            
            leftEye.rotation = rightEyeRotation
            rightEye.rotation = leftEyeRotation
            
            drawEye(leftEye, at: rightEyePoint)
            drawEye(rightEye, at: leftEyePoint)
        } else {
            drawEye(data.leftEye, at: leftEyePoint)
            drawEye(data.rightEye, at: rightEyePoint)
        }
        
        // Draw mouth
        ctx.saveGState()
        
        let width = rect.width * 0.3 - CGFloat(data.mouth.weight / 10)
        let height = 3 + CGFloat(data.mouth.weight) * 0.2
        let x = ((rect.width - width) / 2) + CGFloat(data.mouth.x / 10)
        let y = (rect.height * 0.65) + CGFloat(data.mouth.y / 10)
        
        let rotationDegrees = CGFloat(data.mouth.rotation) / 10.0
        let center = CGPoint(x: x + width / 2, y: y + height / 2)
        ctx.translateBy(x: center.x, y: center.y)
        ctx.rotate(by: rotationDegrees * .pi / 180)
        ctx.translateBy(x: -center.x, y: -center.y)
        
        let mouthRect = CGRect(x: x, y: y, width: width, height: height)
        let mouthPath = UIBezierPath(roundedRect: mouthRect, cornerRadius: height / 2)
        ctx.addPath(mouthPath.cgPath)
        ctx.setFillColor(UIColor.white.cgColor)
        ctx.fillPath()
        
        ctx.restoreGState()
    }
}
