/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package android.hardware.camera2.marshal.impl;

import android.hardware.camera2.marshal.Marshaler;
import android.hardware.camera2.marshal.MarshalQueryable;
import android.hardware.camera2.utils.TypeReference;
import android.util.Log;

import java.nio.ByteBuffer;
import java.util.HashMap;

import static android.hardware.camera2.impl.CameraMetadataNative.*;
import static android.hardware.camera2.marshal.MarshalHelpers.*;

/**
 * Marshal any simple enum (0-arg constructors only) into/from either
 * {@code TYPE_BYTE} or {@code TYPE_INT32}.
 *
 * <p>Default values of the enum are mapped to its ordinal; this can be overridden
 * by providing a manual value with {@link #registerEnumValues}.</p>

 * @param <T> the type of {@code Enum}
 */
public class MarshalQueryableEnum<T extends Enum<T>> implements MarshalQueryable<T> {

    private static final String TAG = MarshalQueryableEnum.class.getSimpleName();
    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);

    private static final int UINT8_MIN = 0x0;
    private static final int UINT8_MAX = (1 << Byte.SIZE) - 1;
    private static final int UINT8_MASK = UINT8_MAX;

    private class MarshalerEnum extends Marshaler<T> {

        private final Class<T> mClass;

        @SuppressWarnings("unchecked")
        protected MarshalerEnum(TypeReference<T> typeReference, int nativeType) {
            super(MarshalQueryableEnum.this, typeReference, nativeType);

            mClass = (Class<T>)typeReference.getRawType();
        }

        @Override
        public void marshal(T value, ByteBuffer buffer) {
            int enumValue = getEnumValue(value);

            if (mNativeType == TYPE_INT32) {
                buffer.putInt(enumValue);
            } else if (mNativeType == TYPE_BYTE) {
                if (enumValue < UINT8_MIN || enumValue > UINT8_MAX) {
                    throw new UnsupportedOperationException(String.format(
                            "Enum value %x too large to fit into unsigned byte", enumValue));
                }
                buffer.put((byte)enumValue);
            } else {
                throw new AssertionError();
            }
        }

        @Override
        public T unmarshal(ByteBuffer buffer) {
            int enumValue;

            switch (mNativeType) {
                case TYPE_INT32:
                    enumValue = buffer.getInt();
                    break;
                case TYPE_BYTE:
                    // get the unsigned byte value; avoid sign extension
                    enumValue = buffer.get() & UINT8_MASK;
                    break;
                default:
                    throw new AssertionError(
                            "Unexpected native type; impossible since its not supported");
            }

            return getEnumFromValue(mClass, enumValue);
        }

        @Override
        public int getNativeSize() {
            return getPrimitiveTypeSize(mNativeType);
        }
    }

    @Override
    public Marshaler<T> createMarshaler(TypeReference<T> managedType, int nativeType) {
        return new MarshalerEnum(managedType, nativeType);
    }

    @Override
    public boolean isTypeMappingSupported(TypeReference<T> managedType, int nativeType) {
        if (nativeType == TYPE_INT32 || nativeType == TYPE_BYTE) {
            if (managedType.getType() instanceof Class<?>) {
                Class<?> typeClass = (Class<?>)managedType.getType();

                if (typeClass.isEnum()) {
                    if (VERBOSE) {
                        Log.v(TAG, "possible enum detected for " + typeClass);
                    }

                    // The enum must not take extra arguments
                    try {
                        // match a class like: "public enum Fruits { Apple, Orange; }"
                        typeClass.getDeclaredConstructor(String.class, int.class);
                        return true;
                    } catch (NoSuchMethodException e) {
                        // Skip: custom enum with a special constructor e.g. Foo(T), but need Foo()
                        Log.e(TAG, "Can't marshal class " + typeClass + "; no default constructor");
                    } catch (SecurityException e) {
                        // Skip: wouldn't be able to touch the enum anyway
                        Log.e(TAG, "Can't marshal class " + typeClass + "; not accessible");
                    }
                }
            }
        }

        return false;
    }

    @SuppressWarnings("rawtypes")
    private static final HashMap<Class<? extends Enum>, int[]> sEnumValues =
            new HashMap<Class<? extends Enum>, int[]>();

    /**
     * Register a non-sequential set of values to be used with the marshal/unmarshal functions.
     *
     * <p>This enables get/set to correctly marshal the enum into a value that is C-compatible.</p>
     *
     * @param enumType The class for an enum
     * @param values A list of values mapping to the ordinals of the enum
     */
    public static <T extends Enum<T>> void registerEnumValues(Class<T> enumType, int[] values) {
        if (enumType.getEnumConstants().length != values.length) {
            throw new IllegalArgumentException(
                    "Expected values array to be the same size as the enumTypes values "
                            + values.length + " for type " + enumType);
        }
        if (VERBOSE) {
            Log.v(TAG, "Registered enum values for type " + enumType + " values");
        }

        sEnumValues.put(enumType, values);
    }

    /**
     * Get the numeric value from an enum.
     *
     * <p>This is usually the same as the ordinal value for
     * enums that have fully sequential values, although for C-style enums the range of values
     * may not map 1:1.</p>
     *
     * @param enumValue Enum instance
     * @return Int guaranteed to be ABI-compatible with the C enum equivalent
     */
    private static <T extends Enum<T>> int getEnumValue(T enumValue) {
        int[] values;
        values = sEnumValues.get(enumValue.getClass());

        int ordinal = enumValue.ordinal();
        if (values != null) {
            return values[ordinal];
        }

        return ordinal;
    }

    /**
     * Finds the enum corresponding to it's numeric value. Opposite of {@link #getEnumValue} method.
     *
     * @param enumType Class of the enum we want to find
     * @param value The numeric value of the enum
     * @return An instance of the enum
     */
    private static <T extends Enum<T>> T getEnumFromValue(Class<T> enumType, int value) {
        int ordinal;

        int[] registeredValues = sEnumValues.get(enumType);
        if (registeredValues != null) {
            ordinal = -1;

            for (int i = 0; i < registeredValues.length; ++i) {
                if (registeredValues[i] == value) {
                    ordinal = i;
                    break;
                }
            }
        } else {
            ordinal = value;
        }

        T[] values = enumType.getEnumConstants();

        if (ordinal < 0 || ordinal >= values.length) {
            throw new IllegalArgumentException(
                    String.format(
                            "Argument 'value' (%d) was not a valid enum value for type %s "
                                    + "(registered? %b)",
                            value,
                            enumType, (registeredValues != null)));
        }

        return values[ordinal];
    }
}
