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

import android.graphics.Color;

import com.facebook.react.bridge.ReadableMap;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import io.scanbot.sdk.ui.camera.FinderAspectRatio;

public final class ObjectMapper {
    public static class InvalidJSONException extends RuntimeException {
        public InvalidJSONException(String message) { super(message); }
    }
    /*
     *  Properties listed here will be skipped by the Object Mapper
     */
    private final static String[] kIgnoredProperties = new String[] {
            "engineMode",
            "barcodeFilter"
    };

    private static Map<String, Map<String, Method>> methodCache = new HashMap<>();

    public static void map(ReadableMap source, Object target, Set<String> keysToIgnore)
            throws IllegalAccessException, InvocationTargetException {
        map(source.toHashMap(), target, keysToIgnore);
    }

    public static void map(ReadableMap source, Object target) throws IllegalAccessException, InvocationTargetException {
        map(source.toHashMap(), target, new HashSet<String>());
    }

    @SuppressWarnings("unchecked")
    private static void map(final Map<String, Object> source, final Object target, Set<String> keysToIgnore)
            throws IllegalAccessException, InvocationTargetException {
        final Class cls = target.getClass();

        final Map<String, Method> methodMap;
        if (!methodCache.containsKey(cls.getName())) {
            methodMap = new HashMap<>();
            methodCache.put(cls.getName(), methodMap);

            final Method[] methods = cls.getMethods();
            for (final Method m : methods) {
                methodMap.put(m.getName(), m);
            }
        } else {
            methodMap = methodCache.get(cls.getName());
        }

        if (methodMap == null) {
            throw new IllegalAccessException("Internal error. methodMap is null");
        }

        for (HashMap.Entry<String, Object> entry : source.entrySet()) {
            final String jsonKey = entry.getKey();
            final String nativeKey = tryToConvertToNativeKey(jsonKey);
            Object value = entry.getValue();
            if (value == null) {
                continue;
            }

            if (shouldIgnoreProperty(jsonKey)) {
                continue;
            }

            if (keysToIgnore.contains(jsonKey)) {
                continue;
            }

            final String setterName = "set" + nativeKey.substring(0, 1).toUpperCase() + nativeKey.substring(1);

            // The default FRONT camera module should be FRONT_MIRRORED on Android
            if (jsonKey.equals("cameraModule") && value.equals("FRONT")) {
                value = "FRONT_MIRRORED";
            }

            if (methodMap.containsKey(setterName)) {
                final Method setter = methodMap.get(setterName);

                if (setter == null) {
                    throw new IllegalAccessException("Internal error. Setter method not found: " + setterName);
                }

                final TryToConvertValueResult convertedValue = tryToConvertValue(jsonKey, value);
                if (convertedValue != null) {
                    if (convertedValue.value != null) {
                        setter.invoke(target, convertedValue.value);
                    }
                    continue;
                }

                if (setterName.equals("setFinderAspectRatio")) {
                    Map<String, Object> pointMap = (Map<String, Object>) value;
                    int x = ((Number) pointMap.get("width")).intValue();
                    int y = ((Number) pointMap.get("height")).intValue();
                    setter.invoke(target, new FinderAspectRatio(x, y));
                } else if (setter.getParameterTypes().length == 2 && value instanceof Map) {
                    // map Size to a binary function
                    Map<String, Object> pointMap = (Map<String, Object>) value;
                    int x = ((Number) pointMap.get("width")).intValue();
                    int y = ((Number) pointMap.get("height")).intValue();
                    setter.invoke(target, x, y);
                } else {
                    final Class paramType = setter.getParameterTypes()[0];
                    if (setterName.contains("Color")) {
                        value = Color.parseColor((String) value);
                    } else if (paramType == int.class) {
                        value = ((Number)value).intValue();
                    } else if (paramType == float.class) {
                        value = ((Number) value).floatValue();
                    } else if (paramType == double.class) {
                        value = ((Number) value).doubleValue();
                    } else if (paramType.isEnum()) {
                        try {
                            value = Enum.valueOf(paramType, (String) value);
                        }
                        catch(Exception ignored) {
                            // If the enum cannot be mapped properly the property will not be set
                            return;
                        }
                    }

                    setter.invoke(target, value);
                }
            }
        }
    }

    private static class TryToConvertValueResult {
        public final Object value;
        public TryToConvertValueResult(final Object value) {
            this.value = value;
        }
    }

    private static TryToConvertValueResult tryToConvertValue(final String propertyName, final Object value) {
        //noinspection SwitchStatementWithTooFewBranches
        switch(propertyName) {
            case "interfaceOrientation":
                return new TryToConvertValueResult(JSONUtils.convertInterfaceOrientation((String) value));
            default:
                return null;
        }
    }

    /*
     * Used when the native setter name differs from the JSON property name.
     * Using this method you can remap the Json Key to the Native Key so that
     * the setter is found on the target object, and the ObjectMapper can
     * automatically map the value
     */
    private static String tryToConvertToNativeKey(final String nativeKey) {
        //noinspection SwitchStatementWithTooFewBranches
        switch(nativeKey) {
            case "interfaceOrientation":
                return "orientationLockMode";
            default:
                return nativeKey;
        }
    }

    private static boolean shouldIgnoreProperty(final String propertyKey) {
        for(String property: kIgnoredProperties) {
            if (property.equals(propertyKey)) {
                return true;
            }
        }
        return false;
    }
}

