package cn.thens.okbinder2;

import android.os.Binder;
import android.os.IBinder;
import android.util.Base64;
import android.util.Log;

import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SuppressWarnings({"ConstantConditions", "unchecked"})
public final class OkBinder {
    static final String TAG = "@OkBinder";

    public static Binder create(Object remoteObject) {
        Class<?> okBinderInterface = getOkBinderInterface(remoteObject);
        require(okBinderInterface != null,
                "Remote object must implement only one interface with @AIDL annotation");
        return create((Class<Object>) okBinderInterface, remoteObject);
    }

    public static <T> Binder create(Class<T> okBinderInterface, T remoteObject) {
        return getFactory(okBinderInterface).newBinder(okBinderInterface, remoteObject);
    }

    public static <T> T proxy(Class<T> serviceClass, IBinder binder) {
        if (binder instanceof OkBinderFactory.BaseBinder) {
            return (T) ((OkBinderFactory.BaseBinder) binder).getRemoteObject();
        }
        return (T) getFactory(serviceClass).newProxy(serviceClass, binder);
    }

    private static final Map<Class<?>, OkBinderFactory> factories = new HashMap<>();
    private static final OkBinderFactory defaultFactory = new ReflectionFactory();

    private static OkBinderFactory getFactory(Class<?> serviceClass) {
        require(isOkBinderInterface(serviceClass),
                "Service class must be an interface with @AIDL annotation");
        OkBinderFactory factory = factories.get(serviceClass);
        if (factory != null) {
            return factory;
        }
        String factoryClassName = serviceClass.getName() + "Factory";
        try {
            ClassLoader classLoader = serviceClass.getClassLoader();
            Class<?> factoryClass = classLoader.loadClass(factoryClassName);
            factory = (OkBinderFactory) factoryClass.newInstance();
        } catch (Throwable e) {
            Log.w(TAG, "Unable to load the factory class " + factoryClassName + ", " +
                    "the default factory will be used");
        }
        if (factory == null) {
            factory = defaultFactory;
        }
        factories.put(serviceClass, factory);
        return factory;
    }

    private static void require(boolean value, String message) {
        if (!value) throw new IllegalArgumentException(message);
    }

    static boolean isOkBinderInterface(Class<?> cls) {
        return cls.isInterface() && cls.isAnnotationPresent(AIDL.class);
    }

    static Class<?> getOkBinderInterface(Object object) {
        Class<?>[] interfaces = object.getClass().getInterfaces();
        List<Class<?>> okBinderInterfaces = new ArrayList<>();
        for (Class<?> anInterface : interfaces) {
            if (isOkBinderInterface(anInterface)) {
                okBinderInterfaces.add(anInterface);
            }
        }
        if (okBinderInterfaces.size() != 1) return null;
        return okBinderInterfaces.get(0);
    }

    public static String getFunctionId(Method method) {
        StringBuilder functionId = new StringBuilder(method.getName());
        StringBuilder params = new StringBuilder();
        boolean isFirst = true;
        for (Class<?> paramType : method.getParameterTypes()) {
            params.append(isFirst ? "" : ",").append(paramType.getName());
            isFirst = false;
        }
        if (params.length() <= 24) {
            return functionId.append("(").append(params).append(")").toString();
        }
        try {
            byte[] bytes = params.toString().getBytes();
            byte[] md5 = MessageDigest.getInstance("MD5").digest(bytes);
            String base64 = Base64.encodeToString(md5, Base64.NO_WRAP);
            return functionId.append("(").append(base64).append(")").toString();
        } catch (Throwable e) {
            throw ErrorUtils.wrap(e);
        }
    }
}
