package com.qrcodestudio;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;

import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;

import com.google.android.gms.tasks.Task;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.barcode.BarcodeScanner;
import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
import com.google.mlkit.vision.barcode.BarcodeScanning;
import com.google.mlkit.vision.barcode.common.Barcode;
import com.google.mlkit.vision.common.InputImage;
import com.getcapacitor.JSObject;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class QRCodeScanner {
    private static final String TAG = "QRCodeScanner";
    
    private final Context context;
    private final Activity activity;
    private Camera camera;
    private PreviewView previewView;
    private ProcessCameraProvider cameraProvider;
    private ImageAnalysis imageAnalysis;
    private ExecutorService cameraExecutor;
    private boolean isScanning = false;
    private boolean torchEnabled = false;
    private int lensFacing = CameraSelector.LENS_FACING_BACK;
    
    // Barcode scanner
    private BarcodeScanner scanner;
    private ScanCallback scanCallback;
    
    // Scanner options
    private List<Integer> scanFormats;
    
    public interface ScanCallback {
        void onScanSuccess(String result);
        void onScanError(Exception error);
    }
    
    public interface DetailedScanCallback {
        void onScanSuccess(List<JSObject> results);
        void onScanError(Exception error);
    }
    
    public QRCodeScanner(Context context, Activity activity) {
        this.context = context;
        this.activity = activity;
        this.cameraExecutor = Executors.newSingleThreadExecutor();
        this.scanFormats = new ArrayList<>();
        this.scanFormats.add(Barcode.FORMAT_QR_CODE);
        
        initializeScanner();
    }
    
    private void initializeScanner() {
        BarcodeScannerOptions.Builder optionsBuilder = new BarcodeScannerOptions.Builder();
        
        if (!scanFormats.isEmpty()) {
            int[] formats = new int[scanFormats.size()];
            for (int i = 0; i < scanFormats.size(); i++) {
                formats[i] = scanFormats.get(i);
            }
            optionsBuilder.setBarcodeFormats(formats[0], formats);
        }
        
        scanner = BarcodeScanning.getClient(optionsBuilder.build());
    }
    
    public void setScanFormats(List<String> formats) {
        scanFormats.clear();
        
        for (String format : formats) {
            switch (format) {
                case "QR_CODE":
                    scanFormats.add(Barcode.FORMAT_QR_CODE);
                    break;
                case "AZTEC":
                    scanFormats.add(Barcode.FORMAT_AZTEC);
                    break;
                case "CODE_128":
                    scanFormats.add(Barcode.FORMAT_CODE_128);
                    break;
                case "CODE_39":
                    scanFormats.add(Barcode.FORMAT_CODE_39);
                    break;
                case "CODE_93":
                    scanFormats.add(Barcode.FORMAT_CODE_93);
                    break;
                case "CODABAR":
                    scanFormats.add(Barcode.FORMAT_CODABAR);
                    break;
                case "DATA_MATRIX":
                    scanFormats.add(Barcode.FORMAT_DATA_MATRIX);
                    break;
                case "EAN_13":
                    scanFormats.add(Barcode.FORMAT_EAN_13);
                    break;
                case "EAN_8":
                    scanFormats.add(Barcode.FORMAT_EAN_8);
                    break;
                case "ITF":
                    scanFormats.add(Barcode.FORMAT_ITF);
                    break;
                case "PDF_417":
                    scanFormats.add(Barcode.FORMAT_PDF417);
                    break;
                case "UPC_A":
                    scanFormats.add(Barcode.FORMAT_UPC_A);
                    break;
                case "UPC_E":
                    scanFormats.add(Barcode.FORMAT_UPC_E);
                    break;
            }
        }
        
        initializeScanner();
    }
    
    public void startScanning(ViewGroup container, ScanCallback callback) {
        this.scanCallback = callback;
        this.isScanning = true;
        
        // Check camera permission
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            callback.onScanError(new SecurityException("Camera permission not granted"));
            return;
        }
        
        // Create preview view
        previewView = new PreviewView(context);
        previewView.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
        ));
        container.addView(previewView);
        
        // Start camera
        startCamera();
    }
    
    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = 
                ProcessCameraProvider.getInstance(context);
        
        cameraProviderFuture.addListener(() -> {
            try {
                cameraProvider = cameraProviderFuture.get();
                bindCameraUseCases();
            } catch (ExecutionException | InterruptedException e) {
                Log.e(TAG, "Error starting camera", e);
                if (scanCallback != null) {
                    scanCallback.onScanError(e);
                }
            }
        }, ContextCompat.getMainExecutor(context));
    }
    
    private void bindCameraUseCases() {
        if (cameraProvider == null) return;
        
        // Preview
        Preview preview = new Preview.Builder()
                .build();
        
        // Image analysis
        imageAnalysis = new ImageAnalysis.Builder()
                .setTargetResolution(new Size(1280, 720))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build();
        
        imageAnalysis.setAnalyzer(cameraExecutor, new QRCodeAnalyzer());
        
        // Camera selector
        CameraSelector cameraSelector = new CameraSelector.Builder()
                .requireLensFacing(lensFacing)
                .build();
        
        try {
            // Unbind all use cases before rebinding
            cameraProvider.unbindAll();
            
            // Bind use cases to camera
            camera = cameraProvider.bindToLifecycle(
                    (LifecycleOwner) activity,
                    cameraSelector,
                    preview,
                    imageAnalysis
            );
            
            // Attach preview to view
            preview.setSurfaceProvider(previewView.getSurfaceProvider());
            
            // Set torch state
            if (camera.getCameraInfo().hasFlashUnit()) {
                camera.getCameraControl().enableTorch(torchEnabled);
            }
            
        } catch (Exception e) {
            Log.e(TAG, "Failed to bind camera use cases", e);
            if (scanCallback != null) {
                scanCallback.onScanError(e);
            }
        }
    }
    
    public void stopScanning() {
        isScanning = false;
        
        if (cameraProvider != null) {
            cameraProvider.unbindAll();
        }
        
        if (previewView != null && previewView.getParent() != null) {
            ((ViewGroup) previewView.getParent()).removeView(previewView);
        }
    }
    
    public void pauseScanning() {
        isScanning = false;
    }
    
    public void resumeScanning() {
        isScanning = true;
    }
    
    public void toggleTorch() {
        if (camera != null && camera.getCameraInfo().hasFlashUnit()) {
            torchEnabled = !torchEnabled;
            camera.getCameraControl().enableTorch(torchEnabled);
        }
    }
    
    public boolean isTorchEnabled() {
        return torchEnabled;
    }
    
    public boolean isTorchAvailable() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            try {
                CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
                String[] cameraIds = cameraManager.getCameraIdList();
                for (String id : cameraIds) {
                    CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
                    Boolean flashAvailable = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
                    if (flashAvailable != null && flashAvailable) {
                        return true;
                    }
                }
            } catch (CameraAccessException e) {
                Log.e(TAG, "Failed to check torch availability", e);
            }
        }
        return false;
    }
    
    public void flipCamera() {
        lensFacing = (lensFacing == CameraSelector.LENS_FACING_BACK) 
                ? CameraSelector.LENS_FACING_FRONT 
                : CameraSelector.LENS_FACING_BACK;
        bindCameraUseCases();
    }
    
    public void setZoom(float zoomRatio) {
        if (camera != null) {
            camera.getCameraControl().setZoomRatio(zoomRatio);
        }
    }
    
    public void scanFromImage(Uri imageUri, ScanCallback callback) {
        try {
            InputImage image = InputImage.fromFilePath(context, imageUri);
            scanImage(image, callback);
        } catch (IOException e) {
            callback.onScanError(e);
        }
    }
    
    public void scanFromBitmap(Bitmap bitmap, ScanCallback callback) {
        InputImage image = InputImage.fromBitmap(bitmap, 0);
        scanImage(image, callback);
    }
    
    private void scanImage(InputImage image, ScanCallback callback) {
        Task<List<Barcode>> result = scanner.process(image)
                .addOnSuccessListener(barcodes -> {
                    if (!barcodes.isEmpty()) {
                        Barcode barcode = barcodes.get(0);
                        String rawValue = barcode.getRawValue();
                        if (rawValue != null) {
                            callback.onScanSuccess(rawValue);
                        } else {
                            callback.onScanError(new Exception("No QR code found"));
                        }
                    } else {
                        callback.onScanError(new Exception("No QR code found"));
                    }
                })
                .addOnFailureListener(callback::onScanError);
    }
    
    public void scanFromImageWithDetails(Uri imageUri, DetailedScanCallback callback) {
        try {
            InputImage image = InputImage.fromFilePath(context, imageUri);
            scanImageWithDetails(image, callback);
        } catch (IOException e) {
            callback.onScanError(e);
        }
    }
    
    private void scanImageWithDetails(InputImage image, DetailedScanCallback callback) {
        Task<List<Barcode>> result = scanner.process(image)
                .addOnSuccessListener(barcodes -> {
                    List<JSObject> results = new ArrayList<>();
                    for (Barcode barcode : barcodes) {
                        JSObject barcodeResult = new JSObject();
                        barcodeResult.put("format", getBarcodeFormatString(barcode.getFormat()));
                        barcodeResult.put("rawValue", barcode.getRawValue());
                        barcodeResult.put("displayValue", barcode.getDisplayValue());
                        
                        // Add bounding box if available
                        if (barcode.getBoundingBox() != null) {
                            JSObject boundingBox = new JSObject();
                            boundingBox.put("left", barcode.getBoundingBox().left);
                            boundingBox.put("top", barcode.getBoundingBox().top);
                            boundingBox.put("right", barcode.getBoundingBox().right);
                            boundingBox.put("bottom", barcode.getBoundingBox().bottom);
                            barcodeResult.put("boundingBox", boundingBox);
                        }
                        
                        // Add corner points if available
                        if (barcode.getCornerPoints() != null && barcode.getCornerPoints().length > 0) {
                            JSObject[] corners = new JSObject[barcode.getCornerPoints().length];
                            for (int i = 0; i < barcode.getCornerPoints().length; i++) {
                                JSObject point = new JSObject();
                                point.put("x", barcode.getCornerPoints()[i].x);
                                point.put("y", barcode.getCornerPoints()[i].y);
                                corners[i] = point;
                            }
                            barcodeResult.put("cornerPoints", corners);
                        }
                        
                        // Add value type
                        barcodeResult.put("valueType", getBarcodeValueTypeString(barcode.getValueType()));
                        
                        results.add(barcodeResult);
                    }
                    
                    if (!results.isEmpty()) {
                        callback.onScanSuccess(results);
                    } else {
                        callback.onScanError(new Exception("No barcodes found"));
                    }
                })
                .addOnFailureListener(callback::onScanError);
    }
    
    private String getBarcodeFormatString(int format) {
        switch (format) {
            case Barcode.FORMAT_CODE_128: return "CODE_128";
            case Barcode.FORMAT_CODE_39: return "CODE_39";
            case Barcode.FORMAT_CODE_93: return "CODE_93";
            case Barcode.FORMAT_CODABAR: return "CODABAR";
            case Barcode.FORMAT_DATA_MATRIX: return "DATA_MATRIX";
            case Barcode.FORMAT_EAN_13: return "EAN_13";
            case Barcode.FORMAT_EAN_8: return "EAN_8";
            case Barcode.FORMAT_ITF: return "ITF";
            case Barcode.FORMAT_QR_CODE: return "QR_CODE";
            case Barcode.FORMAT_UPC_A: return "UPC_A";
            case Barcode.FORMAT_UPC_E: return "UPC_E";
            case Barcode.FORMAT_PDF417: return "PDF_417";
            case Barcode.FORMAT_AZTEC: return "AZTEC";
            default: return "UNKNOWN";
        }
    }
    
    private String getBarcodeValueTypeString(int valueType) {
        switch (valueType) {
            case Barcode.TYPE_CONTACT_INFO: return "CONTACT_INFO";
            case Barcode.TYPE_EMAIL: return "EMAIL";
            case Barcode.TYPE_ISBN: return "ISBN";
            case Barcode.TYPE_PHONE: return "PHONE";
            case Barcode.TYPE_PRODUCT: return "PRODUCT";
            case Barcode.TYPE_SMS: return "SMS";
            case Barcode.TYPE_TEXT: return "TEXT";
            case Barcode.TYPE_URL: return "URL";
            case Barcode.TYPE_WIFI: return "WIFI";
            case Barcode.TYPE_GEO: return "GEO";
            case Barcode.TYPE_CALENDAR_EVENT: return "CALENDAR_EVENT";
            case Barcode.TYPE_DRIVER_LICENSE: return "DRIVER_LICENSE";
            default: return "TEXT";
        }
    }
    
    public void release() {
        if (cameraExecutor != null) {
            cameraExecutor.shutdown();
        }
        if (scanner != null) {
            scanner.close();
        }
    }
    
    // Image analyzer for camera scanning
    private class QRCodeAnalyzer implements ImageAnalysis.Analyzer {
        private long lastAnalyzedTimestamp = 0L;
        private final long THROTTLE_TIMEOUT_MS = 1000L; // Analyze once per second
        
        @Override
        public void analyze(@NonNull ImageProxy imageProxy) {
            if (!isScanning) {
                imageProxy.close();
                return;
            }
            
            long currentTimestamp = System.currentTimeMillis();
            if (currentTimestamp - lastAnalyzedTimestamp < THROTTLE_TIMEOUT_MS) {
                imageProxy.close();
                return;
            }
            
            @SuppressWarnings("UnsafeOptInUsageError")
            InputImage image = InputImage.fromMediaImage(
                    imageProxy.getImage(),
                    imageProxy.getImageInfo().getRotationDegrees()
            );
            
            scanner.process(image)
                    .addOnSuccessListener(barcodes -> {
                        if (!barcodes.isEmpty() && isScanning) {
                            Barcode barcode = barcodes.get(0);
                            String rawValue = barcode.getRawValue();
                            if (rawValue != null) {
                                isScanning = false; // Stop scanning after successful scan
                                lastAnalyzedTimestamp = currentTimestamp;
                                
                                // Run callback on main thread
                                new Handler(Looper.getMainLooper()).post(() -> {
                                    if (scanCallback != null) {
                                        scanCallback.onScanSuccess(rawValue);
                                    }
                                });
                            }
                        }
                    })
                    .addOnFailureListener(e -> {
                        Log.e(TAG, "Barcode scanning failed", e);
                    })
                    .addOnCompleteListener(task -> {
                        imageProxy.close();
                    });
        }
    }
}