/*
 This file is part of GNUnet.
 (C) 2011, 2012 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.construct;


import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

/**
 * Utilities for convenient use of the java reflection API.
 * All methods only throw non-checked exceptions.
 */
public class ReflectUtil {
    public static <T> T justInstantiate(Class<T> c) {
        try {
            return c.getConstructor().newInstance();
        } catch (InstantiationException e) {
            throw new AssertionError("Cannot instantiate " + c);
        } catch (IllegalAccessException e) {
            throw new AssertionError(
                    String.format("Cannot instantiate Message %s (illegal access)", c));
        } catch (NoSuchMethodException e) {
            if (c.isMemberClass()) {
                throw new AssertionError(String.format("Can not instantiate non-static member class %s", c));
            } else {
                throw new AssertionError(
                        String.format("No suitable default constructor for class %s", c));
            }
        } catch (InvocationTargetException e) {
            throw new AssertionError(
                    String.format("Exception thrown while constructing object of class %s", c));
        }
    }

    public static void justSetArray(Object arr, int i, long v) {
        Class t = arr.getClass().getComponentType();
        if (t.equals(Long.TYPE)) {
            Array.setLong(arr, i, v);
        } else if (t.equals(Integer.TYPE)) {
            Array.setInt(arr, i, (int) v);
        } else if (t.equals(Short.TYPE)) {
            Array.setShort(arr, i, (short) v);
        } else if (t.equals(Byte.TYPE)) {
            Array.setByte(arr, i, (byte) v);
        } else if (t.equals(Character.TYPE)) {
            Array.setChar(arr, i, (char) v);
        }
    }

    public static long justGetArrayLong(Object arr, int i) {
        return Array.getLong(arr, i);
    }

    /**
     * An enumeration of all built-in type that can store integers.
     */
    public enum NumFieldType {
        BIGNUM, BYTE_PRIM, SHORT_PRIM, INT_PRIM, LONG_PRIM, BOOLEAN, CHAR_PRIM
    }

    /**
     * Convenience wrapper for a field that stores a numeric value.
     */
    public static class NumField {
        final private Field targetField;
        final private NumFieldType targetType;
        
        
        public NumFieldType getNumFieldType() {
            return targetType;
        }

        public NumField(Field f) {
            this.targetField = f;
            if (f.getType().equals(Long.TYPE)) {
                targetType = NumFieldType.LONG_PRIM;
            } else if (f.getType().equals(Integer.TYPE)) {
                targetType = NumFieldType.INT_PRIM;
            } else if (f.getType().equals(Short.TYPE)) {
                targetType = NumFieldType.SHORT_PRIM;
            } else if (f.getType().equals(Byte.TYPE)) {
                targetType = NumFieldType.BYTE_PRIM;
            } else if (f.getType().equals(Character.TYPE)) {
                targetType = NumFieldType.CHAR_PRIM;
            } else if (f.getType().equals(BigInteger.class)) {
                targetType = NumFieldType.BIGNUM;
            } else if (f.getType().equals(Boolean.TYPE)) {
                targetType = NumFieldType.BOOLEAN;
            } else {
                throw new AssertionError(
                        "expected numeric type, got: " + f.getType());
            }
        }

        public void set(Object obj, long val) {
            try {
                switch (targetType) {
                    case LONG_PRIM:
                        targetField.setLong(obj, val);
                        break;
                    case INT_PRIM:
                        targetField.setInt(obj, (int) val);
                        break;
                    case SHORT_PRIM:
                        targetField.setShort(obj, (short) val);
                        break;
                    case BYTE_PRIM:
                        targetField.setByte(obj, (byte) val);
                        break;
                    case CHAR_PRIM:
                        targetField.setChar(obj, (char) val);
                        break;
                    case BIGNUM:
                        targetField.set(obj, BigInteger.valueOf(val));
                        break;
                    case BOOLEAN:
                        targetField.setBoolean(obj, (val != 0));
                        break;
                }
            } catch (IllegalArgumentException e) {
                throw new AssertionError("cannot access field");
            } catch (IllegalAccessException e) {
                throw new AssertionError("cannot access field");
            }
        }
        
        public void set(Object obj, BigInteger val) {
            try {
                targetField.set(obj, val);
            } catch (IllegalAccessException e) {
                throw new AssertionError("cannot access field");
            }
        }

        public long get(Object obj) {
            try {
                switch (targetType) {
                    case LONG_PRIM:
                        return targetField.getLong(obj);
                    case INT_PRIM:
                        return targetField.getInt(obj);
                    case SHORT_PRIM:
                        return targetField.getShort(obj);
                    case BYTE_PRIM:
                        return targetField.getByte(obj);
                    case CHAR_PRIM:
                        return targetField.getChar(obj);
                    case BOOLEAN:
                        return targetField.getBoolean(obj) ? 1 : 0;
                    case BIGNUM:
                        throw new RuntimeException("get() called on NumField that is a BigInteger (getBig() must be used instead)");
                    default:
                        throw new AssertionError("unreachable");
                }
            } catch (IllegalAccessException e) {
                throw new AssertionError("cannot access field");
            }
        }
        
        public BigInteger getBig(Object obj) {
            if (isBig()) {
                return (BigInteger) justGet(obj, targetField);
            } else {
                return BigInteger.valueOf(this.get(obj));
            }
        }

        public boolean isBig() {
            return targetType.equals(NumFieldType.BIGNUM);
        }
    }


    public static Object followFieldPath(List<Field> fl, Object obj,
                                         int depth) {
        for (int i = 0; i < depth; ++i) {
            try {
                obj = fl.get(i).get(obj);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError("cannot access field " + fl.get(i)
                        + " of " + obj.getClass());
            }
        }
        return obj;
    }

    public static Object followFieldPath(List<Field> fl, Object obj) {
        return followFieldPath(fl, obj, fl.size());
    }

    public static Object followFieldPathToParent(List<Field> fl, Object obj) {
        return followFieldPath(fl, obj, fl.size() - 1);
    }

    public static Object justGet(Object obj, Field f) {
        try {
            return f.get(obj);
        } catch (IllegalAccessException e) {
            throw new AssertionError(
                    String.format("Cannot access private field '%s' in class %s", f, obj.getClass()));
        } catch (IllegalArgumentException e) {
            throw new AssertionError("Cannot access field '" + f.getName() + "' in class " + obj.getClass());
        }
    }

    public static void justSet(Object obj, Field f, Object val) {
        try {
            f.set(obj, val);
        } catch (IllegalAccessException e) {
            throw new AssertionError(
                    String.format("Cannot access private field %s in class %s", f, obj.getClass()));
        }
    }


    public static int justGetInt(Object obj, List<Field> path) {
        for (int i = 0; i < path.size() - 1; ++i) {
            try {
                obj = path.get(i).get(obj);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError("cannot access field " + path.get(i)
                        + " of " + obj.getClass());
            }
        }

        try {
            return path.get(path.size() - 1).getInt(obj);
        } catch (IllegalAccessException e) {
            throw new AssertionError("cannot access field " + path.get(path.size() - 1)
                    + " of " + obj.getClass());
        }
    }

    public static void justSetInt(Object obj, List<Field> path, int val) {
        for (int i = 0; i < path.size() - 1; ++i) {
            try {
                obj = path.get(i).get(obj);

            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError("cannot access field " + path.get(i)
                        + " of " + obj.getClass());
            }
        }

        try {
            path.get(path.size() - 1).setInt(obj, val);
        } catch (IllegalAccessException e) {
            throw new AssertionError("cannot access field " + path.get(path.size() - 1)
                    + " of " + obj.getClass());
        }
    }


    public static List<Field> getFieldPathFromString(final String p, final Class root) {
        Class current = root;

        String[] components = p.split("[.]");

        List<Field> fp = new ArrayList<Field>(components.length);
        for (String member : components) {
            Field f;
            try {
                f = current.getField(member);
            } catch (NoSuchFieldException e) {
                throw new AssertionError("invalid field path, component " + member + " not found");
            }

            fp.add(f);

            current = f.getType();
        }

        return fp;
    }
}
