package com.regula.documentreader;

import android.content.Context;
import android.graphics.Bitmap;
import android.util.Base64;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.regula.documentreader.api.completions.IDocumentReaderCompletion;
import com.regula.documentreader.api.completions.IDocumentReaderInitCompletion;
import com.regula.documentreader.api.completions.IDocumentReaderPrepareCompletion;
import com.regula.documentreader.api.enums.DocReaderAction;
import com.regula.documentreader.api.params.ImageInputParam;
import com.regula.documentreader.api.params.rfid.PKDCertificate;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

import static com.regula.documentreader.api.DocumentReader.Instance;
import static java.util.Objects.requireNonNull;

@SuppressWarnings("unused")
public class RNRegulaDocumentReaderModule extends ReactContextBaseJavaModule {

    private ReactContext reactContext;
    private final static String prepareDatabaseProgressChangeEvent = "prepareDatabaseProgressChangeEvent";
    private static int databaseDownloadProgress = 0;

    public RNRegulaDocumentReaderModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    @NonNull
    @Override
    public String getName() {
        return "RNRegulaDocumentReader";
    }

    private Context getContext() {
        return reactContext.getCurrentActivity();
    }

    private interface SuccessCallback {
        void callback(String success);
    }

    private interface ErrorCallback {
        void callback(String error);
    }

    private interface Result {
        void success(String success);
        void error(String error);
    }

    static class JSONWrapper {
        JSONArray data;

        public JSONWrapper(JSONArray array) {
            data = array;
        }

        @Nullable
        public <T> T argument(String key) throws JSONException {
            //noinspection unchecked
            return (T) data.get(Integer.parseInt(key));
        }
    }

    private void sendProgress(int progress){
        WritableMap map = Arguments.createMap();
        map.putString("msg", "Downloading database: " + progress + "%");
        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(prepareDatabaseProgressChangeEvent, map);
    }

    @ReactMethod
    public void exec(String moduleName, String action, ReadableArray args, Callback successCallback, Callback errorCallback) {
        JSONWrapper input = new JSONWrapper(new JSONArray(args.toArrayList()));
        Result result = new Result() {
            @Override
            public void success(String success) {
                successCallback.invoke(success);
            }

            @Override
            public void error(String error) {
                errorCallback.invoke(error);
            }
        };
        try {
            switch (action) {
                // Common code
                case "getAvailableScenarios":
                    getAvailableScenarios(result::success);
                    break;
                case "getAPIVersion":
                    getAPIVersion(result::success);
                    break;
                case "getCoreVersion":
                    getCoreVersion(result::success);
                    break;
                case "getCoreMode":
                    getCoreMode(result::success);
                    break;
                case "getDatabaseID":
                    getDatabaseID(result::success);
                    break;
                case "getDatabaseVersion":
                    getDatabaseVersion(result::success);
                    break;
                case "getDatabaseDate":
                    getDatabaseDate(result::success);
                    break;
                case "getDatabaseDescription":
                    getDatabaseDescription(result::success);
                    break;
                case "getDatabaseCountriesNumber":
                    getDatabaseCountriesNumber(result::success);
                    break;
                case "getDatabaseDocumentsNumber":
                    getDatabaseDocumentsNumber(result::success);
                    break;
                case "getScenario":
                    getScenario(result::success, input.argument("0"));
                    break;
                case "isRFIDAvailableForUse":
                    isRFIDAvailableForUse(result::success);
                    break;
                case "getDocumentReaderIsReady":
                    getDocumentReaderIsReady(result::success);
                    break;
                case "getDocumentReaderStatus":
                    getDocumentReaderStatus(result::success);
                    break;
                case "selectedScenario":
                    selectedScenario(result::success);
                    break;
                case "getSessionLogFolder":
                    getSessionLogFolder(result::success);
                    break;
                case "initializeReader":
                    initializeReader(result::success, result::error, input.argument("0"));
                    break;
                case "prepareDatabase":
                    prepareDatabase(result::success, result::error, input.argument("0"));
                    break;
                case "recognizeImage":
                    recognizeImage(result::success, result::error, input.argument("0"));
                    break;
                case "recognizeImageFrame":
                    recognizeImageFrame(result::success, result::error, input.argument("0"), input.argument("1"));
                    break;
                case "recognizeImageWithOpts":
                    recognizeImageWithOpts(result::success, result::error, input.argument("0"), input.argument("1"));
                    break;
                case "recognizeVideoFrame":
                    recognizeVideoFrame(result::success, result::error, input.argument("0"), input.argument("1"));
                    break;
                case "runAutoUpdate":
                    runAutoUpdate(result::success, result::error, input.argument("0"));
                    break;
                case "setConfig":
                    setConfig(result::success, input.argument("0"));
                    break;
                case "setCameraSessionIsPaused":
                    setCameraSessionIsPaused(result::error, requireNonNull(input.argument("0")));
                    break;
                case "readRFID":
                    readRFID(result::error);
                    break;
                case "setRfidScenario":
                    setRfidScenario(result::success, input.argument("0"));
                    break;
                case "showScanner":
                    showScanner(result::success, result::error);
                    break;
                case "showScannerWithCameraID":
                    showScannerWithCameraID(result::success, result::error, requireNonNull(input.argument("0")));
                    break;
                case "showScannerWithCameraIDAndOpts":
                    showScannerWithCameraIDAndOpts(result::success, result::error, requireNonNull(input.argument("0")), input.argument("1"));
                    break;
                case "startNewPage":
                    startNewPage(result::success);
                    break;
                case "startNewSession":
                    startNewSession(result::success);
                    break;
                case "startRFIDReader":
                    startRFIDReader(result::success, result::error);
                    break;
                case "stopRFIDReader":
                    stopRFIDReader(result::success);
                    break;
                case "stopScanner":
                    stopScanner(result::success);
                    break;
                case "deinitializeReader":
                    deinitializeReader(result::success);
                    break;
                case "isAuthenticatorAvailableForUse":
                    isAuthenticatorAvailableForUse(result::success);
                    break;
                case "getConfig":
                    getConfig(result::success);
                    break;
                case "getRfidScenario":
                    getRfidScenario(result::success);
                    break;
                case "getLicenseExpiryDate":
                    getLicenseExpiryDate(result::success, result::error);
                    break;
                case "getLicenseCountryFilter":
                    getLicenseCountryFilter(result::success, result::error);
                    break;
                case "licenseIsRfidAvailable":
                    licenseIsRfidAvailable(result::success);
                    break;
                case "recognizeImageWithImageInputParams":
                    recognizeImageWithImageInputParams(result::success, result::error, input.argument("0"), input.argument("1"));
                    break;
                case "recognizeImages":
                    recognizeImages(result::success, result::error, input.argument("0"));
                    break;
                case "removeDatabase":
                    removeDatabase(result::success);
                    break;
                case "cancelDBUpdate":
                    cancelDBUpdate(result::success);
                    break;
                case "resetConfiguration":
                    resetConfiguration(result::success);
                    break;
                case "setEnableCoreLogs":
                    setEnableCoreLogs(result::success, requireNonNull(input.argument("0")));
                    break;
                case "addPKDCertificates":
                    addPKDCertificates(result::success, requireNonNull(input.argument("0")));
                    break;
                case "clearPKDCertificates":
                    clearPKDCertificates(result::success);
                    break;
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    public void getAvailableScenarios(SuccessCallback successCallback) throws JSONException {
        successCallback.callback(JSONConstructor.generateList(Instance().availableScenarios, JSONConstructor::generateDocumentReaderScenario, getContext()).toString());
    }

    public void getAPIVersion(SuccessCallback successCallback) {
        successCallback.callback(requireNonNull(Instance().version).api);
    }

    public void getCoreVersion(SuccessCallback successCallback) {
        successCallback.callback(requireNonNull(Instance().version).core);
    }

    public void getCoreMode(SuccessCallback successCallback) {
        successCallback.callback(requireNonNull(Instance().version).coreMode);
    }

    public void getDatabaseID(SuccessCallback successCallback) {
        successCallback.callback(requireNonNull(requireNonNull(Instance().version).database).databaseID);
    }

    public void getDatabaseVersion(SuccessCallback successCallback) {
        successCallback.callback(requireNonNull(requireNonNull(Instance().version).database).version);
    }

    public void getDatabaseDate(SuccessCallback successCallback) {
        successCallback.callback(requireNonNull(requireNonNull(Instance().version).database).date);
    }

    public void getDatabaseDescription(SuccessCallback successCallback) {
        successCallback.callback(requireNonNull(requireNonNull(Instance().version).database).databaseDescription);
    }

    public void getDatabaseCountriesNumber(SuccessCallback successCallback) {
        successCallback.callback("" + requireNonNull(requireNonNull(Instance().version).database).countriesNumber);
    }

    public void getDatabaseDocumentsNumber(SuccessCallback successCallback) {
        successCallback.callback("" + requireNonNull(requireNonNull(Instance().version).database).documentsNumber);
    }

    public void deinitializeReader(SuccessCallback successCallback) {
        Instance().deinitializeReader();
        successCallback.callback("");
    }

    public void isAuthenticatorAvailableForUse(SuccessCallback successCallback) {
        successCallback.callback(Instance().isAuthenticatorAvailableForUse() ? "1" : "0");
    }

    public void getConfig(SuccessCallback successCallback) throws JSONException {
        successCallback.callback(RegulaConfig.getConfig(Instance(), getContext()).toString());
    }

    public void getRfidScenario(SuccessCallback successCallback) {
        successCallback.callback(Instance().rfidScenario().toJson());
    }

    public void selectedScenario(SuccessCallback successCallback) throws JSONException {
        successCallback.callback(JSONConstructor.generateDocumentReaderScenario(Instance().getScenario(Instance().processParams().getScenario())).toString());
    }

    public void getScenario(SuccessCallback successCallback, String scenario) throws JSONException {
        successCallback.callback(JSONConstructor.generateDocumentReaderScenario(Instance().getScenario(scenario)).toString());
    }

    public void getLicenseExpiryDate(SuccessCallback successCallback, ErrorCallback errorCallback) {
        if (Instance().license().getExpiryDate() == null)
            errorCallback.callback("null");
        else
            successCallback.callback(requireNonNull(Instance().license().getExpiryDate()).toString());
    }

    public void getLicenseCountryFilter(SuccessCallback successCallback, ErrorCallback errorCallback) {
        if (Instance().license().getCountryFilter() == null)
            errorCallback.callback("null");
        else
            successCallback.callback(JSONConstructor.generateList(requireNonNull(Instance().license().getCountryFilter())).toString());
    }

    public void licenseIsRfidAvailable(SuccessCallback successCallback) {
        successCallback.callback(Instance().license().isRfidAvailable() ? "1" : "0");
    }

    public void getDocumentReaderIsReady(SuccessCallback successCallback) {
        successCallback.callback(Instance().getDocumentReaderIsReady() ? "1" : "0");
    }

    public void getDocumentReaderStatus(SuccessCallback successCallback) {
        successCallback.callback(Instance().getDocumentReaderStatus());
    }

    public void isRFIDAvailableForUse(SuccessCallback successCallback) {
        successCallback.callback(Instance().isRFIDAvailableForUse() ? "1" : "0");
    }

    private void initializeReader(SuccessCallback successCallback, ErrorCallback errorCallback, Object license) {
        if (!Instance().getDocumentReaderIsReady())
            Instance().initializeReader(getContext(), Base64.decode(license.toString(), Base64.DEFAULT), getInitCompletion(successCallback, errorCallback));
        else
            successCallback.callback("already initialized");
    }

    public void startNewSession(SuccessCallback successCallback) {
        Instance().startNewSession();
        successCallback.callback("");
    }

    public void startNewPage(SuccessCallback successCallback) {
        Instance().startNewPage();
        successCallback.callback("");
    }

    public void recognizeImageWithImageInputParams(SuccessCallback successCallback, ErrorCallback errorCallback, String base64Image, final JSONObject params) throws JSONException {
        if (Instance().getDocumentReaderIsReady())
            Instance().recognizeImage(JSONConstructor.bitmapFromBase64(base64Image), new ImageInputParam(params.getInt("width"), params.getInt("height"), params.getInt("type")), getCompletion(successCallback, errorCallback));
        else
            errorCallback.callback("document reader not ready");
    }

    public void recognizeImageWithOpts(SuccessCallback successCallback, ErrorCallback errorCallback, final JSONObject opts, String base64Image) throws JSONException {
        RegulaConfig.setConfig(Instance(), opts, getContext());
        recognizeImage(successCallback, errorCallback, base64Image);
    }

    public void recognizeImage(SuccessCallback successCallback, ErrorCallback errorCallback, String base64Image) {
        if (Instance().getDocumentReaderIsReady())
            Instance().recognizeImage(JSONConstructor.bitmapFromBase64(base64Image), getCompletion(successCallback, errorCallback));
        else
            errorCallback.callback("document reader not ready");
    }

    public void recognizeImages(SuccessCallback successCallback, ErrorCallback errorCallback, JSONArray base64Images) throws JSONException {
        if (Instance().getDocumentReaderIsReady()) {
            Bitmap[] images = new Bitmap[base64Images.length()];
            for (int i = 0; i < images.length; i++)
                images[i] = JSONConstructor.bitmapFromBase64(base64Images.getString(i));
            Instance().recognizeImages(images, getCompletion(successCallback, errorCallback));
        } else
            errorCallback.callback("document reader not ready");
    }

    public void removeDatabase(SuccessCallback successCallback) {
        successCallback.callback(Instance().removeDatabase(getContext()) ? "1" : "0");
    }

    public void cancelDBUpdate(SuccessCallback successCallback) {
        successCallback.callback(Instance().cancelDBUpdate() ? "1" : "0");
    }

    public void resetConfiguration(SuccessCallback successCallback) {
        Instance().resetConfiguration();
        successCallback.callback("");
    }

    public void setEnableCoreLogs(SuccessCallback successCallback, boolean enableLogs) {
        Instance().setEnableCoreLogs(enableLogs);
        successCallback.callback("");
    }

    public void addPKDCertificates(SuccessCallback successCallback, JSONArray certificatesJSON) throws JSONException {
        List<PKDCertificate> certificates = new ArrayList<>();
        for (int i = 0; i < certificatesJSON.length(); i++) {
            JSONObject certificate = certificatesJSON.getJSONObject(i);
            certificates.add(new PKDCertificate(JSONConstructor.byteArrayFromJson(certificate.getJSONArray("binaryData")), certificate.getInt("resourceType"), certificate.has("certificate") ? JSONConstructor.byteArrayFromJson(certificate.getJSONArray("privateKey")) : null));
        }
        Instance().addPKDCertificates(certificates);
        successCallback.callback("");
    }

    public void clearPKDCertificates(SuccessCallback successCallback) {
        Instance().clearPKDCertificates();
        successCallback.callback("");
    }

    public void recognizeImageFrame(SuccessCallback successCallback, ErrorCallback errorCallback, String base64Image, final JSONObject opts) throws JSONException {
        if (Instance().getDocumentReaderIsReady())
            Instance().recognizeImageFrame(JSONConstructor.bitmapFromBase64(base64Image), new ImageInputParam(opts.getInt("width"), opts.getInt("height"), opts.getInt("type")), getCompletion(successCallback, errorCallback));
        else
            errorCallback.callback("document reader not ready");
    }

    public void recognizeVideoFrame(SuccessCallback successCallback, ErrorCallback errorCallback, String byteString, final JSONObject opts) throws JSONException {
        if (Instance().getDocumentReaderIsReady())
            Instance().recognizeVideoFrame(byteString.getBytes(), new ImageInputParam(opts.getInt("width"), opts.getInt("height"), opts.getInt("type")), getCompletion(successCallback, errorCallback));
        else
            errorCallback.callback("document reader not ready");
    }

    public void showScannerWithCameraID(SuccessCallback successCallback, ErrorCallback errorCallback, int cameraID) {
        if (Instance().getDocumentReaderIsReady())
            Instance().showScanner(getContext(), cameraID, getCompletion(successCallback, errorCallback));
        else
            errorCallback.callback("document reader not ready");
    }

    public void showScanner(SuccessCallback successCallback, ErrorCallback errorCallback) {
        showScannerWithCameraID(successCallback, errorCallback, -1);
    }

    public void showScannerWithCameraIDAndOpts(SuccessCallback successCallback, ErrorCallback errorCallback, int cameraID, final JSONObject opts) throws JSONException {
        if (Instance().getDocumentReaderIsReady()) {
            RegulaConfig.setConfig(Instance(), opts, getContext());
            Instance().showScanner(getContext(), cameraID, getCompletion(successCallback, errorCallback));
        } else
            errorCallback.callback("document reader not ready");
    }

    public void stopScanner(SuccessCallback successCallback) {
        Instance().stopScanner(getContext());
        successCallback.callback("");
    }

    public void startRFIDReader(SuccessCallback successCallback, ErrorCallback errorCallback) {
        Instance().startRFIDReader(getContext(), getCompletion(successCallback, errorCallback));
    }

    public void stopRFIDReader(SuccessCallback successCallback) {
        Instance().stopRFIDReader(getContext());
        successCallback.callback("");
    }

    public void prepareDatabase(SuccessCallback successCallback, ErrorCallback errorCallback, String dbID) {
        Instance().prepareDatabase(getContext(), dbID, getPrepareCompletion(successCallback, errorCallback));
    }

    public void runAutoUpdate(SuccessCallback successCallback, ErrorCallback errorCallback, String dbID) {
        Instance().runAutoUpdate(getContext(), dbID, getPrepareCompletion(successCallback, errorCallback));
    }

    public void setRfidScenario(SuccessCallback successCallback, final JSONObject opts) throws JSONException {
        RegulaConfig.setRfidScenario(opts);
        successCallback.callback("");
    }

    public void getSessionLogFolder(SuccessCallback successCallback) {
        successCallback.callback(Instance().processParams().sessionLogFolder);
    }

    public void setConfig(SuccessCallback successCallback, final JSONObject opts) throws JSONException {
        RegulaConfig.setConfig(Instance(), opts, getContext());
        successCallback.callback("");
    }

    public void setCameraSessionIsPaused(ErrorCallback errorCallback, @SuppressWarnings("unused") boolean ignored) {
        errorCallback.callback("setCameraSessionIsPaused() is an ios-only method");
    }

    public void readRFID(ErrorCallback errorCallback) {
        errorCallback.callback("readRFID() is an ios-only method");
    }

    private IDocumentReaderCompletion getCompletion(SuccessCallback successCallback, ErrorCallback errorCallback) {
        return (action, results, error) -> {
            switch (action) {
                case DocReaderAction.COMPLETE:
                    successCallback.callback(JSONConstructor.resultsToJsonObject(results, getContext()).toString());
                    break;
                case DocReaderAction.CANCEL:
                    errorCallback.callback("Canceled by user");
                    break;
                case DocReaderAction.ERROR:
                    errorCallback.callback("Error: " + error);
                    break;
            }
        };
    }

    private IDocumentReaderPrepareCompletion getPrepareCompletion(SuccessCallback successCallback, ErrorCallback errorCallback) {
        return new IDocumentReaderPrepareCompletion() {
            @Override
            public void onPrepareProgressChanged(int progress) {
                if (progress != databaseDownloadProgress) {
                    sendProgress(progress);
                    databaseDownloadProgress = progress;
                }
            }

            @Override
            public void onPrepareCompleted(boolean status, String error) {
                if (status)
                    successCallback.callback("database prepared");
                else
                    errorCallback.callback("database preparation failed: " + error);
            }
        };
    }

    private IDocumentReaderInitCompletion getInitCompletion(SuccessCallback successCallback, ErrorCallback errorCallback) {
        return (success, error) -> {
            if (success)
                successCallback.callback("init completed");
            else
                errorCallback.callback("Init failed:" + error);
        };
    }
}