package cordova.plugin.netcontroll.integration;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;

import br.com.gertec.gdk.codescanner.CodeScanner;
import br.com.gertec.gdk.codescanner.ScannerCallback;
import br.com.gertec.gdk.printer.CutType;
import br.com.gertec.gdk.printer.Gray;
import br.com.gertec.gdk.printer.OrientationType;
import br.com.gertec.gdk.printer.Printer;
import br.com.gertec.gdk.printer.PrinterError;
import br.com.gertec.gdk.printer.PrinterUtils;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;


public class GertecSK210 implements Printer.Listener {
    private static final String TAG = "GertecSK210";

    //Cordova/Java Params
    private Activity mActivity;
    private static Context mContext;
    public static CordovaInterface mCordova;
    private CordovaWebView mWebView;
    volatile boolean mInPrinter = false;

    //Printer (GerSDK)
    // Singleton de processo: a SDK do Gertec mantem uma unica instancia ligada ao
    // servico da impressora. Em apps que recarregam a WebView (F5) o Initialize pode
    // ser chamado varias vezes; manter estatico e inicializar uma unica vez evita
    // re-registrar o listener / re-vincular o servico (o que travava as impressoes).
    private static Printer printer;
    private static PrinterUtils printerUtils;
    // Sincronizacao da impressao: a SDK sinaliza a conclusao de forma assincrona
    // via Printer.Listener. Usado para so cortar o papel apos a imagem ser impressa.
    private static volatile CountDownLatch printLatch;
    private static volatile boolean printHadError = false;

    //Scanner (GerSDK)
    private CodeScanner codeScanner;
    private boolean isDecoding = false;
    // Intervalo minimo entre leituras aceitas (o leitor fica ativo continuamente e
    // poderia ler o mesmo codigo varias vezes em sequencia).
    private static final long SCAN_COOLDOWN_MS = 2000;
    private long lastScanTimeMs = 0;

    //Sensor
    SensorEventListener proximitySensorEventListener;
    SensorManager sensorManager;
    Sensor proximitySensor;

    public static Context getContext() {
        return mContext;
    }

    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
        mActivity = cordova.getActivity();
        mContext = cordova.getActivity().getApplicationContext();
        GertecSK210.mCordova = cordova;
        mWebView = webView;
    }

    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        String callbackContextMessageBase = "Result Gertec SK210 - " + action + ": ";
        if (action.equals("Initialize")) {
            JSONObject params = args.getJSONObject(0);
            try {
                // Idempotente: so cria/registra a impressora na primeira chamada.
                // Chamadas seguintes (ex.: apos reload da WebView) reaproveitam a
                // instancia existente para nao travar a impressao.
                ensurePrinter();
                callbackContext.success(callbackContextMessageBase + "Success");
                return true;
            } catch (Exception error) {
                callbackContext.error(callbackContextMessageBase + "Error Detail: " + error);
            }
        } else if (action.equals("ImpressaoTextoNetPrint")) {
            JSONObject params = args.getJSONObject(0);
            try {

                String dados = params.getString("dados");
                Boolean negrito = false;
                try {
                    negrito = params.getBoolean("negrito");
                } catch (Exception ex) {
                    negrito = false;
                }

                // SK210 = bitmap 380px (tamanho reduzido atual); SK315 = bobina grande (resize 550px, padrão TecToy).
                final String tipoImpressao;
                String tipoImpressaoRaw = "SK210";
                try {
                    if (params.has("tipoImpressao") && !params.isNull("tipoImpressao")) {
                        tipoImpressaoRaw = params.getString("tipoImpressao");
                    }
                } catch (Exception ex) {
                    tipoImpressaoRaw = "SK210";
                }
                if (tipoImpressaoRaw == null || tipoImpressaoRaw.trim().isEmpty()) {
                    tipoImpressao = "SK210";
                } else {
                    tipoImpressao = tipoImpressaoRaw.trim().toUpperCase(Locale.US);
                }

                String html = cordova.plugin.netcontroll.integration.util.NetControllPrinter.ConverterTextoImpressoToHtml(dados, negrito);
                cordova.plugin.netcontroll.integration.util.NetControllPrinter.ConverterHtmlToBitmap(html, mContext, bitmapResult -> {
                    Bitmap dadosImg = bitmapResult;
                    if ("SK315".equals(tipoImpressao) && dadosImg != null) {
                        dadosImg = cordova.plugin.netcontroll.integration.util.NetControllPrinter.ResizeBitmap(dadosImg, 550);
                    }
                    imprimirImagem(dadosImg, callbackContext, callbackContextMessageBase);
                });
            } catch (Exception e) {
                callbackContext.error("ImpressaoTextoNetPrintImg error: " + e);
            }
            return true;
        } else if (action.equals("ImpressaoBase64NetPrint")) {
            try {
                JSONObject params = args.getJSONObject(0);
                String base64String = params.getString("base64String");

                // Mesmo critério de tamanho do texto: SK315 amplia o bitmap antes de imprimir.
                String tipoImpressao = "SK210";
                try {
                    if (params.has("tipoImpressao") && !params.isNull("tipoImpressao")) {
                        String raw = params.getString("tipoImpressao");
                        if (raw != null && !raw.trim().isEmpty()) {
                            tipoImpressao = raw.trim().toUpperCase(Locale.US);
                        }
                    }
                } catch (Exception ignored) {
                }

                Bitmap dadosImg = cordova.plugin.netcontroll.integration.util.NetControllPrinter.ConverterBase64ToBitmap(base64String);
                if ("SK315".equals(tipoImpressao) && dadosImg != null) {
                    dadosImg = cordova.plugin.netcontroll.integration.util.NetControllPrinter.ResizeBitmap(dadosImg, 550);
                }
                // O success/error e disparado dentro de imprimirImagem, apos a impressao concluir.
                imprimirImagem(dadosImg, callbackContext, callbackContextMessageBase);
            } catch (Exception e) {
                callbackContext.error("PrintTesteBase64 error: " + e.toString());
            }
            return true;
        } else if (action.equals("ScannerIniciar")) {
            try {
                JSONObject params = args.getJSONObject(0);
                String scannerHandleCallBack = params.getString("scannerHandleCallBack");
                Boolean scannerRestartAfterRead = params.getBoolean("scannerRestartAfterRead");
                startDecode(scannerHandleCallBack, scannerRestartAfterRead);
                callbackContext.success(callbackContextMessageBase + "Success");
            } catch (Exception e) {
                callbackContext.error("PrintTesteBase64 error: " + e.toString());
            }
            return true;
        } else if (action.equals("ScannerParar")) {
            try {
                stopDecode();
                callbackContext.success(callbackContextMessageBase + "Success");
            } catch (Exception e) {
                callbackContext.error("PrintTesteBase64 error: " + e.toString());
            }
            return true;
        } else if (action.equals("SensorPresencaIniciar")) {
            try {
                JSONObject params = args.getJSONObject(0);
                String sensorHandleCallBack = params.getString("sensorHandleCallBack");
                sensorManager = (SensorManager) mActivity.getSystemService(Context.SENSOR_SERVICE);
                proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);

                if (proximitySensor == null) {
                    callbackContext.error("No proximity sensor found in device.");
                    return false;
                }

                proximitySensorEventListener = new SensorEventListener() {
                    @Override
                    public void onSensorChanged(SensorEvent event) {
                        if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
                            executeGlobalJavascript(sensorHandleCallBack + "('" + event.values[0] + "')");
                        }
                    }

                    @Override
                    public void onAccuracyChanged(Sensor sensor, int accuracy) {
                        int teste = accuracy;
                        int teste2 = teste;
                    }
                };
                startSensor();
                callbackContext.success(callbackContextMessageBase + "Success");
            } catch (Exception e) {
                callbackContext.error("PrintTesteBase64 error: " + e.toString());
            }
            return true;
        } else if (action.equals("SensorPresencaParar")) {
            try {
                stopSensor();
                callbackContext.success(callbackContextMessageBase + "Success");
            } catch (Exception e) {
                callbackContext.error("PrintTesteBase64 error: " + e.toString());
            }
            return true;
        }
        return false;
    }


    // Cria/registra a impressora uma unica vez por processo. O Gertec GerSDK mantem
    // a impressora ligada a um servico; chamar getInstance repetidamente (ex.: a cada
    // Initialize apos reload da WebView) re-registra o listener e trava as impressoes.
    private synchronized void ensurePrinter() {
        if (printer == null) {
            printer = Printer.getInstance(mContext, this);
            printer.setPrinterGray(Gray.HIGH);
            printer.setPrinterOrientation(OrientationType.INVERTED);
            printerUtils = printer.getPrinterUtils();
        }
    }

    //Impressao
    public void imprimirImagem(final Bitmap bitmap, final CallbackContext callbackContext, final String callbackContextMessageBase) {
        // A impressao da nova SDK e sincrona/bloqueante; executar fora da UI thread
        // evita ANR (o bitmap chega via AsyncTask.onPostExecute, que roda na UI thread).
        Runnable printTask = new Runnable() {
            @Override
            public void run() {
                try {
                    mInPrinter = true;

                    if (bitmap == null) {
                        Log.e(TAG, "imprimirImagem: bitmap nulo, nada a imprimir");
                        if (callbackContext != null) {
                            callbackContext.error(callbackContextMessageBase + "Error: bitmap nulo");
                        }
                        return;
                    }

                    // Garante a instancia da impressora caso o Initialize nao tenha sido chamado.
                    ensurePrinter();

                    Bitmap monochromaticBitmap = bitmap;
                    if (printerUtils != null) {
                        // Niveis de intensidade vao de INTENSITY_LEVEL_1 (mais fraco) a
                        // INTENSITY_LEVEL_5 (mais denso/escuro). Usa OpenCV nativo; se a lib
                        // nativa nao estiver disponivel (ABI), faz fallback para o bitmap original.
                        try {
                            monochromaticBitmap = printerUtils.applyIntensityLevel(bitmap, PrinterUtils.INTENSITY_LEVEL_5);
                        } catch (Throwable t) {
                            Log.e(TAG, "applyIntensityLevel falhou, usando bitmap original", t);
                            monochromaticBitmap = bitmap;
                        }
                    }

                    // Prepara a sincronizacao: o corte so pode ocorrer apos a imagem ser
                    // efetivamente impressa (a SDK sinaliza via Printer.Listener).
                    printHadError = false;
                    printLatch = new CountDownLatch(1);

                    // A largura da imagem e ajustada automaticamente ao papel quando necessario.
                    int retImage = printer.printImageAutoResize(monochromaticBitmap);

                    if (retImage < 0) {
                        Log.e(TAG, "imprimirImagem: printImageAutoResize retornou " + retImage);
                        if (callbackContext != null) {
                            callbackContext.error(callbackContextMessageBase + "Error: retorno da impressao = " + retImage);
                        }
                        return;
                    }

                    // Aguarda o sinal de conclusao da impressao da imagem (piso).
                    printLatch.await(30, TimeUnit.SECONDS);

                    // Avanco suficiente para o conteudo passar do cortador antes do corte
                    // (o cortador fica acima da cabeca de impressao).
                    printer.scrollPaper(100);
                    printer.cutPaper(CutType.PAPER_PARTIAL_CUT);

                    if (callbackContext != null) {
                        if (printHadError) {
                            callbackContext.error(callbackContextMessageBase + "Error: falha na impressao da imagem");
                        } else {
                            callbackContext.success(callbackContextMessageBase + "Success");
                        }
                    }
                } catch (Throwable t) {
                    Log.e(TAG, "imprimirImagem error", t);
                    if (callbackContext != null) {
                        callbackContext.error(callbackContextMessageBase + "Error: " + t);
                    }
                } finally {
                    mInPrinter = false;
                }
            }
        };

        if (mCordova != null) {
            mCordova.getThreadPool().execute(printTask);
        } else {
            new Thread(printTask).start();
        }
    }

    //Scanner
    private void startDecode(String scannerHandleCallBack, boolean restartDecode) {
        if (isDecoding) {
            return;
        }
        // Garante que a primeira leitura apos iniciar nunca seja bloqueada pelo cooldown.
        lastScanTimeMs = 0;
        try {
            codeScanner = CodeScanner.getInstance(new ScannerCallback() {
                @Override
                public void result(String barcodeType, String data) {
                    if (data != null) {
                        // Ignora leituras dentro da janela de cooldown para evitar
                        // ler o mesmo codigo repetidamente (o leitor fica sempre ativo).
                        long now = System.currentTimeMillis();
                        if (now - lastScanTimeMs < SCAN_COOLDOWN_MS) {
                            return;
                        }
                        lastScanTimeMs = now;
                        executeGlobalJavascript(scannerHandleCallBack + "('" + data + "')");
                    }
                }

                @Override
                public void cancelled(String causes) {
                    Log.d(TAG, "Scanner cancelled: " + causes);
                }
            });
            // Leitura continua: o leitor permanece ativo lendo N codigos ate stopService().
            codeScanner.scanCode(mActivity);
            isDecoding = true;
        } catch (Exception e) {
            Log.e(TAG, "startDecode error", e);
        }
    }

    private void stopDecode() {
        try {
            if (codeScanner != null) {
                codeScanner.stopService();
            }
        } catch (Exception e) {
            Log.e(TAG, "stopDecode error", e);
        } finally {
            isDecoding = false;
        }
    }

    //Sensor Presenca

    private void startSensor() {
        sensorManager.registerListener(proximitySensorEventListener,
                proximitySensor,
                SensorManager.SENSOR_DELAY_NORMAL);
    }

    private void stopSensor() {
        sensorManager.unregisterListener(proximitySensorEventListener);
    }


    private void executeGlobalJavascript(final String jsString) {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mWebView.loadUrl("javascript:" + jsString);
            }
        });
    }

    //Printer.Listener
    @Override
    public void onPrinterError(PrinterError printerError) {
        String message = String.format(Locale.US, "Id: [%d] | Cause: [\"%s\"]", printerError.getRequestId(), printerError.getCause());
        Log.d("[onPrinterError]", message);
        printHadError = true;
        CountDownLatch latch = printLatch;
        if (latch != null) {
            latch.countDown();
        }
    }

    @Override
    public void onPrinterSuccessful(int printerRequestId) {
        String message = String.format(Locale.US, "Id: [%d]", printerRequestId);
        Log.d("[onPrinterSuccessful]", message);
        CountDownLatch latch = printLatch;
        if (latch != null) {
            latch.countDown();
        }
    }

}
