//
//  SwiftBarcodeScannerView.swift
//  react-native-barcode-scanner
//
//  Created by Thanh Duy Truong on 08/08/2023.
//

import AVFoundation
import UIKit
import Vision
import Combine

@objc public protocol SwiftBarcodeScannerViewDelegate: AnyObject {
    func swiftBarcodeScannerView(_ swiftBarcodeScannerView: SwiftBarcodeScannerView, didScanCode code: String)
}

@objc public class SwiftBarcodeScannerView: UIView, AVCaptureMetadataOutputObjectsDelegate {
    @objc public weak var delegate: SwiftBarcodeScannerViewDelegate?
    
    private let queue = DispatchQueue(label: "ScannerViewController.SerialQueue")
    
    private let sampleBufferPublisher: PassthroughSubject<CMSampleBuffer, Never> = .init()
    private var cancellables: Set<AnyCancellable> = []
    
    private let borderWidth: CGFloat = 3
    private var focusWidth: CGFloat
    private var focusHeight: CGFloat
    private var focusBorderColor: UIColor?
    private var flashModeEnabled: Bool {
        didSet {
            toggleFlashMode()
        }
    }
    private var acceptedFormats: [String] = []
    
    private let scanView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    private let containerView: UIView = {
        let view = UIView()
        view.layer.cornerRadius = 23 // Set corner radius
        view.clipsToBounds = true   // Clip to bounds
        view.backgroundColor = .black.withAlphaComponent(0.3)
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    private var shapeLayer = CAShapeLayer()
    private var fillLayer = CAShapeLayer()

    private let videoOutput: AVCaptureVideoDataOutput = {
        let videoOutput = AVCaptureVideoDataOutput()
        videoOutput.alwaysDiscardsLateVideoFrames = true
        return videoOutput
    }()
    
    private let videoCaptureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)

    @objc public init(
        focusWidth: NSNumber?,
        focusHeight: NSNumber?,
        focusBorderColor: String?,
        flashModeEnabled: Bool,
        acceptedFormats: [String]
    ) {
        print("init", acceptedFormats)
        self.focusWidth = CGFloat(focusWidth?.doubleValue ?? 0)
        self.focusHeight = CGFloat(focusHeight?.doubleValue ?? 0)
        self.focusBorderColor = UIColor(hexString: focusBorderColor ?? "")
        self.flashModeEnabled = flashModeEnabled
        self.acceptedFormats = acceptedFormats
        super.init(frame: .zero)
        backgroundColor = .systemBackground

        setupViews()
        setupCamera()
        setupSubscriptions()
    }

    @objc public func update(
        focusWidth: NSNumber?,
        focusHeight: NSNumber?,
        focusBorderColor: String?,
        flashModeEnabled: Bool,
        acceptedFormats: [String]
    ) {
        print("update", acceptedFormats)
        self.focusWidth = CGFloat(focusWidth?.doubleValue ?? 0)
        self.focusHeight = CGFloat(focusHeight?.doubleValue ?? 0)
        self.focusBorderColor = UIColor(hexString: focusBorderColor ?? "")
        self.flashModeEnabled = flashModeEnabled
        self.acceptedFormats = acceptedFormats
        
        updateScanViewConstrains()
        setNeedsLayout()
        layoutIfNeeded()
    }

    private func drawCorners() {
        shapeLayer.removeFromSuperlayer()
        
        let focusRect = scanView.frame
        let focusRectX = focusRect.origin.x
        let focusRectY = focusRect.origin.y
        let focusRectWidth = focusRect.width
        let focusRectHeight = focusRect.height
        
        let size: CGFloat = 100
        let radius: CGFloat = 10
        
        // topLeft
        let path = UIBezierPath()
        path.move(to: CGPoint(x: focusRectX + size, y: focusRectY))
        path.addLine(to: CGPoint(x: focusRectX + radius, y: focusRectY))
        path.addArc(
            withCenter: CGPoint(x: focusRectX + radius, y: focusRectY + radius),
            radius: radius,
            startAngle: -CGFloat.pi / 2,
            endAngle: -CGFloat.pi,
            clockwise: false
        )
        path.addLine(to: CGPoint(x: focusRectX, y: focusRectY + size))
        
        // bottomLeft
        path.move(to: CGPoint(x: focusRectX, y: focusRectY + focusRectHeight - size))
        path.addLine(to: CGPoint(x: focusRectX, y: focusRectY + focusRectHeight - radius))
        path.addArc(
            withCenter: CGPoint(x: focusRectX + radius, y: focusRectY + focusRectHeight - radius),
            radius: radius,
            startAngle: CGFloat.pi,
            endAngle: CGFloat.pi / 2,
            clockwise: false
        )
        path.addLine(to: CGPoint(x: focusRectX + size, y: focusRectY + focusRectHeight))
        
        // trailingRight
        path.move(to: CGPoint(x: focusRectX + focusRectWidth - size, y: focusRectY + focusRectHeight))
        path.addLine(to: CGPoint(x: focusRectX + focusRectWidth - radius, y: focusRectY + focusRectHeight))
        path.addArc(
            withCenter: CGPoint(x: focusRectX + focusRectWidth - radius, y: focusRectY + focusRectHeight - radius),
            radius: radius,
            startAngle: CGFloat.pi / 2,
            endAngle: 0,
            clockwise: false
        )
        path.addLine(to: CGPoint(x: focusRectX + focusRectWidth, y: focusRectY + focusRectHeight - size))
        
        // topRight
        path.move(to: CGPoint(x: focusRectX + focusRectWidth, y: focusRectY + size))
        path.addLine(to: CGPoint(x: focusRectX + focusRectWidth, y: focusRectY + radius))
        path.addArc(
            withCenter: CGPoint(x: focusRectX + focusRectWidth - radius, y: focusRectY + radius),
            radius: radius,
            startAngle: 0,
            endAngle: -CGFloat.pi / 2,
            clockwise: false
        )
        path.addLine(to: CGPoint(x: focusRectX + focusRectWidth - size, y: focusRectY))
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        shapeLayer.strokeColor = self.focusBorderColor?.cgColor
        shapeLayer.lineWidth = borderWidth
        shapeLayer.fillColor = UIColor.clear.cgColor
        layer.addSublayer(shapeLayer)
        self.shapeLayer = shapeLayer
    }
    
    private func addOverlayPath() {
        fillLayer.removeFromSuperlayer()
        
        let focusRect = CGRect(
            x: (bounds.width - self.focusWidth - borderWidth) / 2,
            y: (bounds.height - self.focusHeight - borderWidth) / 2,
            width: self.focusWidth + borderWidth,
            height: self.focusHeight + borderWidth
        )
        let overlayPath = UIBezierPath(rect: bounds)
        let transparentPath = UIBezierPath(roundedRect: focusRect, cornerRadius: 12)
        overlayPath.append(transparentPath)
        overlayPath.usesEvenOddFillRule = true
        let fillLayer = CAShapeLayer()
        fillLayer.path = overlayPath.cgPath
        fillLayer.fillRule = .evenOdd
        fillLayer.fillColor = UIColor(white: 0, alpha: 0.5).cgColor
        layer.insertSublayer(fillLayer, below: containerView.layer)
        
        self.fillLayer = fillLayer
    }
    
    private func updateScanViewConstrains() {
        scanViewWidthConstraint?.constant = focusWidth
        scanViewHeightConstraint?.constant = focusHeight
        scanView.updateConstraintsIfNeeded()
    }
    
    private var scanViewWidthConstraint: NSLayoutConstraint?
    private var scanViewHeightConstraint: NSLayoutConstraint?
    
    let label = UILabel()
     
    private func setupViews() {
        addSubview(scanView)
        let scanViewWidthConstraint = scanView.widthAnchor.constraint(equalToConstant: focusWidth)
        let scanViewHeightConstraint = scanView.heightAnchor.constraint(equalToConstant: focusHeight)
        NSLayoutConstraint.activate([
            scanView.centerXAnchor.constraint(equalTo: centerXAnchor),
            scanView.centerYAnchor.constraint(equalTo: centerYAnchor),
            scanViewWidthConstraint,
            scanViewHeightConstraint,
        ])
        self.scanViewWidthConstraint = scanViewWidthConstraint
        self.scanViewHeightConstraint = scanViewHeightConstraint
        
        
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Center barcode in scanner."
        label.textColor = .white
        label.font = .systemFont(ofSize: 16, weight: .medium)
        addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: scanView.centerXAnchor),
            label.bottomAnchor.constraint(equalTo: scanView.topAnchor, constant: -32),
            
        ])
        
        insertSubview(containerView, belowSubview: label)
        NSLayoutConstraint.activate([
            containerView.centerXAnchor.constraint(equalTo: label.centerXAnchor),
            containerView.centerYAnchor.constraint(equalTo: label.centerYAnchor),
            containerView.leadingAnchor.constraint(equalTo: label.leadingAnchor, constant: -16),
        containerView.topAnchor.constraint(equalTo: label.topAnchor, constant: -12),
        ])
        
        
        let blurEffect = UIBlurEffect(style: .regular)
        let blurView = UIVisualEffectView(effect: blurEffect)
        blurView.layer.cornerRadius = 23
        blurView.layer.backgroundColor = UIColor.black.withAlphaComponent(0.3).cgColor
        blurView.translatesAutoresizingMaskIntoConstraints = false

        containerView.addSubview(blurView)
        NSLayoutConstraint.activate([
            blurView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            blurView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            blurView.topAnchor.constraint(equalTo: containerView.topAnchor),
            blurView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
        ])
    }
    
    public override func layoutSubviews() {
        super.layoutSubviews()
        drawCorners()
        addOverlayPath()
        previewLayer?.frame = layer.bounds
    }
    
    private var previewLayer: AVCaptureVideoPreviewLayer?

    private func setupCamera() {
        let captureSession = AVCaptureSession()
        captureSession.canSetSessionPreset(.hd1280x720)
        
        guard let videoCaptureDevice else { return }
        
        toggleFlashMode()
        
        do {
            try videoCaptureDevice.lockForConfiguration()
            let desiredFPS: Double = 15
            videoCaptureDevice.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: Int32(desiredFPS)) // 15 FPS
            videoCaptureDevice.unlockForConfiguration()
        } catch {
            // Handle error
        }
        let videoInput: AVCaptureDeviceInput
        
        do {
            videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
        } catch {
            return
        }
        
        if (captureSession.canAddInput(videoInput)) {
            captureSession.addInput(videoInput)
        } else {
            failed()
            return
        }
        
        if (captureSession.canAddOutput(videoOutput)) {
            captureSession.addOutput(videoOutput)
            
            videoOutput.setSampleBufferDelegate(self, queue: queue)
        } else {
            failed()
            return
        }
        
        let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = layer.bounds
        previewLayer.videoGravity = .resizeAspectFill
        layer.insertSublayer(previewLayer, at: 0)
        self.previewLayer = previewLayer

        DispatchQueue.global(qos: .background).async {
            captureSession.startRunning()
        }
        
        sampleBufferPublisher
            .throttle(for: .seconds(0.5), scheduler: queue, latest: true)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in
                self?.detectBarcodeAndQRCode(from: $0)
            }
            .store(in: &cancellables)
    }
    
    private func setupSubscriptions() {
        NotificationCenter.default
            .publisher(for: UIDevice.orientationDidChangeNotification)
            .map { _ in Self.getCurrentVideoOrientation() }
            .prepend(Self.getCurrentVideoOrientation())
            .removeDuplicates()
            .subscribe(on: DispatchQueue.main)
            .sink { [weak self] videoOrientation in
                self?.previewLayer?.connection?.videoOrientation = videoOrientation
            }
            .store(in: &cancellables)
    }
    
    private static func getCurrentVideoOrientation() -> AVCaptureVideoOrientation {
        let interfaceOrientation = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?
            .windowScene?.interfaceOrientation
        switch interfaceOrientation {
        case .portrait, .unknown, .none:
            return .portrait
        case .portraitUpsideDown:
            return .portraitUpsideDown
        case .landscapeLeft:
            return .landscapeLeft
        case .landscapeRight:
            return .landscapeRight
        @unknown default:
            return .portrait
        }
    }
    
    // MARK: - APIs
    
    private func toggleFlashMode() {
        // Check device has torch or not
        guard let device = videoCaptureDevice, device.hasTorch else { return }
        videoCaptureDevice?.torchMode = self.flashModeEnabled ? .on : .off
    }
    
    func found(code: String) {
        print("Scanned code: ", code)
        delegate?.swiftBarcodeScannerView(self, didScanCode: code)
    }
    
    private func failed() {
        let ac = UIAlertController(title: "Scanning not supported", message: "Your device does not support scanning a code from an item. Please use a device with a camera.", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK", style: .default))
    }
    
    private func mapToVNBarcodeSymbology(_ format: String) -> VNBarcodeSymbology? {
        switch format {
        case "QR_CODE":
            return .QR
        case "CODE_39":
            return .Code39
        case "CODE_93":
            return .Code93
        case "CODE_128":
            return .Code128
        case "EAN_13":
            return .EAN13
        case "EAN_8":
            return .EAN8
        case "UPC_E":
            return .UPCE
        case "PDF":
            return .PDF417
        case "AZTEC":
            return .Aztec
        case "UPC_A":
            return .EAN13
        default:
            return nil
        }
    }

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

extension SwiftBarcodeScannerView: AVCaptureVideoDataOutputSampleBufferDelegate {
    public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        sampleBufferPublisher.send(sampleBuffer)
    }
    
    fileprivate func detectBarcodeAndQRCode(from sampleBuffer: CMSampleBuffer) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        let barcodesRequest = VNDetectBarcodesRequest { [weak self] request, error in
            if let error = error as NSError? {
                print("Error in detecting - \(error)")
                return
            }
            else {
                guard let observations = request.results as? [VNBarcodeObservation],
                      !observations.isEmpty
                else {
                    return
                }
                observations.map { $0.payloadStringValue }.forEach { payloadStringValue in
                    if let payloadStringValue {
                        self?.found(code: payloadStringValue)
                    }
                }
            }
        }
        
        if let previewLayer = previewLayer {
            let scanFrame = CGRect(x:(bounds.width - focusWidth) / 2, y:(bounds.height - focusHeight) / 2, width:focusWidth, height: focusHeight);
            let intersestFrame = previewLayer.metadataOutputRectConverted(fromLayerRect: scanFrame)
            barcodesRequest.regionOfInterest = intersestFrame
        } else {
            return
        }
        
        let supportedSymbologies: [VNBarcodeSymbology] = acceptedFormats.compactMap { format in
            mapToVNBarcodeSymbology(format)
        }
        
        barcodesRequest.symbologies = supportedSymbologies
        let requestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)

        queue.async {
            do {
                try requestHandler.perform([barcodesRequest])
            } catch {
                NSLog(error.localizedDescription)
            }
        }
    }
}

extension UIColor {
    convenience init?(hexString: String) {
        var cleanedString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
        cleanedString = cleanedString.replacingOccurrences(of: "#", with: "")

        var rgbValue: UInt64 = 0
        Scanner(string: cleanedString).scanHexInt64(&rgbValue)

        let red = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0
        let green = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0
        let blue = CGFloat(rgbValue & 0x0000FF) / 255.0

        self.init(red: red, green: green, blue: blue, alpha: 1.0)
    }
}
