/*
 * System.Reflection-like API for access to .NET assemblies (DLL & EXE)
 */


package ch.epfl.lamp.compiler.msil;

import ch.epfl.lamp.compiler.msil.util.Signature;

import java.util.Map;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Iterator;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.io.UnsupportedEncodingException;

/**
 * Describes custom attribute instances.
 *
 * @author Nikolay Mihaylov
 * @version 1.0
 */
public class Attribute {

    //##########################################################################

    private final ConstructorInfo constr;

    private final byte[] value;

    Attribute(ConstructorInfo constr, byte[] value) {
        assert constr != null;
	this.constr = constr;
        assert value != null : constr.toString();
	this.value = value;
    }

    //##########################################################################
    // public interface

    /** @return the type (class) of the attribute. */
    public Type GetType() { return constr.DeclaringType; }

    /** @return the constructor of this attribute. */
    public ConstructorInfo getConstructor() {
	return constr;
    }

    /** @return the Blob with serialized constructor & named arguments. */
    public byte[] getValue() {
	byte[] value = new byte[this.value.length];
	System.arraycopy(this.value, 0, value, 0, value.length);
	return value;
    }

    /**@return an array with the arguments to the attribute's constructor. */
    public Object[] getConstructorArguments() {
        parseBlob();
        Object[] cas = new Object[constrArgs.length];
        System.arraycopy(constrArgs, 0, cas, 0, cas.length);
        return cas;
    }

    /** @return the named argument with the given name. */
    public NamedArgument getNamedArgument(String name) {
        return (NamedArgument)namedArgs.get(name);
    }

    /** @return an array of all named arguments for this attribute. */
    public NamedArgument[] getNamedArguments() {
        NamedArgument[] nargs =
            (NamedArgument[])namedArgs.values().toArray(NamedArgument.EMPTY);
        return nargs;
    }

    /** @return a string representation of this attribute. */
    public String toString() {
        parseBlob();
	ParameterInfo[] params = constr.GetParameters();
	assert params.length == constrArgs.length : this.constr;
        StringBuffer str = new StringBuffer();
	str.append('[');
	str.append(constr.DeclaringType.FullName);
        str.append('(');
        for (int i = 0; i < constrArgs.length; i++) {
            if (i > 0)
                str.append(", ");
	    Type t = params[i].ParameterType;
	    if (t.IsEnum()) {
		str.append('(');
		str.append(t.FullName);
		str.append(')');
	    }
            formatValue(str, constrArgs[i]);
        }
        NamedArgument[] nargs = getNamedArguments();
        for (int i = 0; i < nargs.length; i++) {
            str.append(", ").append(nargs[i]);
            }
        str.append(")]");
	return str.toString();
    }

    //#########################################################################

    private static final Map type2id = new HashMap();
    private static final Map id2type = new HashMap();
    static {
        map("Boolean", Signature.ELEMENT_TYPE_BOOLEAN);
        map("Char", Signature.ELEMENT_TYPE_CHAR);
        map("SByte", Signature.ELEMENT_TYPE_I1);
        map("Byte", Signature.ELEMENT_TYPE_U1);
        map("Int16", Signature.ELEMENT_TYPE_I2);
        map("UInt16", Signature.ELEMENT_TYPE_U2);
        map("Int32", Signature.ELEMENT_TYPE_I4);
        map("UInt32", Signature.ELEMENT_TYPE_U4);
        map("Int64", Signature.ELEMENT_TYPE_I8);
        map("UInt64", Signature.ELEMENT_TYPE_U8);
        map("Single", Signature.ELEMENT_TYPE_R4);
        map("Double", Signature.ELEMENT_TYPE_R8);
        map("String", Signature.ELEMENT_TYPE_STRING);
        map("Type", Signature.X_ELEMENT_TYPE_TYPE);
        map("Object", Signature.ELEMENT_TYPE_OBJECT);
    }
    private static void map(String type, int id) {
        Type t = Type.GetType("System." + type);
        assert type != null : type + " -> " + id;
        Integer i = new Integer(id);
        type2id.put(t, i);
        id2type.put(i, t);
    }
    private static int getTypeId(Type type) {
        Integer id = (Integer)type2id.get(type);
        assert id != null : type;
        return id.intValue();
    }

    private Object[] constrArgs;
    private Map namedArgs;
    private ByteBuffer buf;

    private void parseBlob() {
        try { parseBlob0(); }
        catch (RuntimeException e) {
            throw new RuntimeException(PEFile.bytes2hex(value), e);
        }
    }

    private void parseBlob0() {
        if (buf != null)
            return;
        buf = ByteBuffer.wrap(value);
        buf.order(ByteOrder.LITTLE_ENDIAN);

        short sig = buf.getShort();
        assert sig == 1 : PEFile.bytes2hex(value);
        ParameterInfo[] params = constr.GetParameters();
        constrArgs = new Object[params.length];
        for (int i = 0; i < params.length; i++) {
            constrArgs[i] = parseElement(params[i].ParameterType);
        }

        int ncount = buf.getShort();
        namedArgs = new LinkedHashMap();
        for (int i = 0; i < ncount; i++) {
            int designator = buf.get();
            assert designator == Signature.X_ELEMENT_KIND_FIELD
                || designator == Signature.X_ELEMENT_KIND_PROPERTY
                : "0x" + PEFile.byte2hex(designator);
            Type type = parseType();
            String name = parseString();
            Object value = parseElement(type);
            NamedArgument narg =
                new NamedArgument(designator, name, type, value);
            namedArgs.put(name, narg);
        }
    }

    private Object parseElement(Type type) {
        if (type.IsArray())
	    return parseArray(type.GetElementType());
	if (type.IsEnum())
	    return parseElement(type.getUnderlyingType());
	return parseElement(getTypeId(type));
    }

    private Object parseElement(int id) {
        switch (id) {
        case Signature.ELEMENT_TYPE_BOOLEAN:
            return new Boolean(buf.get() == 0 ? false : true);
        case Signature.ELEMENT_TYPE_CHAR:
            return new Character(buf.getChar());
        case Signature.ELEMENT_TYPE_I1:
        case Signature.ELEMENT_TYPE_U1:
            return new Byte(buf.get());
        case Signature.ELEMENT_TYPE_I2:
        case Signature.ELEMENT_TYPE_U2:
            return new Short(buf.getShort());
        case Signature.ELEMENT_TYPE_I4:
        case Signature.ELEMENT_TYPE_U4:
            return new Integer(buf.getInt());
        case Signature.ELEMENT_TYPE_I8:
        case Signature.ELEMENT_TYPE_U8:
            return new Long(buf.getLong());
        case Signature.ELEMENT_TYPE_R4:
            return new Float(buf.getFloat());
        case Signature.ELEMENT_TYPE_R8:
            return new Double(buf.getDouble());
        case Signature.ELEMENT_TYPE_STRING:
            return parseString();
        case Signature.X_ELEMENT_TYPE_TYPE:
                return getType();
        case Signature.ELEMENT_TYPE_OBJECT:
            Type type = parseType();
            return new BoxedArgument(type, parseElement(type));
        default:
            throw new RuntimeException("Unknown type id: " + id);
        }
    }

    private Object parseArray(Type type) {
	if (type.IsEnum())
	    return parseArray(type.getUnderlyingType());
	return parseArray(getTypeId(type));
    }

    private Object parseArray(int id) {
        switch (id) {
        case Signature.ELEMENT_TYPE_BOOLEAN:
            return parseBooleanArray();
        case Signature.ELEMENT_TYPE_CHAR:
            return parseCharArray();
        case Signature.ELEMENT_TYPE_I1:
        case Signature.ELEMENT_TYPE_U1:
            return parseByteArray();
        case Signature.ELEMENT_TYPE_I2:
        case Signature.ELEMENT_TYPE_U2:
            return parseShortArray();
        case Signature.ELEMENT_TYPE_I4:
        case Signature.ELEMENT_TYPE_U4:
            return parseIntArray();
        case Signature.ELEMENT_TYPE_I8:
        case Signature.ELEMENT_TYPE_U8:
            return parseLongArray();
        case Signature.ELEMENT_TYPE_R4:
            return parseFloatArray();
        case Signature.ELEMENT_TYPE_R8:
            return parseDoubleArray();
        case Signature.ELEMENT_TYPE_STRING:
            return parseStringArray();
        case Signature.X_ELEMENT_TYPE_ENUM:
	    return parseArray(getType());
        default:
            throw new RuntimeException("Unknown type id: " + id);
        }
    }

    private Type parseType() {
        int id = buf.get();
        switch (id) {
        case Signature.ELEMENT_TYPE_SZARRAY:
            return Type.mkArray(parseType(), 1);
        case Signature.X_ELEMENT_TYPE_ENUM:
            return getType();
        default:
            Type t = (Type)id2type.get(new Integer(id));
            assert t != null : PEFile.byte2hex(id);
            return t;
        }
    }

    private Type getType() {
        String typename = parseString();
        int i = typename.indexOf(',');
        // fully qualified assembly name follows
        // just strip it on the assumtion theat the
        // assembly is referenced in the externs and the
        // type will be found
        String name = (i < 0) ? typename : typename.substring(0, i);
        Type t = Type.GetType(name);
        if (t == null && i > 0) {
            int j = typename.indexOf(',', i + 1);
            if (j > 0) {
                String assemName = typename.substring(i + 1, j);
                try {
                    Assembly.LoadFrom(assemName);
                } catch (Throwable e) {
                    throw new RuntimeException(typename, e);
                }
                t = Type.GetType(name);
            }
        }
        assert t != null : typename;
        return t;
    }

    private boolean[] parseBooleanArray() {
        boolean[] arr = new boolean[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.get() == 0 ? false : true;
        return arr;
    }

    private char[] parseCharArray() {
        char[] arr = new char[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.getChar();
        return arr;
    }

    private byte[] parseByteArray() {
        byte[] arr = new byte[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.get();
        return arr;
    }

    private short[] parseShortArray() {
        short[] arr = new short[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.getShort();
        return arr;
    }

    private int[] parseIntArray() {
        int[] arr = new int[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.getInt();
        return arr;
    }

    private long[] parseLongArray() {
        long[] arr = new long[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.getLong();
        return arr;
    }

    private float[] parseFloatArray() {
        float[] arr = new float[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.getFloat();
        return arr;
    }

    private double[] parseDoubleArray() {
        double[] arr = new double[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = buf.getDouble();
        return arr;
    }

    private String[] parseStringArray() {
        String[] arr = new String[buf.getInt()];
        for (int i = 0; i < arr.length; i++)
            arr[i] = parseString();
        return arr;
    }

    private String parseString() {
        String str = null;
        int length = parseLength();
        if (length < 0)
            return null;
        try { str = new String(value, buf.position(), length, "UTF-8" ); }
        catch (UnsupportedEncodingException e) { throw new Error(e); }
        buf.position(buf.position() + length);
        return str;
    }

    private int getByte() {
        return (buf.get() + 0x0100) & 0xff;
    }

    public int parseLength() {
	int length = getByte();
        // check for invalid length format: the first, second or third
        // most significant bits should be 0; if all are 1 the length is invalid.
        if ((length & 0xe0) == 0xe0)
            return -1;
	if ((length & 0x80) != 0) {
	    length = ((length & 0x7f) << 8) | getByte();
	    if ((length & 0x4000) != 0)
		length = ((length & 0x3fff) << 16) | (getByte()<<8) | getByte();
	}
	return length;
    }

    //##########################################################################
    private static void formatValue(StringBuffer str, Object o) {
        Class c = (o == null) ? null : o.getClass();
        if (c == null) {
            str.append("<null>");
        } else if (c == String.class) {
            str.append('"');
            str.append(o);
            str.append('"');
        } else if (c == Character.class) {
            str.append('\'');
            str.append(o);
            str.append('\'');
        } else if (c == boolean[].class) {
            str.append("new boolean[] {");
            boolean[] arr = (boolean[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == char[].class) {
            str.append("new short[] {");
            short[] arr = (short[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == byte[].class) {
            str.append("new byte[] {");
            byte[] arr = (byte[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == short[].class) {
            str.append("new short[] {");
            short[] arr = (short[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == int[].class) {
            str.append("new int[] {");
            int[] arr = (int[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == long[].class) {
            str.append("new long[] {");
            long[] arr = (long[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == float[].class) {
            str.append("new float[] {");
            float[] arr = (float[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == double[].class) {
            str.append("new double[] {");
            double[] arr = (double[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                str.append(arr[i]);
            }
            str.append('}');
        } else if (c == String[].class) {
            str.append("new String[] {");
            String[] arr = (String[])o;
            for (int i = 0; i < arr.length; i++) {
                if (i > 0) str.append(", ");
                formatValue(str, arr[i]);
            }
            str.append('}');
	} else if (o instanceof Type) {
	    str.append("typeof(");
	    str.append(o);
	    str.append(")");
        } else
            str.append(o);
    }

    //##########################################################################

    /** Represents named arguments (assigned outside of the constructor)
     *  of a custom attribute
     */
    public static class NamedArgument {

        /** Designates if the named argument corresponds to a field or property.
         *  Possible values:
         *      Signature.X_ELEMENT_KIND_FIELD = 0x53
         *      Signature.X_ELEMENT_KIND_PROPERTY = 0x54
         */
        public final int designator;

        /** The name of the field/property. */
        public final String name;

        /** Type of the field/property. */
        public final Type type;

        /** The value for the field/property. */
        public final Object value;

        /** An empty array NamedArgument. */
        public static final NamedArgument[] EMPTY = new NamedArgument[0];

        public NamedArgument(int designator, String name,Type type,Object value)
        {
            this.designator = designator;
            this.name = name;
            this.type = type;
            this.value = value;
        }

        /** @return <b>true</b> if the named argument specifies a field;
         *  <b>false<b> otherwise.
         */
        public boolean isField() {
            return designator == Signature.X_ELEMENT_KIND_FIELD;
        }

        /** @return <b>true</b> if the named argument specifies a property;
         *  <b>false<b> otherwise.
         */
        public boolean isProperty() {
            return designator == Signature.X_ELEMENT_KIND_PROPERTY;
        }

        /** @return a string representation of the named argument. */
        public String toString() {
            StringBuffer str = new StringBuffer(name);
            str.append(" = ");
            if (type.IsEnum())
                str.append('(').append(type.FullName).append(')');
            formatValue(str, value);
            return str.toString();
        }
    }

    //##########################################################################

    protected static final class BoxedArgument {
        public final Type type;
        public final Object value;
        public BoxedArgument(Type type, Object value) {
            this.type = type; this.value = value;
        }
        public String toString() {
            return "(" + type.FullName + ")" + value;
        }
    }

    //##########################################################################

}  // class Attribute
