package de.renew.util;

import java.io.Serializable;


// I wrap an object corresponding to a primitive value
// into a special layer so that primitive values and
// the corresponding objects are not confused inside
// a place instance.

/**
 * Special wrapper for primitive value objects that makes primitive values distinct inside of place instances.
 *
 * @author Olaf Kummer
 * @author Michael Duvigneau
 */
public final class Value implements TextToken, Serializable {
    /**
     * The wrapped primitive type object holding some primitive type value.
     */
    public final Object value;

    /**
     * Creat a new Value instance from a primitive type (Byte, Short, Integer, Long, Float, Double, Character, Boolean)
     * @param value an Object which should be of type Byte, Short, Integer, Long, Float, Double, Character or Boolean.
     * @throws RuntimeException if the submitted Object is an instance of this class, a Throwable, or other not-primitive type.
     */
    public Value(Object value) {
        if (value instanceof Byte || value instanceof Short || value instanceof Integer
            || value instanceof Long || value instanceof Float || value instanceof Double
            || value instanceof Character || value instanceof Boolean) {
            this.value = value;
        } else {
            if (value instanceof Value) {
                throw new RuntimeException("Tried to make a nested value. Strange.");
            } else if (value instanceof Throwable) {
                throw new RuntimeException(
                    "Tried to make a Throwable a value. Strange. " + "Throwable is: " + value,
                    (Throwable) value);
            } else {
                throw new RuntimeException("Tried to make object " + value + " a value. Strange.");
            }
        }
    }

    @Override
    public int hashCode() {
        // Add a constant to distinguish a value from its
        // wrapped object.
        return value.hashCode() + 517293561;
    }

    @Override
    public boolean equals(Object that) {
        if (that instanceof Value) {
            return value.equals(((Value) that).value);
        } else {
            return false;
        }
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as a Boolean.
     *
     * @return A boolean cast primitive of the encapsulated object
     */
    public boolean booleanValue() {
        return (Boolean) value;
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as a Char
     *
     * @return A Char cast primitive of the encapsulated object
     */
    public char charValue() {
        return (Character) value;
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as a Byte
     *
     * @return A Byte cast primitive of the encapsulated object
     */
    public byte byteValue() {
        return ((Number) value).byteValue();
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as a Short
     *
     * @return a Short cast primitive of the encapsulated object
     */
    public short shortValue() {
        return ((Number) value).shortValue();
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as an Int
     *
     * @return An Int cast primitive of the encapsulated object
     */
    public int intValue() {
        return ((Number) value).intValue();
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as a Long
     *
     * @return a Long cast primitive of the encapsulated object
     */
    public long longValue() {
        return ((Number) value).longValue();
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as a Float
     *
     * @return a Float cast primitive of the encapsulated object
     */
    public float floatValue() {
        return ((Number) value).floatValue();
    }

    /**
     * Attempt to retrieve the wrapped primitive type object as a Double
     *
     * @return a Double cast primitive of the encapsulated object
     */
    public double doubleValue() {
        return ((Number) value).doubleValue();
    }

    /**
     * Retrieve a string representation of the wrapped primitive type
     *
     * @return A String representation of the encapsulated object
     */
    @Override
    public String toTokenText() {
        return value.toString();
    }

    /**
     * Returns a String representation of the type
     *
     * @return a String representation of the value, consistent of its class and the string representation of the value wrapped in brackets
     */
    @Override
    public String toString() {
        return Types.typify(value.getClass()) + "(" + value + ")";
    }

    /**
     * For a submitted object, can attempt to construct a Value wrapper around it
     *
     * @param obj  the Object to process
     * @param wrap controls if a wrap into a Value should be attempted or not.
     * @return the obj unaltered if wrap is False; the wrapped object is wrap is True and creation is possible
     * @throws RuntimeException if wrap is set to true and the construction contract is violated
     */
    static public Object possiblyWrap(Object obj, boolean wrap) {
        if (wrap) {
            obj = new Value(obj);
        }
        return obj;
    }

    /**
     * Convert primitive values represented by objects, allowing for
     * widening conversion.
     *
     * @param value the object to convert into a primitive value
     *              wrapper object
     * @param clazz the expected primitive type (should be one of
     *              {@link Character#TYPE},
     *              {@link Double#TYPE},
     *              {@link Float#TYPE},
     *              {@link Long#TYPE},
     *              {@link Integer#TYPE},
     *              {@link Short#TYPE}, or
     *              {@link Byte#TYPE}).
     * @return the converted value as object according to the
     * expected type.
     * @throws IllegalArgumentException if the value cannot be converted losslessly into the
     *                                  expected primitive type
     */
    static public Object convertPrimitive(Object value, Class<?> clazz) {
        // Try to return characters, if possible.
        if (value instanceof Character) {
            if (clazz == Character.TYPE) {
                return value;
            } else {
                // No, cast the character to an integer and
                // process it later.
                value = (int) (Character) value;
            }
        }

        if (value instanceof Number number) {
            if (clazz == Double.TYPE) {
                return number.doubleValue();
            }
            if (number instanceof Double) {
                throw new IllegalArgumentException();
            }
            if (clazz == Float.TYPE) {
                return number.floatValue();
            }
            if (number instanceof Float) {
                throw new IllegalArgumentException();
            }
            if (clazz == Long.TYPE) {
                return number.longValue();
            }
            if (number instanceof Long) {
                throw new IllegalArgumentException();
            }
            if (clazz == Integer.TYPE) {
                return number.intValue();
            }
            if (number instanceof Integer) {
                throw new IllegalArgumentException();
            }
            if (clazz == Short.TYPE) {
                return number.shortValue();
            }
            if (number instanceof Short) {
                throw new IllegalArgumentException();
            }
            if (clazz == Byte.TYPE) {
                return number.byteValue();
            }
            throw new IllegalArgumentException();
        } else if (value instanceof Boolean) {
            if (clazz == Boolean.TYPE) {
                return value;
            } else {
                throw new IllegalArgumentException();
            }
        } else {
            throw new RuntimeException("Encountered a bad value.");
        }
    }

    /**
     * Remove the valueness of this object and convert it
     * to the desired class, if that is possible.
     *
     * @param clazz the Class to attempt casting the wrapped primitive type object into
     * @return the unwrapped primitive type, cast to the submitted Class.
     * @throws IllegalArgumentException if the submitted Class is not a primitive type, or a conversion cannot happen
     *                                  losslessly.
     */
    public Object unvalueAndCast(Class<?> clazz) throws IllegalArgumentException {
        if (clazz.isPrimitive()) {
            return convertPrimitive(value, clazz);
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Remove the valueness of the argument and convert it
     * to the desired class, if that is possible.
     *
     * @param obj   the Value object to unwrap and convert
     * @param clazz the Class to attempt casting the wrapped primitive type into
     * @return null if both arguments are null; the unwrapped primitive type cast to the submitted Class.
     * @throws IllegalArgumentException if the obj is not of type Value, or if the submitted Class is not a primitive type, or the conversion cannot happen losslessly.
     */
    static public Object unvalueAndCast(Object obj, Class<?> clazz)
        throws IllegalArgumentException
    {
        if (obj instanceof Value) {
            return ((Value) obj).unvalueAndCast(clazz);
        } else {
            if (obj == null || clazz.isInstance(obj)) {
                return obj;
            } else {
                throw new IllegalArgumentException();
            }
        }
    }

    /**
     * Unvalue and cast a complete array.
     *
     * @param objs    an Array of Objects of type Value
     * @param clazzes an Array of desired classes which specify what class to cast the i-th value of the objs array to
     * @return an Array of unwrapped and cast Objects
     * @throws IllegalArgumentException if the arrays are of different lengths, or if the obj is not of type Value,
     *                                  or if the submitted Class is not a primitive type, or the conversion cannot happen losslessly.
     */
    static public Object[] unvalueAndCast(Object[] objs, Class<?>[] clazzes)
        throws IllegalArgumentException
    {
        Object[] results = new Object[objs.length];
        for (int i = 0; i < objs.length; i++) {
            results[i] = unvalueAndCast(objs[i], clazzes[i]);
        }
        return results;
    }

    /**
     * A unique return value for {@link #castOrReturnImpossible(Class, Object)}.
     */
    static public final Object IMPOSSIBLE_CAST = new Object();

    /**
     * Cast arg to class, possibly wrapping or unwrapping values.
     *
     * @param clazz the target class
     * @param arg   the argument value
     * @return the cast value or {@link #IMPOSSIBLE_CAST} when the cast is forbidden
     */
    static public Object castOrReturnImpossible(Class<?> clazz, Object arg) {
        if (clazz.isPrimitive()) {
            if (arg instanceof Value) {
                Object value = ((Value) arg).value;
                if (value instanceof Boolean) {
                    if (clazz == Boolean.TYPE) {
                        return arg;
                    }
                } else {
                    Number number;
                    if (value instanceof Character) {
                        char charVal = (Character) value;
                        number = (int) charVal;
                    } else {
                        number = (Number) value;
                    }
                    if (clazz == Character.TYPE) {
                        return new Value((char) number.intValue());
                    } else if (clazz == Byte.TYPE) {
                        return new Value(number.byteValue());
                    } else if (clazz == Short.TYPE) {
                        return new Value(number.shortValue());
                    } else if (clazz == Integer.TYPE) {
                        return new Value(number.intValue());
                    } else if (clazz == Long.TYPE) {
                        return new Value(number.longValue());
                    } else if (clazz == Float.TYPE) {
                        return new Value(number.floatValue());
                    } else if (clazz == Double.TYPE) {
                        return new Value(number.doubleValue());
                    }
                }
            }
        } else {
            if (arg == null || clazz.isInstance(arg)) {
                return arg;
            }
        }
        return IMPOSSIBLE_CAST;
    }
}