/*
    Scanbot SDK ReactNative Module
    Copyright (c) 2017 doo GmbH. All rights reserved.
*/
package io.scanbot.sdk.reactnative;

import android.net.Uri;
import android.util.Log;

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

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;

import io.scanbot.sdk.barcode.entity.BarcodeDensity;
import io.scanbot.sdk.barcode.entity.BarcodeDocumentFormat;
import io.scanbot.sdk.barcode.entity.BarcodeFormat;
import io.scanbot.sdk.barcode.entity.BarcodeScannerAdditionalConfig;
import io.scanbot.sdk.barcode.entity.EngineMode;
import io.scanbot.sdk.barcode.entity.MSIPlesseyChecksumAlgorithm;
import io.scanbot.sdk.ui.view.base.configuration.CameraOrientationMode;

public final class JSONUtils {

    public static class WritableMapBuilder {

        private final WritableNativeMap _writableMap = new WritableNativeMap();

        public WritableMapBuilder putBoolean(@NonNull String key, boolean value) {
            _writableMap.putBoolean(key, value);
            return this;
        }

        public WritableMapBuilder putDouble(@NonNull String key, double value) {
            _writableMap.putDouble(key, value);
            return this;
        }

        public WritableMapBuilder putInt(@NonNull String key, int value) {
            _writableMap.putInt(key, value);
            return this;
        }

        public WritableMapBuilder putNull(@NonNull String key) {
            _writableMap.putNull(key);
            return this;
        }

        public WritableMapBuilder putString(@NonNull String key, @Nullable String value) {
            _writableMap.putString(key, value);
            return this;
        }

        public WritableMapBuilder putMap(@NonNull String key, @Nullable ReadableMap value) {
            _writableMap.putMap(key, value);
            return this;
        }

        public WritableMapBuilder putArray(@NonNull String key, @Nullable ReadableArray value) {
            _writableMap.putArray(key, value);
            return this;
        }

        public WritableMap build() {
            return this._writableMap;
        }
    }

    private static WritableMap convertJsonToMap(JSONObject jsonObject) {
        WritableMap map = new WritableNativeMap();

        try {
            Iterator<String> iterator = jsonObject.keys();
            while (iterator.hasNext()) {
                String key = iterator.next();
                Object value = jsonObject.get(key);
                if (value instanceof JSONObject) {
                    map.putMap(key, convertJsonToMap((JSONObject) value));
                } else if (value instanceof JSONArray) {
                    map.putArray(key, convertJsonToArray((JSONArray) value));
                } else if (value instanceof Boolean) {
                    map.putBoolean(key, (Boolean) value);
                } else if (value instanceof Integer) {
                    map.putInt(key, (Integer) value);
                } else if (value instanceof Double || value instanceof Float) {
                    map.putDouble(key, ((Number) value).doubleValue());
                } else if (value instanceof String) {
                    map.putString(key, (String) value);
                } else if (value instanceof Uri) {
                    map.putString(key, ((Uri) value).toString());
                } else {
                    map.putString(key, value.toString());
                }
            }
        } catch (Exception e) {
            Log.e("JSONUtils", e.toString());
            // TODO: handle
        }

        return map;
    }

    private static WritableArray convertJsonToArray(JSONArray jsonArray) {
        WritableArray array = Arguments.createArray();

        try {
            for (int i = 0; i < jsonArray.length(); i++) {
                Object value = jsonArray.get(i);
                if (value instanceof JSONObject) {
                    array.pushMap(convertJsonToMap((JSONObject) value));
                } else if (value instanceof JSONArray) {
                    array.pushArray(convertJsonToArray((JSONArray) value));
                } else if (value instanceof Boolean) {
                    array.pushBoolean((Boolean) value);
                } else if (value instanceof Integer) {
                    array.pushInt((Integer) value);
                } else if (value instanceof Double || value instanceof Float) {
                    array.pushDouble(((Number) value).doubleValue());
                } else if (value instanceof String) {
                    array.pushString((String) value);
                } else if (value instanceof Uri) {
                    array.pushString(((Uri) value).toString());
                } else {
                    array.pushString(value.toString());
                }
            }
        } catch (Exception e) {
            Log.e("JSONUtils", e.toString());
            // TODO: handle
        }

        return array;
    }

    public static WritableArray toWritableArray(Object[] array) {
        WritableArray writableArray = Arguments.createArray();

        for (int i = 0; i < array.length; i++) {
            Object value = array[i];

            if (value == null) {
                writableArray.pushNull();
            }
            if (value instanceof Boolean) {
                writableArray.pushBoolean((Boolean) value);
            }
            if (value instanceof Double || value instanceof Float) {
                writableArray.pushDouble(((Number) value).doubleValue());
            }
            if (value instanceof Integer) {
                writableArray.pushInt((Integer) value);
            }
            if (value instanceof String) {
                writableArray.pushString((String) value);
            }
            if (value instanceof Map) {
                writableArray.pushMap(toWritableMap((Map<String, Object>) value));
            }
            if (value.getClass().isArray()) {
                writableArray.pushArray(toWritableArray((Object[]) value));
            }
            if (Collection.class.isAssignableFrom(value.getClass())) {
                writableArray.pushArray(toWritableArray((Object[]) ((Collection<?>) value).toArray()));
            }
            if (value instanceof Uri) {
                writableArray.pushString(value.toString());
            }
        }

        return writableArray;
    }

    public static WritableMap toWritableMap(Map<String, ?> map) {
        WritableMap writableMap = Arguments.createMap();
        Iterator iterator = map.entrySet().iterator();

        while (iterator.hasNext()) {
            Map.Entry pair = (Map.Entry) iterator.next();
            Object value = pair.getValue();
            if (value == null) {
                writableMap.putNull((String) pair.getKey());
            } else if (value instanceof Boolean) {
                writableMap.putBoolean((String) pair.getKey(), (Boolean) value);
            } else if (value instanceof Double || value instanceof Float) {
                writableMap.putDouble((String) pair.getKey(), ((Number) value).doubleValue());
            } else if (value instanceof Integer) {
                writableMap.putInt((String) pair.getKey(), (Integer) value);
            } else if (value instanceof String) {
                writableMap.putString((String) pair.getKey(), (String) value);
            } else if (value instanceof Map) {
                writableMap.putMap((String) pair.getKey(), toWritableMap((Map<String, Object>) value));
            } else if (value.getClass().isArray()) {
                writableMap.putArray((String) pair.getKey(), toWritableArray((Object[]) value));
            } else if (Collection.class.isAssignableFrom(value.getClass())) {
                writableMap.putArray((String) pair.getKey(), toWritableArray((Object[]) ((Collection<?>) value).toArray()));
            } else if (value instanceof JSONArray) {
                writableMap.putArray((String) pair.getKey(), convertJsonToArray((JSONArray) value));
            } else if (value instanceof JSONObject) {
                writableMap.putMap((String) pair.getKey(), convertJsonToMap((JSONObject) value));
            } else if (value instanceof Uri) {
                writableMap.putString((String) pair.getKey(), value.toString());
            }
        }

        return writableMap;
    }

    public static ArrayList<BarcodeFormat> extractBarcodeFormats(final ReadableMap configuration) {
        ArrayList<BarcodeFormat> nativeBarcodeFormats = new ArrayList<>();
        if (configuration.hasKey("barcodeFormats")) {
            ArrayList barcodeFormats = new ArrayList();
            ReadableArray nullable = configuration.getArray("barcodeFormats");
            if (nullable != null) {
                barcodeFormats = nullable.toArrayList();
            }

            for (Object format : barcodeFormats) {
                nativeBarcodeFormats.add(BarcodeFormat.valueOf((String) format));
            }
        }

        return nativeBarcodeFormats;
    }

    public static ArrayList<BarcodeDocumentFormat> extractBarcodeDocumentFormats(final ReadableMap configuration) {
        ArrayList<BarcodeDocumentFormat> nativeBarcodeDocumentFormats = new ArrayList<>();
        ArrayList<String> barcodeDocumentFormats = JSONUtils.getStringArrayListFromMap(configuration, "acceptedDocumentFormats");

        for (String format : barcodeDocumentFormats) {
            nativeBarcodeDocumentFormats.add(BarcodeDocumentFormat.valueOf(format));
        }

        return nativeBarcodeDocumentFormats;
    }

    public static ArrayList<String> getStringArrayListFromMap(final ReadableMap configuration, final String key) {
        ArrayList<String> out = new ArrayList<>();
        if (!configuration.hasKey(key)) {
            return out;
        }
        ReadableArray nullable = configuration.getArray(key);
        if (nullable != null) {
            ArrayList<Object> objects = nullable.toArrayList();
            for (Object obj : objects) {
                try {
                    out.add(Objects.toString(obj));
                } catch (ClassCastException ignored) {
                }
            }
        }
        return out;
    }

    public static @Nullable
    BarcodeScannerAdditionalConfig extractBarcodeScannerAdditionalConfig(final ReadableMap configuration) {
        boolean changed = false;

        int minimumTextLength = BarcodeScannerAdditionalConfig.DEFAULT_MIN_TEXT_LENGTH;
        if (configuration.hasKey("minimumTextLength")) {
            minimumTextLength = configuration.getInt("minimumTextLength");
            changed = true;
        }

        int maximumTextLength = BarcodeScannerAdditionalConfig.DEFAULT_MAX_TEXT_LENGTH;
        if (configuration.hasKey("maximumTextLength")) {
            maximumTextLength = configuration.getInt("maximumTextLength");
            changed = true;
        }

        int minimum1DQuietZoneSize = BarcodeScannerAdditionalConfig.DEFAULT_MIN_1D_QUIET_ZONE_SIZE;
        if (configuration.hasKey("minimum1DBarcodesQuietZone")) {
            minimum1DQuietZoneSize = configuration.getInt("minimum1DBarcodesQuietZone");
            changed = true;
        }

        boolean gs1DecodingEnabled = BarcodeScannerAdditionalConfig.DEFAULT_GS1_DECODING_ENABLED;
        if (configuration.hasKey("gs1DecodingEnabled")) {
            gs1DecodingEnabled = configuration.getBoolean("gs1DecodingEnabled");
            changed = true;
        }

        EnumSet<MSIPlesseyChecksumAlgorithm> msiPlesseyChecksumAlgorithms =
                BarcodeScannerAdditionalConfig.Companion.getDEFAULT_MSI_PLESSEY_CHECKSUM_ALGORITHMS();
        if (configuration.hasKey("msiPlesseyChecksumAlgorithm")) {
            msiPlesseyChecksumAlgorithms.clear();
            String algorithm = configuration.getString("msiPlesseyChecksumAlgorithm");
            msiPlesseyChecksumAlgorithms.add(MSIPlesseyChecksumAlgorithm.valueOf(algorithm));
            changed = true;
        }

        boolean stripCheckDigits = BarcodeScannerAdditionalConfig.DEFAULT_STRIP_CHECK_DIGITS;
        if (configuration.hasKey("stripCheckDigits")) {
            stripCheckDigits = configuration.getBoolean("stripCheckDigits");
            changed = true;
        }

        boolean lowPowerMode = BarcodeScannerAdditionalConfig.DEFAULT_LOW_POWER_MODE;
        if (configuration.hasKey("lowPowerMode")) {
            lowPowerMode = configuration.getBoolean("lowPowerMode");
            changed = true;
        }

        BarcodeDensity codeDensity = BarcodeScannerAdditionalConfig.getDEFAULT_CODE_DENSITY();

        int decodeStacks1D = 1;

        return changed ? new BarcodeScannerAdditionalConfig(
                minimumTextLength,
                maximumTextLength,
                minimum1DQuietZoneSize,
                gs1DecodingEnabled,
                msiPlesseyChecksumAlgorithms,
                stripCheckDigits,
                lowPowerMode,
                codeDensity
        ) : null;
    }

    public static EngineMode extractEngineMode(ReadableMap configuration) {
        if (!configuration.hasKey("engineMode")) {
            return EngineMode.NextGen;
        }

        String mode = configuration.getString("engineMode");
        if ("LEGACY".equals(mode)) {
            return EngineMode.LegacyFast;
        }
        return EngineMode.NextGen;
    }

    public static CameraOrientationMode convertInterfaceOrientation(final String orientation) {
        switch (orientation) {
            case "PORTRAIT":
            case "PORTRAIT_UPSIDE_DOWN":
                return CameraOrientationMode.PORTRAIT;
            case "LANDSCAPE_LEFT":
            case "LANDSCAPE_RIGHT":
            case "LANDSCAPE":
                return CameraOrientationMode.LANDSCAPE;
            default:
                return null;
        }
    }
}
