/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.stack.core.util;

import java.util.UUID;

import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableMap;
import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.DiagnosticInfo;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.builtin.XmlElement;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import org.jetbrains.annotations.Nullable;

public class TypeUtil {

    private static final ImmutableMap<Integer, Class<?>> BUILTIN_TYPES;
    private static final ImmutableMap<Class<?>, Integer> BUILTIN_TYPES_INVERSE;

    private static final ImmutableMap<Class<?>, Integer> PRIMITIVE_BUILTIN_TYPES;
    private static final ImmutableMap<Integer, Class<?>> PRIMITIVE_BUILTIN_TYPES_INVERSE;

    static {
        HashBiMap<Integer, Class<?>> builtinTypes = HashBiMap.create();
        builtinTypes.put(1, Boolean.class);
        builtinTypes.put(2, Byte.class);
        builtinTypes.put(3, UByte.class);
        builtinTypes.put(4, Short.class);
        builtinTypes.put(5, UShort.class);
        builtinTypes.put(6, Integer.class);
        builtinTypes.put(7, UInteger.class);
        builtinTypes.put(8, Long.class);
        builtinTypes.put(9, ULong.class);
        builtinTypes.put(10, Float.class);
        builtinTypes.put(11, Double.class);
        builtinTypes.put(12, String.class);
        builtinTypes.put(13, DateTime.class);
        builtinTypes.put(14, UUID.class);
        builtinTypes.put(15, ByteString.class);
        builtinTypes.put(16, XmlElement.class);
        builtinTypes.put(17, NodeId.class);
        builtinTypes.put(18, ExpandedNodeId.class);
        builtinTypes.put(19, StatusCode.class);
        builtinTypes.put(20, QualifiedName.class);
        builtinTypes.put(21, LocalizedText.class);
        builtinTypes.put(22, ExtensionObject.class);
        builtinTypes.put(23, DataValue.class);
        builtinTypes.put(24, Variant.class);
        builtinTypes.put(25, DiagnosticInfo.class);

        BUILTIN_TYPES = ImmutableMap.copyOf(builtinTypes);
        BUILTIN_TYPES_INVERSE = ImmutableMap.copyOf(builtinTypes.inverse());

        HashBiMap<Class<?>, Integer> primitiveBuiltinTypes = HashBiMap.create();
        primitiveBuiltinTypes.put(boolean.class, 1);
        primitiveBuiltinTypes.put(byte.class, 2);
        primitiveBuiltinTypes.put(short.class, 4);
        primitiveBuiltinTypes.put(int.class, 6);
        primitiveBuiltinTypes.put(long.class, 8);
        primitiveBuiltinTypes.put(float.class, 10);
        primitiveBuiltinTypes.put(double.class, 11);

        PRIMITIVE_BUILTIN_TYPES = ImmutableMap.copyOf(primitiveBuiltinTypes);
        PRIMITIVE_BUILTIN_TYPES_INVERSE = ImmutableMap.copyOf(primitiveBuiltinTypes.inverse());
    }

    /**
     * @param backingType the backing {@link Class} of the builtin type.
     * @return the id of the builtin type backed by {@code backingType}, or -1 if backingType is not builtin.
     */
    public static int getBuiltinTypeId(Class<?> backingType) {
        if (backingType.isPrimitive()) {
            return PRIMITIVE_BUILTIN_TYPES.getOrDefault(backingType, -1);
        } else {
            return BUILTIN_TYPES_INVERSE.getOrDefault(backingType, -1);
        }
    }

    /**
     * @param typeId the id of the builtin type.
     * @return true if the type is builtin.
     */
    public static boolean isBuiltin(NodeId typeId) {
        return BUILTIN_TYPES.containsKey(id(typeId));
    }

    /**
     * @param typeId the id of the builtin type.
     * @return true if the type is builtin.
     */
    public static boolean isBuiltin(ExpandedNodeId typeId) {
        return BUILTIN_TYPES.containsKey(id(typeId));
    }

    /**
     * @param id the id of the builtin type.
     * @return the {@link Class} backing the builtin type.
     */
    @Nullable
    public static Class<?> getBackingClass(int id) {
        return BUILTIN_TYPES.get(id);
    }

    /**
     * @param typeId the id of the builtin type.
     * @return the {@link Class} backing the builtin type.
     */
    @Nullable
    public static Class<?> getBackingClass(NodeId typeId) {
        return getBackingClass(id(typeId));
    }

    /**
     * @param id the id of the builtin type.
     * @return the {@link Class} backing the builtin type, favoring the primitive version, if applicable.
     */
    @Nullable
    public static Class<?> getPrimitiveBackingClass(int id) {
        return PRIMITIVE_BUILTIN_TYPES_INVERSE.getOrDefault(id, getBackingClass(id));
    }

    /**
     * @param typeId the id of the builtin type.
     * @return the {@link Class} backing the builtin type, favoring the primitive version, if applicable.
     */
    @Nullable
    public static Class<?> getPrimitiveBackingClass(NodeId typeId) {
        return getPrimitiveBackingClass(id(typeId));
    }

    private static int id(NodeId nodeId) {
        if (nodeId.getIdentifier() instanceof UInteger) {
            return ((UInteger) nodeId.getIdentifier()).intValue();
        } else {
            return -1;
        }
    }

    private static int id(ExpandedNodeId nodeId) {
        if (nodeId.getIdentifier() instanceof UInteger) {
            return ((UInteger) nodeId.getIdentifier()).intValue();
        } else {
            return -1;
        }
    }

}
