import Foundation
import ScanbotSDK
import ScanbotSDKNativeWrapper

@objc public class RNScanbotBarcodeScannerViewController: UIViewController {

    private let scannerViewController = SBSDKBarcodeScannerViewController()
    private var defaults = RNScanbotBarcodeScannerDefaults()
    private var barcodeItemOverlayViewBinders: [String: BarcodeItemOverlayViewConfig]? = nil
    private var barcodeItemOverlayLoadingText: String? = nil

    @objc public var delegate: RNScanbotBarcodeScannerEventDelegate? = nil

    public override func viewDidLoad() {
        super.viewDidLoad()

        scannerViewController.delegate = self
        scannerViewController.trackingOverlayController.delegate = self
        self.sbsdk_attach(scannerViewController, in: self.view)
    }

    @objc public func setDefaults(){
        hardwareButtonsEnabled(defaults.hardwareButtonsEnabled)

        //Finder Configs
        finderOverlayColor(defaults.finderOverlayColor)
        finderStrokeColor(defaults.finderStrokeColor)
        finderRequiredAspectRatios()
        finderInsets()

        //Overlay Configs
        overlayPolygonColor(defaults.overlayPolygonColor)
        overlayStrokeColor(defaults.overlayStrokeColor)
        overlayTextColor(defaults.overlayTextColor)
        overlayTextContainerColor(defaults.overlayTextContainerColor)
        overlayTextFormat()
    }

    @objc public func prepareForRecycle(){
        scannerViewController.delegate = nil
        scannerViewController.trackingOverlayController.delegate = nil
        barcodeItemOverlayViewBinders = nil
    }

    // MARK: General settings

    @objc public func flashEnabled(_ enabled: Bool) {
        scannerViewController.isFlashLightEnabled = enabled
    }

    @objc public func hardwareButtonsEnabled(_ enabled: Bool) {
        scannerViewController.hardwareButtonsEnabled = enabled
    }

    @objc public func scanningEnabled(_ enabled: Bool) {
        scannerViewController.isScanningEnabled = enabled
    }

    // MARK: Finder Configuration

    @objc public func finderEnabled(_ enabled: Bool) {
        scannerViewController.viewFinderConfiguration.isViewFinderEnabled = enabled
    }

    @objc public func finderStrokeWidth(_ width: NSNumber?) {
        scannerViewController.viewFinderConfiguration.lineWidth = width?.doubleValue ?? defaults.finderStrokeWidth
    }

    @objc public func finderStrokeColor(_ value: UIColor?) {
        scannerViewController.viewFinderConfiguration.lineColor = value ?? defaults.finderStrokeColor
    }

    @objc public func finderOverlayColor(_ value: UIColor?) {
        scannerViewController.viewFinderConfiguration.backgroundColor = value ?? defaults.finderOverlayColor
    }

    @objc public func finderInsets(_ value: NSDictionary? = nil){
        if let dict = value {
            let left = (dict["left"] as? Double) ?? defaults.finderInset.left
            let top = (dict["top"] as? Double) ?? defaults.finderInset.top
            let right = (dict["right"] as? Double) ?? defaults.finderInset.right
            let bottom = (dict["bottom"] as? Double) ?? defaults.finderInset.bottom

            finderInsets(left: left, top: top, right: right, bottom: bottom)
        } else {
            scannerViewController.viewFinderConfiguration.minimumInset = defaults.finderInset
        }
    }

    @objc public func finderInsets(left: Double, top: Double, right: Double, bottom: Double) {
        scannerViewController.viewFinderConfiguration.minimumInset = UIEdgeInsets(
            top: top,left: left,bottom: bottom, right: right
        )
    }

    @objc public func finderRequiredAspectRatios(_ value: NSDictionary? = nil) {
        if let dict = value {
            let width = (dict["width"] as? Double) ?? defaults.finderAspectRatio.width
            let height = (dict["height"] as? Double) ?? defaults.finderAspectRatio.height

            finderRequiredAspectRatios(width: width, height: height)
        } else {
            scannerViewController.viewFinderConfiguration.aspectRatio = defaults.finderAspectRatio
        }
    }

    @objc public func finderRequiredAspectRatios(width: Double, height: Double) {
        scannerViewController.viewFinderConfiguration.aspectRatio = SBSDKAspectRatio(
            width: width, height: height
        )
    }

    // MARK: Camera Configuration

    @objc public func cameraZoomFactor(_ zoomFactor: NSNumber?) {
        scannerViewController.zoomConfiguration.initialZoomFactor = zoomFactor?.doubleValue ?? defaults.cameraZoomFactor
    }

    @objc public func cameraZoomRange(_ value: NSDictionary?) {
        if let dict = value {
            let minZoom = (dict["minZoom"] as? Double) ?? defaults.cameraZoomRange.minZoom
            let maxZoom = (dict["maxZoom"] as? Double) ?? defaults.cameraZoomRange.maxZoom

            cameraZoomRange(minZoom: minZoom, maxZoom: maxZoom)
        } else {
            scannerViewController.zoomConfiguration.zoomRange = defaults.cameraZoomRange
        }
    }

    @objc public func cameraZoomRange(minZoom: Double, maxZoom: Double) {
        scannerViewController.zoomConfiguration.zoomRange = SBSDKZoomRange(
            minZoom: minZoom, maxZoom: maxZoom
        )
    }

    @objc public func cameraDevice(_ device: NSString?) {
        if let device = device {
            let cameraDevice = SBComponentUtils.cameraDeviceToNative(device as String)
            scannerViewController.cameraDevice = cameraDevice
        } else {
            scannerViewController.cameraDevice = .defaultBackFacingCamera
        }
    }

    @objc public func minFocusDistanceLock(_ enabled: Bool) {
        if(enabled){
            scannerViewController.beginFocusLockAt(lensPosition: 0.0)
        } else {
            scannerViewController.endFocusLock()
        }
    }

    // MARK: Overlay Configuration

    @objc public func overlayEnabled(_ enabled: Bool) {
        scannerViewController.isTrackingOverlayEnabled = enabled
    }

    @objc public func overlayPolygonColor(_ value: UIColor?) {
        scannerViewController.trackingOverlayController.configuration.polygonStyle.polygonBackgroundColor = value ?? defaults.overlayPolygonColor
    }

    @objc public func overlayStrokeColor(_ value: UIColor?) {
        scannerViewController.trackingOverlayController.configuration.polygonStyle.polygonColor = value ?? defaults.overlayStrokeColor
    }

    @objc public func overlayTextColor(_ value: UIColor?) {
        scannerViewController.trackingOverlayController.configuration.textStyle.textColor = value ?? defaults.overlayTextColor
    }

    @objc public func overlayTextContainerColor(_ value: UIColor?) {
        scannerViewController.trackingOverlayController.configuration.textStyle.textBackgroundColor = value ?? defaults.overlayTextContainerColor
    }

    @objc public func overlayTextFormat(_ format: NSString? = nil) {
        if let format = format {
            scannerViewController.trackingOverlayController.configuration.textStyle.trackingOverlayTextFormat = SBComponentUtils.barcodeOverlayTextFormatToNative(format as String)
        } else {
            scannerViewController.trackingOverlayController.configuration.textStyle.trackingOverlayTextFormat = defaults.overlayTextFormat
        }
    }

    @objc public func overlayLoadingTextValue(_ value: NSString?) {
        self.barcodeItemOverlayLoadingText = value as String?
    }

    // MARK: Barcode Configuration

    @objc public func configExtractedDocumentFormats(_ formats: NSArray?) {
        let configuration = scannerViewController.copyCurrentConfiguration()

        if let formats = formats as? [String] {
            configuration.extractedDocumentFormats = SBComponentUtils.barcodeDocumentFormatToNative(formats)
        } else {
            configuration.extractedDocumentFormats = defaults.configExtractedDocumentFormats
        }

        scannerViewController.setConfiguration(configuration)
    }

    @objc public func configFormatConfigurations(_ formats: [NSDictionary]) {
        let configuration = scannerViewController.copyCurrentConfiguration()
        configuration.barcodeFormatConfigurations = formats.compactMap {
            if let configDict = $0 as? [String: Any],
               let config = try? SBConfiguration(configDict).convert(to: SBSDKBarcodeFormatConfigurationBase.self){
                return config
            }
            delegate?.onError("Unable to decode format configurations", errorCode: SBSDKErrorType.invalidData.rawValue)
            return nil
        }
        scannerViewController.setConfiguration(configuration)
    }

    @objc public func configOnlyAcceptDocuments(_ onlyAcceptDocuments: Bool) {
        let configuration = scannerViewController.copyCurrentConfiguration()
        configuration.onlyAcceptDocuments = onlyAcceptDocuments
        scannerViewController.setConfiguration(configuration)
    }

    @objc public func configReturnBarcodeImage(_ returnBarcodeImage: Bool) {
        let configuration = scannerViewController.copyCurrentConfiguration()
        configuration.returnBarcodeImage = returnBarcodeImage
        scannerViewController.setConfiguration(configuration)
    }

    @objc public func configEngineMode(_ engineMode: NSString?) {
        let configuration = scannerViewController.copyCurrentConfiguration()

        if let engineMode = engineMode {
            configuration.engineMode = SBComponentUtils.barcodeEngineModeToNative(engineMode as String)
        } else {
            configuration.engineMode = defaults.configEngineMode
        }

        scannerViewController.setConfiguration(configuration)
    }

    @objc public func configAccumulationConfiguration(_ config: NSDictionary?) {
        if let dict = config {
            let accumulationTime = (dict["accumulationTime"] as? Int) ?? defaults.configAccumulationConfigTime
            let removeUnconnectedResults = dict["removeUnconnectedResults"] as? Bool ?? defaults.configAccumulationConfigRemoveUnconnectedResults
            let method = dict["method"] as? NSString ?? defaults.configAccumulationConfigMethod

            configAccumulationConfiguration(
                accumulationTime: accumulationTime,
                removeUnconnectedResults: removeUnconnectedResults,
                method: method
            )

        } else {
            configAccumulationConfiguration(
                accumulationTime: defaults.configAccumulationConfigTime,
                removeUnconnectedResults: defaults.configAccumulationConfigRemoveUnconnectedResults,
                method: defaults.configAccumulationConfigMethod
            )
        }
    }

    @objc public func configAccumulationConfiguration(accumulationTime: Int, removeUnconnectedResults: Bool, method: NSString) {
        let configuration = scannerViewController.copyCurrentConfiguration()
        configuration.accumulationConfig.accumulationTime = accumulationTime
        configuration.accumulationConfig.removeUnconnectedResults = removeUnconnectedResults
        if let method = SBComponentUtils.decodeFromString(type: SBSDKBarcodeAccumulationMethod.self, value: method as String) {
            configuration.accumulationConfig.method = method
        } else {
            configuration.accumulationConfig.method = .interpolateByCamera
        }
        scannerViewController.setConfiguration(configuration)
    }

    @objc public func configOptimizedForOverlays(_ optimizedForOverlays: Bool) {
        let configuration = scannerViewController.copyCurrentConfiguration()
        configuration.optimizedForOverlays = optimizedForOverlays
        scannerViewController.setConfiguration(configuration)
    }

    // MARK: Commands

    @objc public func freezeCamera() {
        scannerViewController.freezeCamera()
    }

    @objc public func unfreezeCamera() {
        scannerViewController.unfreezeCamera()
    }

    // MARK: Barcode Item Overlay

    @objc public func bindBarcodeItemOverlayView(barcodeID: NSString, bindingConfigJson: NSString) {
        if self.barcodeItemOverlayViewBinders?[barcodeID as String] != nil {

            if let data = (bindingConfigJson as String).data(using: .utf8),
               var updatedBoundItem = try? JSONDecoder().decode(BarcodeItemOverlayViewConfig.self, from: data){
                updatedBoundItem.lastBind = Date().timeIntervalSince1970 * 1000.0

                self.barcodeItemOverlayViewBinders?[barcodeID as String] = updatedBoundItem
            } else {
                delegate?.onError(error: SBError.argumentError(message: "Unable to decode binder configuration"))
            }
        }
    }

    @objc public func overlayViewBinder(_ enabled: Bool){
        if(enabled){
            barcodeItemOverlayViewBinders = [:]
        } else {
            barcodeItemOverlayViewBinders = nil
        }
    }

    private func addBarcodeItemOverlayViewInitialVinder(_ barcodes: [SBSDKBarcodeItem]){
        for barcode in barcodes {
            if barcodeItemOverlayViewBinders?[barcode.uuid] == nil {
                barcodeItemOverlayViewBinders?[barcode.uuid] = BarcodeItemOverlayViewConfig(text: barcodeItemOverlayLoadingText)
            }
        }
    }

    private func refreshBarcodeItemOverlayViewConfig(_ config: BarcodeItemOverlayViewConfig) -> Bool {
        if let refreshRate = config.refreshRate, refreshRate > 0,
           let lastBind = config.lastBind, lastBind > 0 {
            let currentTime = (Date().timeIntervalSince1970 * 1000.0)
            return  ( currentTime - lastBind ) >= refreshRate
        }

        return false
    }

    // MARK: Utils

    private func barcodeResultToString(_ result: [SBSDKBarcodeItem]) -> String? {

        if let barcodes = SBComponentUtils.nativeResultsToDictionary(result),
           let data = try? JSONSerialization.data(withJSONObject: barcodes),
           let jsonString = String(data: data, encoding: .utf8) {
            return jsonString
        }

        return nil
    }

    @objc public func attachController(parentViewController: UIViewController?, inView view: UIView){
        parentViewController?.sbsdk_attach(self, in: view)
    }

    @objc public func detachController(){
        self.sbsdk_detach(self)
    }
}

//MARK: Delegates

extension RNScanbotBarcodeScannerViewController: SBSDKBarcodeScannerViewControllerDelegate {

    public func barcodeScannerController(_ controller: SBSDKBarcodeScannerViewController,
                                  didScanBarcodes codes: [SBSDKBarcodeItem]) {

        if let jsonString = barcodeResultToString(codes) {
            delegate?.onScannedBarcode(jsonString)
            addBarcodeItemOverlayViewInitialVinder(codes)
        } else {
            delegate?.onError(error: SBError.resultError(reason: "Unable to process barcode data"))
        }
    }

    public func barcodeScannerController(_ controller: SBSDKBarcodeScannerViewController, didFailScanning error: any Error) {
        delegate?.onError(error: SBError(error: error))
    }
}

extension RNScanbotBarcodeScannerViewController: SBSDKBarcodeTrackingOverlayControllerDelegate {

    public func barcodeTrackingOverlay(_ controller: SBSDKBarcodeTrackingOverlayController, didTapOnBarcode barcode: SBSDKBarcodeItem) {
        if let jsonString = barcodeResultToString([barcode]) {
            delegate?.onSelectedBarcode(jsonString)
        } else {
            delegate?.onError(error: SBError.resultError(reason: "Unable to process barcode data"))
        }
    }

    public func barcodeTrackingOverlay(_ controller: SBSDKBarcodeTrackingOverlayController, polygonStyleFor barcode: SBSDKBarcodeItem, proposedStyle: SBSDKBarcodeTrackedViewPolygonStyle) -> SBSDKBarcodeTrackedViewPolygonStyle {

        if let boundItem = self.barcodeItemOverlayViewBinders?[barcode.uuid] {

            guard !refreshBarcodeItemOverlayViewConfig(boundItem)
            else { return proposedStyle }

            if let strokeColor = boundItem.strokeColor {
                proposedStyle.polygonColor = SBComponentUtils.fromRgbAHex(strokeColor)
            }

            if let polygonColor = boundItem.polygonColor {
                proposedStyle.polygonBackgroundColor = SBComponentUtils.fromRgbAHex(polygonColor)
            }
        }

        return proposedStyle
    }

    public func barcodeTrackingOverlay(_ controller: SBSDKBarcodeTrackingOverlayController, textStyleFor barcode: SBSDKBarcodeItem, proposedStyle: SBSDKBarcodeTrackedViewTextStyle) -> SBSDKBarcodeTrackedViewTextStyle {

        if let boundItem = self.barcodeItemOverlayViewBinders?[barcode.uuid] {

            guard !refreshBarcodeItemOverlayViewConfig(boundItem)
            else { return proposedStyle }

            if let textColor = boundItem.textColor {
                proposedStyle.textColor = SBComponentUtils.fromRgbAHex(textColor)
            }

            if let textContainerColor = boundItem.textContainerColor {
                proposedStyle.textBackgroundColor = SBComponentUtils.fromRgbAHex(textContainerColor)
            }
        }

        return proposedStyle
    }

    public func barcodeTrackingOverlay(_ controller: SBSDKBarcodeTrackingOverlayController, overrideTextFor barcode: SBSDKBarcodeItem, proposedString: String) -> String {

        if let boundItem = self.barcodeItemOverlayViewBinders?[barcode.uuid] ,
           let text = boundItem.text {
            return text
        }

        return proposedString
    }

}
