//
//  CameraView.swift
//  ReactNativeCameraKit
//

import AVFoundation
import AVKit
import React
import UIKit

/*
 * View abtracting the logic unrelated to the actual camera
 * Like permission, ratio overlay, focus, zoom gesture, write image, etc
 */
@objc(CKCameraView)
public class CameraView: UIView {
    private let camera: CameraProtocol

    // Focus
    private let focusInterfaceView: FocusInterfaceView

    // scanner
    private var lastBarcodeDetectedTime: TimeInterval = 0
    private var scannerInterfaceView: ScannerInterfaceView

    // camera
    private var ratioOverlayView: RatioOverlayView?

    // gestures
    private var zoomGestureRecognizer: UIPinchGestureRecognizer?

    // props
    // camera settings
    @objc public var cameraType: CameraType = .back
    @objc public var resizeMode: ResizeMode = .contain
    @objc public var flashMode: FlashMode = .auto
    @objc public var torchMode: TorchMode = .off
    @objc public var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization = .balanced
    // ratio overlay
    @objc public var ratioOverlay: String?
    @objc public var ratioOverlayColor: UIColor?
    // scanner
    @objc public var scanBarcode = false
    @objc public var showFrame = false
    @objc public var onReadCode: RCTDirectEventBlock?
    @objc public var scanThrottleDelay = 2000
    @objc public var frameColor: UIColor?
    @objc public var laserColor: UIColor?
    @objc public var barcodeFrameSize: NSDictionary?
    @objc public var allowedBarcodeTypes: NSArray?

    // other
    @objc public var onOrientationChange: RCTDirectEventBlock?
    @objc public var onZoom: RCTDirectEventBlock?
    @objc public var resetFocusTimeout = 0
    @objc public var resetFocusWhenMotionDetected = false
    @objc public var focusMode: FocusMode = .on
    @objc public var zoomMode: ZoomMode = .on
    @objc public var zoom: NSNumber?
    @objc public var maxZoom: NSNumber?
    @objc public var iOsDeferredStart: Bool = true

    @objc public var onCaptureButtonPressIn: RCTDirectEventBlock?
    @objc public var onCaptureButtonPressOut: RCTDirectEventBlock?

    var eventInteraction: Any? = nil

    // MARK: - Setup

    // This is used to delay camera setup until we have both granted permission & received default props
    var hasCameraBeenSetup = false
    var hasPropBeenSetup = false {
        didSet {
            setupCamera()
        }
    }
    var hasPermissionBeenGranted = false {
        didSet {
            setupCamera()
        }
    }
    private func setupCamera() {
        if hasPropBeenSetup && hasPermissionBeenGranted && !hasCameraBeenSetup {
            let convertedAllowedTypes = convertAllowedBarcodeTypes()

            camera.update(iOsDeferredStartEnabled: iOsDeferredStart)

            hasCameraBeenSetup = true
            #if targetEnvironment(macCatalyst)
                // Force front camera on Mac Catalyst during initial setup
                camera.setup(
                    cameraType: .front,
                    supportedBarcodeType: scanBarcode && onReadCode != nil
                        ? convertedAllowedTypes : [])
            #else
                camera.setup(
                    cameraType: cameraType,
                    supportedBarcodeType: scanBarcode && onReadCode != nil
                        ? convertedAllowedTypes : [])
            #endif
        }
    }

    // Use constraints for FABRIC 0.80.0
    private func addFullSizeSubview(_ subview: UIView) {
        subview.translatesAutoresizingMaskIntoConstraints = false
        addSubview(subview)
        NSLayoutConstraint.activate([
            subview.topAnchor.constraint(equalTo: self.topAnchor),
            subview.bottomAnchor.constraint(equalTo: self.bottomAnchor),
            subview.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            subview.trailingAnchor.constraint(equalTo: self.trailingAnchor),
        ])
    }

    // MARK: Lifecycle

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override init(frame: CGRect) {
        #if targetEnvironment(simulator)
            camera = SimulatorCamera()
        #else
            camera = RealCamera()
        #endif

        scannerInterfaceView = ScannerInterfaceView(frameColor: .white, laserColor: .red)
        focusInterfaceView = FocusInterfaceView()

        super.init(frame: frame)

        // Transfer the default values, otherwise the default wont take effect since it's a separate class
        focusInterfaceView.update(focusMode: focusMode)
        focusInterfaceView.update(resetFocusTimeout: resetFocusTimeout)
        focusInterfaceView.update(resetFocusWhenMotionDetected: resetFocusWhenMotionDetected)
        update(zoomMode: zoomMode)

        addSubview(camera.previewView)

        addSubview(scannerInterfaceView)
        scannerInterfaceView.isHidden = true

        addSubview(focusInterfaceView)

        addFullSizeSubview(camera.previewView)
        addFullSizeSubview(scannerInterfaceView)
        addFullSizeSubview(focusInterfaceView)

        focusInterfaceView.delegate = camera

        handleCameraPermission()

        configureHardwareInteraction()
    }

    private func configureHardwareInteraction() {
        #if !targetEnvironment(macCatalyst)
            // Create a new capture event interaction with a handler that captures a photo.
            if #available(iOS 17.2, *) {
                let interaction = AVCaptureEventInteraction { [weak self] event in
                    // Capture a photo on "press up" of a hardware button.
                    if event.phase == .began {
                        self?.onCaptureButtonPressIn?(nil)
                    } else if event.phase == .ended {
                        self?.onCaptureButtonPressOut?(nil)
                    }
                }
                // Add the interaction to the view controller's view.
                self.addInteraction(interaction)
                eventInteraction = interaction
            }
        #endif
    }

    override public func removeFromSuperview() {
        camera.cameraRemovedFromSuperview()

        super.removeFromSuperview()
    }

    // MARK: React lifecycle

    override public func reactSetFrame(_ frame: CGRect) {
        super.reactSetFrame(frame)
        self.updateSubviewsBounds(frame)
    }

    @objc public func updateSubviewsBounds(_ frame: CGRect) {
        camera.previewView.frame = bounds

        scannerInterfaceView.frame = bounds
        // If frame size changes, we have to update the scanner
        camera.update(scannerFrameSize: showFrame ? scannerInterfaceView.frameSize : nil)

        focusInterfaceView.frame = bounds

        ratioOverlayView?.frame = bounds
    }

    override public func removeReactSubview(_ subview: UIView) {
        subview.removeFromSuperview()
        super.removeReactSubview(subview)
    }

    // Called once when all props have been set, then every time one is updated
    // swiftlint:disable:next cyclomatic_complexity function_body_length
    override public func didSetProps(_ changedProps: [String]) {
        hasPropBeenSetup = true

        // Camera settings
        if changedProps.contains("cameraType") {
            #if targetEnvironment(macCatalyst)
                // Force front camera on Mac Catalyst regardless of what's passed
                camera.update(cameraType: .front)
            #else
                camera.update(cameraType: cameraType)
            #endif
        }
        if changedProps.contains("flashMode") {
            camera.update(flashMode: flashMode)
        }
        if changedProps.contains("cameraType") || changedProps.contains("torchMode") {
            camera.update(torchMode: torchMode)
        }
        if changedProps.contains("maxPhotoQualityPrioritization") {
            camera.update(maxPhotoQualityPrioritization: maxPhotoQualityPrioritization)
        }

        if changedProps.contains("onOrientationChange") {
            camera.update(onOrientationChange: onOrientationChange)
        }

        if changedProps.contains("onZoom") {
            camera.update(onZoom: onZoom)
        }

        if changedProps.contains("resizeMode") {
            camera.update(resizeMode: resizeMode)
        }

        // Ratio overlay
        if changedProps.contains("ratioOverlay") {
            if let ratioOverlay {
                if let ratioOverlayView {
                    ratioOverlayView.setRatio(ratioOverlay)
                } else {
                    ratioOverlayView = RatioOverlayView(
                        frame: bounds, ratioString: ratioOverlay, overlayColor: ratioOverlayColor)
                    addSubview(ratioOverlayView!)
                }
            } else {
                ratioOverlayView?.removeFromSuperview()
                ratioOverlayView = nil
            }
        }

        if changedProps.contains("ratioOverlayColor"), let ratioOverlayColor {
            ratioOverlayView?.setColor(ratioOverlayColor)
        }

        // Scanner
        if changedProps.contains("scanBarcode") || changedProps.contains("onReadCode")
            || changedProps.contains("allowedBarcodeTypes")
        {
            let convertedAllowedTypes: [CodeFormat] = convertAllowedBarcodeTypes()

            camera.isBarcodeScannerEnabled(
                scanBarcode,
                supportedBarcodeTypes: convertedAllowedTypes,
                onBarcodeRead: { [weak self] (barcode, codeFormat) in
                    self?.onBarcodeRead(barcode: barcode, codeFormat: codeFormat)
                })
        }

        if changedProps.contains("showFrame") || changedProps.contains("scanBarcode") {
            DispatchQueue.main.async {
                self.scannerInterfaceView.isHidden = !self.showFrame

                self.camera.update(
                    scannerFrameSize: self.showFrame ? self.scannerInterfaceView.frameSize : nil)
            }
        }

        if changedProps.contains("barcodeFrameSize"), let barcodeFrameSize, showFrame, scanBarcode {
            if let width = barcodeFrameSize["width"] as? CGFloat,
                let height = barcodeFrameSize["height"] as? CGFloat
            {
                scannerInterfaceView.update(frameSize: CGSize(width: width, height: height))
                camera.update(scannerFrameSize: showFrame ? scannerInterfaceView.frameSize : nil)
            }
        }

        if changedProps.contains("laserColor"), let laserColor {
            scannerInterfaceView.update(laserColor: laserColor)
        }

        if changedProps.contains("frameColor"), let frameColor {
            scannerInterfaceView.update(frameColor: frameColor)
        }

        // Others
        if changedProps.contains("iOsDeferredStart") {
            camera.update(iOsDeferredStartEnabled: iOsDeferredStart)
        }
        if changedProps.contains("focusMode") {
            focusInterfaceView.update(focusMode: focusMode)
        }
        if changedProps.contains("resetFocusTimeout") {
            focusInterfaceView.update(resetFocusTimeout: resetFocusTimeout)
        }
        if changedProps.contains("resetFocusWhenMotionDetected") {
            focusInterfaceView.update(resetFocusWhenMotionDetected: resetFocusWhenMotionDetected)
        }

        if changedProps.contains("zoomMode") {
            self.update(zoomMode: zoomMode)
        }

        if changedProps.contains("zoom") {
            camera.update(zoom: zoom?.doubleValue)
        }

        if changedProps.contains("maxZoom") {
            camera.update(maxZoom: maxZoom?.doubleValue)
        }
    }

    // MARK: Public

    @objc public func capture(
        onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
        onError: @escaping (_ error: String) -> Void
    ) {
        camera.capturePicture(
            onWillCapture: { [weak self] in
                // Flash/dim preview to indicate shutter action
                DispatchQueue.main.async {
                    self?.camera.previewView.alpha = 0
                    UIView.animate(
                        withDuration: 0.35,
                        animations: {
                            self?.camera.previewView.alpha = 1
                        })
                }
            },
            onSuccess: { [weak self] imageData, thumbnailData, dimensions in
                DispatchQueue.global(qos: .default).async {
                    self?.writeCaptured(
                        imageData: imageData,
                        thumbnailData: thumbnailData,
                        dimensions: dimensions,
                        onSuccess: onSuccess,
                        onError: onError)

                    self?.focusInterfaceView.resetFocus()
                }
            }, onError: onError)
    }

    // MARK: - Private Helper

    private func update(zoomMode: ZoomMode) {
        if zoomMode == .on {
            if zoomGestureRecognizer == nil {
                let pinchGesture = UIPinchGestureRecognizer(
                    target: self, action: #selector(handlePinchToZoomRecognizer(_:)))
                addGestureRecognizer(pinchGesture)
                zoomGestureRecognizer = pinchGesture
            }
        } else {
            if let zoomGestureRecognizer {
                removeGestureRecognizer(zoomGestureRecognizer)
                self.zoomGestureRecognizer = nil
            }
        }
    }

    private func handleCameraPermission() {
        #if targetEnvironment(macCatalyst)
            // On macOS, camera permissions are handled differently
            if #available(macCatalyst 14.0, *) {
                switch AVCaptureDevice.authorizationStatus(for: .video) {
                case .authorized:
                    hasPermissionBeenGranted = true
                case .notDetermined:
                    AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                        if granted {
                            DispatchQueue.main.async {
                                self?.hasPermissionBeenGranted = true
                            }
                        }
                    }
                default:
                    break
                }
            }
        #else
            // iOS permission handling
            switch AVCaptureDevice.authorizationStatus(for: .video) {
            case .authorized:
                // The user has previously granted access to the camera.
                hasPermissionBeenGranted = true
            case .notDetermined:
                // The user has not yet been presented with the option to grant video access.
                AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                    if granted {
                        self?.hasPermissionBeenGranted = true
                    }
                }
            default:
                // The user has previously denied access.
                break
            }
        #endif
    }

    private func writeCaptured(
        imageData: Data,
        thumbnailData: Data?,
        dimensions: CMVideoDimensions,
        onSuccess: @escaping (_ imageObject: [String: Any]) -> Void,
        onError: @escaping (_ error: String) -> Void
    ) {
        do {
            let temporaryImageFileURL = try saveToTmpFolder(imageData)

            onSuccess([
                "size": imageData.count,
                "uri": temporaryImageFileURL.description,
                "name": temporaryImageFileURL.lastPathComponent,
                "thumb": "",
                "height": dimensions.height,
                "width": dimensions.width,
            ])
        } catch {
            let errorMessage =
                "Error occurred while writing image data to a temporary file: \(error)"
            print(errorMessage)
            onError(errorMessage)
        }
    }

    private func saveToTmpFolder(_ data: Data) throws -> URL {
        let temporaryFileName = ProcessInfo.processInfo.globallyUniqueString
        // Store temporary photos in the 'caches' directory to support expo-file-system
        let cachesUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
        var temporaryFolderURL = cachesUrl
        if let bundleId = Bundle.main.bundleIdentifier {
            temporaryFolderURL = temporaryFolderURL.appendingPathComponent(
                bundleId, isDirectory: true)
        }
        temporaryFolderURL = temporaryFolderURL.appendingPathComponent(
            "com.tesla.react-native-camera-kit", isDirectory: true)
        try FileManager.default.createDirectory(
            at: temporaryFolderURL, withIntermediateDirectories: true)
        let temporaryFileURL = temporaryFolderURL.appendingPathComponent("\(temporaryFileName).jpg")

        try data.write(to: temporaryFileURL, options: .atomic)

        return temporaryFileURL
    }

    private func onBarcodeRead(barcode: String, codeFormat: CodeFormat) {
        // Throttle barcode detection
        let now = Date.timeIntervalSinceReferenceDate
        guard lastBarcodeDetectedTime + Double(scanThrottleDelay) / 1000 < now else {
            return
        }

        lastBarcodeDetectedTime = now

        onReadCode?(["codeStringValue": barcode, "codeFormat": codeFormat.rawValue])
    }

    private func convertAllowedBarcodeTypes() -> [CodeFormat] {
        guard let allowedTypes = allowedBarcodeTypes as? [String], !allowedTypes.isEmpty else {
            return CodeFormat.allCases
        }

        return allowedTypes.compactMap { CodeFormat(rawValue: $0) }
    }

    // MARK: - Gesture selectors

    @objc func handlePinchToZoomRecognizer(_ pinchRecognizer: UIPinchGestureRecognizer) {
        if pinchRecognizer.state == .began {
            camera.zoomPinchStart()
        }
        if pinchRecognizer.state == .changed {
            camera.zoomPinchChange(pinchScale: pinchRecognizer.scale)
        }
    }
}
