/*
 * Copyright © Dynamsoft Corporation. All rights reserved.
 */

import UIKit
import React
import DynamsoftCaptureVisionBundle

@objc(RNDynamsoftCameraViewManager)
class RNDynamsoftCameraViewManager: RCTViewManager {

    private static let maxAttempts = 10
    private static let retryDelay: TimeInterval = 0.1 // 100ms

    private var dce: CameraEnhancer?

    /// Bumped for every setCameraView call so that a pending retry from an earlier call can
    /// never overwrite a newer bind/unbind. Main thread only.
    private var bindGeneration = 0

    override func view() -> UIView! {
        return RNDynamsoftCameraView()
    }

    @objc(createInstance)
    func createInstance() -> String {
        if let dce = dce {
            DSISAStore.shared.map.removeValue(forKey: String(dce.hash))
        }
        let newDce = CameraEnhancer()
        dce = newDce
        let key = String(newDce.hash)
        DSISAStore.shared.map[key] = newDce
        return key
    }

    @objc(destroyInstance:)
    func destroyInstance(_ key: String) -> NSNumber {
        dce = nil
        DSISAStore.shared.map.removeValue(forKey: key)
        return true
    }

    @objc func open() {
        dce?.open()
    }

    @objc func close() {
        dce?.close()
    }

    @objc func setCameraView(_ tag: NSNumber) {
        // Bind and unbind must share one queue, otherwise a late unbind from a screen that
        // was just torn down can null out the view a newly mounted screen already bound.
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.bindGeneration += 1
            if tag.intValue == 0 {
                self.dce?.cameraView = nil
                return
            }
            self.bindCameraView(tag: tag, generation: self.bindGeneration, attempt: 1)
        }
    }

    private func bindCameraView(tag: NSNumber, generation: Int, attempt: Int) {
        guard bindGeneration == generation else { return }

        if let view = RNDynamsoftCameraView.view(forReactTag: tag) {
            dce?.cameraView = view.cameraView
            return
        }

        guard attempt < Self.maxAttempts else {
            RNDynamsoftLogError("Cannot find RNDynamsoftCameraView with tag #\(tag) after \(attempt) attempts")
            return
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + Self.retryDelay) { [weak self] in
            self?.bindCameraView(tag: tag, generation: generation, attempt: attempt + 1)
        }
    }

    @objc func selectCamera(_ position: NSInteger) {
        dce?.selectCamera(with: CameraPosition(rawValue: position) ?? .backDualWideAuto, completion: nil)
    }

    @objc(getCameraPosition:reject:)
    func getCameraPosition(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        let postion = dce?.getCameraPosition() ?? .back
        resolve(NSNumber(value: postion.rawValue))
    }

    @objc func setFocus(_ x: CGFloat, y: CGFloat, mode: NSInteger) {
        dce?.setFocus(CGPoint(x: x, y: y), focusMode: FocusMode(rawValue: mode) ?? .continuousAuto)
    }

    @objc(getFocusMode:reject:)
    func getFocusMode(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        let mode = dce?.getFocusMode() ?? .continuousAuto
        resolve(NSNumber(value: mode.rawValue))
    }

    @objc func setZoomFactor(_ factor: CGFloat) {
        dce?.setZoomFactor(factor)
    }

    @objc(getZoomFactor:reject:)
    func getZoomFactor(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        resolve(dce?.getZoomFactor())
    }

    @objc func enableEnhancedFeatures(_ features: NSInteger) {
        dce?.enableEnhancedFeatures(EnhancedFeature(rawValue: UInt(features)))
    }

    @objc func disableEnhancedFeatures(_ features: NSInteger) {
        dce?.disableEnhancedFeatures(EnhancedFeature(rawValue: UInt(features)))
    }

    // Note: CameraEnhancer's `setScanRegion:error:` is imported into Swift as a
    // throwing method (Objective-C `BOOL ...error:(NSError **)` bridging convention).
    // Matches the void return declared in NativeDynamsoftCameraViewModule's Spec; failures are logged, not surfaced to JS.
    @objc func setScanRegion(_ region: NSDictionary?) {
        guard let region = region else {
            try? dce?.setScanRegion(nil)
            return
        }
        guard let top = (region["top"] as? NSNumber)?.floatValue,
              let left = (region["left"] as? NSNumber)?.floatValue,
              let right = (region["right"] as? NSNumber)?.floatValue,
              let bottom = (region["bottom"] as? NSNumber)?.floatValue,
              let measuredInPercentage = (region["measuredInPercentage"] as? NSNumber)?.boolValue else {
            RNDynamsoftLogError("setScanRegion: invalid region dictionary")
            return
        }
        let rect = Rect()
        rect.top = CGFloat(top)
        rect.left = CGFloat(left)
        rect.right = CGFloat(right)
        rect.bottom = CGFloat(bottom)
        rect.measuredInPercentage = measuredInPercentage

        do {
            try dce?.setScanRegion(rect)
        } catch {
            RNDynamsoftLogError("setScanRegion failed: \(error.localizedDescription)")
        }
    }

    @objc(getScanRegion:reject:)
    func getScanRegion(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        guard let rect = dce?.getScanRegion() else {
            resolve(nil)
            return
        }
        resolve([
            "top": rect.top,
            "left": rect.left,
            "right": rect.right,
            "bottom": rect.bottom,
            "measuredInPercentage": rect.measuredInPercentage
        ])
    }

    @objc func turnOnTorch() {
        dce?.turnOnTorch()
    }

    @objc func turnOffTorch() {
        dce?.turnOffTorch()
    }

    @objc func beep() {
        Feedback.beep()
    }

    @objc func vibrate() {
        Feedback.vibrate()
    }

    @objc func setResolution(_ resolution: NSInteger) {
        dce?.setResolution(Resolution(rawValue: resolution) ?? .resolution1080P)
    }

    @objc(getResolution:reject:)
    func getResolution(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        var size = CGSize.zero
        let resolution = dce?.getResolution()
        switch resolution {
        case .resolution1080P:
            size = CGSize(width: 1920, height: 1080)
        case .some(.resolutionAuto):
            size = CGSize(width: 1920, height: 1080)
        case .some(.resolution480P):
            size = CGSize(width: 640, height: 480)
        case .some(.resolution720P):
            size = CGSize(width: 1280, height: 720)
        case .some(.resolution4K):
            size = CGSize(width: 3840, height: 2160)
        case .some(.resolutionMax):
            size = CGSize(width: 4032, height: 3024)
        default:
            break
        }
        resolve([
            "width": size.width,
            "height": size.height
        ])
    }

    @objc(convertRectToViewCoordinates:)
    func convertRectToViewCoordinates(_ rectDict: NSDictionary) -> NSDictionary {
        let dsRect = Rect()
        dsRect.top = (rectDict["top"] as? NSNumber)?.doubleValue ?? 0
        dsRect.left = (rectDict["left"] as? NSNumber)?.doubleValue ?? 0
        dsRect.right = (rectDict["right"] as? NSNumber)?.doubleValue ?? 0
        dsRect.bottom = (rectDict["bottom"] as? NSNumber)?.doubleValue ?? 0
        dsRect.measuredInPercentage = (rectDict["measuredInPercentage"] as? NSNumber)?.boolValue ?? false

        let rect = dce?.convertRectToViewCoordinates(dsRect) ?? .zero
        return [
            "x": rect.origin.x,
            "y": rect.origin.y,
            "width": rect.size.width,
            "height": rect.size.height
        ]
    }

    @objc(convertPointToViewCoordinates:)
    func convertPointToViewCoordinates(_ pointDict: NSDictionary) -> NSDictionary {
        let x = (pointDict["x"] as? NSNumber)?.doubleValue ?? 0
        let y = (pointDict["y"] as? NSNumber)?.doubleValue ?? 0
        let point = dce?.convertPointToViewCoordinates(CGPoint(x: x, y: y)) ?? .zero
        return ["x": point.x, "y": point.y]
    }
}
