package com.ammarahmed.mmkv;

import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

public class MMKV {

    static public final int SINGLE_PROCESS_MODE = 0x1;
    static public final int MULTI_PROCESS_MODE = 0x2;
    private long nativeHandle;
    private static final Set<Long> checkedHandleSet;
    private String rootDir;

    static {
        checkedHandleSet = Collections.synchronizedSet(new HashSet<Long>());
    }

    // call on program start
    public static String initialize(Context context) {
        String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
       return initialize(root);
    }

    public static String initialize(String rootDir) {

        jniInitialize(rootDir, 0);
        rootDir = rootDir;
        return rootDir;
    }

    private static native void jniInitialize(String rootDir, int level);


    @Nullable
    public static MMKV mmkvWithID(String mmapID) {

        long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, null);
       return  checkProcessMode(handle, mmapID, SINGLE_PROCESS_MODE);
    }

    @Nullable
    public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey) {

        long handle = getMMKVWithID(mmapID, mode, cryptKey, null);
        return checkProcessMode(handle, mmapID, mode);
    }

    @Nullable
    public static MMKV mmkvWithID(String mmapID, int mode) {


        long handle = getMMKVWithID(mmapID, mode, null, null);
        return checkProcessMode(handle, mmapID, mode);
    }

    private native static long
    getMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, @Nullable String rootPath);


    @Nullable
    private static MMKV checkProcessMode(long handle, String mmapID, int mode) {
        if (handle == 0) {
            return null;
        }
        if (!checkedHandleSet.contains(handle)) {
            if (!checkProcessMode(handle)) {
                String message;
                if (mode == SINGLE_PROCESS_MODE) {
                    message = "Opening a multi-process MMKV instance [" + mmapID + "] with SINGLE_PROCESS_MODE!";
                } else {
                    message = "Opening a MMKV instance [" + mmapID + "] with MULTI_PROCESS_MODE, ";
                    message += "while it's already been opened with SINGLE_PROCESS_MODE by someone somewhere else!";
                }
                throw new IllegalArgumentException(message);
            }
            checkedHandleSet.add(handle);
        }

        return new MMKV(handle);

    }

    private MMKV(long handle) {
        nativeHandle = handle;
    }

    private static native boolean checkProcessMode(long handle);

    public boolean containsKey(String key) {
        return containsKey(nativeHandle, key);
    }
    private native boolean containsKey(long handle, String key);

    @SuppressWarnings("unchecked")
    @Nullable
    public <T extends Parcelable> T decodeParcelable(String key, Class<T> tClass) {
        return decodeParcelable(key, tClass, null);
    }

    @SuppressWarnings("unchecked")
    @Nullable
    public <T extends Parcelable> T decodeParcelable(String key, Class<T> tClass, @Nullable T defaultValue) {
        if (tClass == null) {
            return defaultValue;
        }

        byte[] bytes = decodeBytes(nativeHandle, key);
        if (bytes == null) {
            return defaultValue;
        }

        Parcel source = Parcel.obtain();
        source.unmarshall(bytes, 0, bytes.length);
        source.setDataPosition(0);

        try {
            String name = tClass.toString();
            Parcelable.Creator<T> creator;
            synchronized (mCreators) {
                creator = (Parcelable.Creator<T>) mCreators.get(name);
                if (creator == null) {
                    Field f = tClass.getField("CREATOR");
                    creator = (Parcelable.Creator<T>) f.get(null);
                    if (creator != null) {
                        mCreators.put(name, creator);
                    }
                }
            }
            if (creator != null) {
                return creator.createFromParcel(source);
            } else {
                throw new Exception("Parcelable protocol requires a "
                        + "non-null static Parcelable.Creator object called "
                        + "CREATOR on class " + name);
            }
        } catch (Exception e) {

        } finally {
            source.recycle();
        }
        return defaultValue;
    }

    private static final HashMap<String, Parcelable.Creator<?>> mCreators = new HashMap<>();

    @Nullable
    private native byte[] decodeBytes(long handle, String key);

    public void removeValueForKey(String key) {
        removeValueForKey(nativeHandle, key);
    }

    private native void removeValueForKey(long handle, String key);

    @Nullable
    public Set<String> decodeStringSet(String key) {
        return decodeStringSet(key, null);
    }

    @Nullable
    public Set<String> decodeStringSet(String key, @Nullable Set<String> defaultValue) {
        return decodeStringSet(key, defaultValue, HashSet.class);
    }

    @Nullable
    private native String[] decodeStringSet(long handle, String key);

    @SuppressWarnings("unchecked")
    @Nullable
    public Set<String> decodeStringSet(String key, @Nullable Set<String> defaultValue, Class<? extends Set> cls) {
        String[] result = decodeStringSet(nativeHandle, key);
        if (result == null) {
            return defaultValue;
        }
        Set<String> a;
        try {
            a = cls.newInstance();
        } catch (IllegalAccessException e) {
            return defaultValue;
        } catch (InstantiationException e) {
            return defaultValue;
        }
        a.addAll(Arrays.asList(result));
        return a;
    }

    public int decodeInt(String key) {
        return decodeInt(nativeHandle, key, 0);
    }

    public int decodeInt(String key, int defaultValue) {
        return decodeInt(nativeHandle, key, defaultValue);
    }

    private native int decodeInt(long handle, String key, int defaultValue);

    public double decodeDouble(String key) {
        return decodeDouble(nativeHandle, key, 0);
    }

    public double decodeDouble(String key, double defaultValue) {
        return decodeDouble(nativeHandle, key, defaultValue);
    }

    private native double decodeDouble(long handle, String key, double defaultValue);

    public boolean encode(String key, @Nullable String value) {
        return encodeString(nativeHandle, key, value);
    }

    private native boolean encodeString(long handle, String key, @Nullable String value);

    public boolean encode(String key, double value) {
        return encodeDouble(nativeHandle, key, value);
    }

    private native boolean encodeDouble(long handle, String key, double value);

    public boolean encode(String key, int value) {
        return encodeInt(nativeHandle, key, value);
    }

    private native boolean encodeInt(long handle, String key, int value);

    public boolean encode(String key, @Nullable Set<String> value) {
        return encodeSet(nativeHandle, key, (value == null) ? null : value.toArray(new String[0]));
    }

    private native boolean encodeSet(long handle, String key, @Nullable String[] value);

    @Nullable
    public native String[] getAllKeys(long handle);

    @NonNull
    public Set<String> getAllKeys() {
        String[] result = getAllKeys(nativeHandle);

        if(result == null) {
            return Collections.emptySet();
        }

        return new HashSet<>(Arrays.asList(result));
    }

    @Nullable
    private native String decodeString(long handle, String key, String defaultValue);

    @Nullable
    public String decodeString(String key, @Nullable String defaultValue) {
        return decodeString(nativeHandle, key, defaultValue);
    }
}
