/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.reflection.inspect;

import java.lang.reflect.Array;
import net.ranides.assira.generic.PrivilegedActions;

/**
 * Klasa udostępniająca bezpośredni dostęp do tablic dowolnego typu znaczniej 
 * wydajniej i JIT-friendly, niż reflection API. Samodzielnie zarządza 
 * autoboxingiem typów primitive. 
 * <p>
 * Klasa umożliwia 'transparentne' rzutowanie typów prostych, np pozwala
 * traktować tablicę {@code long[]} jako tablicę {@code byte[]}. Konwersja jest
 * wykonywana natywnie przez system hostujący (a nie przez VM!). Przekłada się
 * to przede wszystkim na względnie większą wydajność kosztem 
 * nieprzenośności kodu ze względu na endianess.</p>
 * </p>
 * <p>
 * Klasa nie może być tworzona bezpośrednio, należy użyć jednej z metod {@link #getInstance} 
 * </p>
 * @see net.ranides.assira.collection.ArrayUtils#wrap(Object) 
 * @see net.ranides.assira.collection.ArrayUtils#wrap(Class, Object) 
 * @author ranides
 */
public abstract class ArrayInspector {
    
    /**
     * Wewnętrzne API udostępniane przez wirtualną maszynę Sun/Oracle pozwalające
     * m.in. na bezpośredni dostęp do pamięci.
     */
    private static final sun.misc.Unsafe API = PrivilegedActions.$getStaticField("sun.misc.Unsafe", "theUnsafe");
    
    public static ArrayInspector forObject(Object array) {
        return forType(array.getClass());
    }
    
    public static ArrayInspector forType(Class<?> type) {
        return forType(type, type);
    }

    /**
     * Tworzy obiekt {@code ArrayInspector} odczytujący danez tablicy o klasie
     * {@code sourceType} i prezentujący jako tablicę typu {@code targetType}.
     * <p>Uwaga! Argumenty mają definiować klasę tablicy, a nie jej komponentu</p>
     * @param target typ, na który tablica będzie rzutowana
     * @param source realny typ tablicy przechowującej dane
     * @return
     */
    @SuppressWarnings("PMD")
    public static ArrayInspector forType(Class<?> target, Class<?> source) {
        Class<?> ctarget = asComponentType(target);
        Class<?> csource = asComponentType(source);
        Class<?> asource = asArrayType(source);
        
        if(byte.class.equals(ctarget)) {
            return ByteArrayNativeAccessor.getInstance(asource);
        } 
        if(short.class.equals(ctarget)) {
            return ShortArrayNativeAccessor.getInstance(asource);
        } 
        if(int.class.equals(ctarget)) {
            return IntArrayNativeAccessor.getInstance(asource);
        } 
        if(long.class.equals(ctarget)) {
            return LongArrayNativeAccessor.getInstance(asource);
        } 
        if(char.class.equals(ctarget)) {
            return CharArrayNativeAccessor.getInstance(asource);
        } 
        if(float.class.equals(ctarget)) {
            return FloatArrayNativeAccessor.getInstance(asource);
        } 
        if(double.class.equals(ctarget)) {
            return DoubleArrayNativeAccessor.getInstance(asource);
        } 
        if(boolean.class.equals(ctarget)) {
            return BooleanArrayNativeAccessor.getInstance(asource);
        } 
        return ObjectArrayNativeAccessor.forComponent(csource);
    }
    
    @SuppressWarnings("PMD")
    public static Class<?> asComponentType(Class<?> type) {
        if( !type.isArray() ) {
            return type;
        } else {
            return type.getComponentType();
        }
    }
    
    @SuppressWarnings("PMD")
    public static Class<?> asArrayType(Class<?> type) {
        return asArrayType(type, true, 1);
    }
    
    @SuppressWarnings("PMD")
    public static Class<?> asArrayType(Class<?> type, int dimensions) {
        return asArrayType(type, false, dimensions);
    }
    
    @SuppressWarnings("PMD")
    private static Class<?> asArrayType(Class<?> type, boolean raw, int dimensions) {
        Class<?> ctype = type;
        for(int i=1; i<dimensions && ctype.isArray(); i++) {
            ctype = ctype.getComponentType();
        }
        if( ctype.isArray() ) {
            return ctype;
        }
        if(byte.class.equals(ctype)) {
            return byte[].class;
        } 
        if(short.class.equals(ctype)) {
            return short[].class;
        } 
        if(int.class.equals(ctype)) {
            return int[].class;
        } 
        if(long.class.equals(ctype)) {
            return long[].class;
        } 
        if(char.class.equals(ctype)) {
            return char[].class;
        } 
        if(float.class.equals(ctype)) {
            return float[].class;
        } 
        if(double.class.equals(ctype)) {
            return double[].class;
        } 
        if(boolean.class.equals(ctype)) {
            return boolean[].class;
        }
        if(raw) {
            return Object[].class;
        }
        return Array.newInstance(ctype, 0).getClass();
    }

/* ************************************************************************** */

    final long base;
    final int scale;

    private ArrayInspector(Class<?> clazz) {
        this.base = API.arrayBaseOffset(clazz);
        this.scale = API.arrayIndexScale(clazz);
    }

    public abstract Object get(Object object, int index);

    public abstract void put(Object object, int index, Object value);

    public abstract void putNull(Object array, int index);
    
    public abstract Object create(int size);
    
    public abstract int length(Object array);

    public Object change(Object object, int index, Object value) {
        Object prev = get(object, index);
        put(object, index, value);
        return prev;
    }

/* ************************************************************************** */

    static class ByteArrayNativeAccessor extends ArrayInspector {
        
        private static final int SCALE = API.arrayIndexScale(byte[].class);

        private static final ArrayInspector THIS = new ByteArrayNativeAccessor(byte[].class);

        ByteArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return byte[].class.equals(clazz) ? THIS : new ByteArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getByte(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putByte(object, base+index*SCALE, (Byte)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putByte(object, base+index*SCALE, (byte)0 );
        }
        @Override
        public Object create(int size) {
            return new byte[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class ShortArrayNativeAccessor extends ArrayInspector {
        
        private static final int SCALE = API.arrayIndexScale(short[].class);

        private static final ArrayInspector THIS = new ShortArrayNativeAccessor(short[].class);

        ShortArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return short[].class.equals(clazz) ? THIS : new ShortArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getShort(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putShort(object, base+index*SCALE, (Short)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putShort(object, base+index*SCALE, (short)0 );
        }
        @Override
        public Object create(int size) {
            return new short[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class IntArrayNativeAccessor extends ArrayInspector {
        
        private static final int SCALE = API.arrayIndexScale(int[].class);

        private static final ArrayInspector THIS = new IntArrayNativeAccessor(int[].class);

        IntArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return int[].class.equals(clazz) ? THIS : new IntArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getInt(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putInt(object, base+index*SCALE, (Integer)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putInt(object, base+index*SCALE, 0 );
        }
        @Override
        public Object create(int size) {
            return new int[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class LongArrayNativeAccessor extends ArrayInspector {
        
        private static final int SCALE = API.arrayIndexScale(long[].class);

        private static final ArrayInspector THIS = new LongArrayNativeAccessor(long[].class);

        LongArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return long[].class.equals(clazz) ? THIS : new LongArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getLong(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putLong(object, base+index*SCALE, (Long)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putLong(object, base+index*SCALE, 0L );
        }
        @Override
        public Object create(int size) {
            return new long[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class CharArrayNativeAccessor extends ArrayInspector {

        private static final int SCALE = API.arrayIndexScale(char[].class);
        
        private static final ArrayInspector THIS = new CharArrayNativeAccessor(char[].class);

        CharArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return char[].class.equals(clazz) ? THIS : new CharArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getChar(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putChar(object, base+index*SCALE, (Character)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putChar(object, base+index*SCALE, '\000' );
        }
        @Override
        public Object create(int size) {
            return new char[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class FloatArrayNativeAccessor extends ArrayInspector {
        
        private static final int SCALE = API.arrayIndexScale(float[].class);

        private static final ArrayInspector THIS = new FloatArrayNativeAccessor(float[].class);

        FloatArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return float[].class.equals(clazz) ? THIS : new FloatArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getFloat(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putFloat(object, base+index*SCALE, (Float)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putFloat(object, base+index*SCALE, .0f );
        }
        @Override
        public Object create(int size) {
            return new float[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class DoubleArrayNativeAccessor extends ArrayInspector {
        
        private static final int SCALE = API.arrayIndexScale(double[].class);

        private static final ArrayInspector THIS = new DoubleArrayNativeAccessor(double[].class);

        DoubleArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return double[].class.equals(clazz) ? THIS : new DoubleArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getDouble(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putDouble(object, base+index*SCALE, (Double)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putDouble(object, base+index*SCALE, .0 );
        }
        @Override
        public Object create(int size) {
            return new double[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class BooleanArrayNativeAccessor extends ArrayInspector {

        private static final int SCALE = API.arrayIndexScale(boolean[].class);
        
        private static final ArrayInspector THIS = new BooleanArrayNativeAccessor(boolean[].class);

        BooleanArrayNativeAccessor(Class<?> clazz) {
            super(clazz);
        }

        public static ArrayInspector getInstance(Class<?> clazz) {
            return boolean[].class.equals(clazz) ? THIS : new BooleanArrayNativeAccessor(clazz);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getBoolean(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            API.putBoolean(object, base+index*SCALE, (Boolean)value);
        }
        @Override
        public void putNull(Object object, int index) {
            API.putBoolean(object, base+index*SCALE, false );
        }
        @Override
        public Object create(int size) {
            return new boolean[size];
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

    static class ObjectArrayNativeAccessor extends ArrayInspector {

        private static final int SCALE = API.arrayIndexScale(Object[].class);
        
        private final Class<?> component;

        ObjectArrayNativeAccessor(Class<?> component) {
            super(Object[].class);
            this.component = component;
        }

        public static ArrayInspector forComponent(Class<?> component) {
            return new ObjectArrayNativeAccessor(component);
        }

        @Override
        public Object get(Object object, int index) {
            return API.getObject(object, base+index*SCALE);
        }
        @Override
        public void put(Object object, int index, Object value) {
            try {
                API.putObject(object, base+index*SCALE, component.cast(value));
            } catch(ClassCastException _e) {
                throw new ClassCastException("cannot cast " + value.getClass().getName() + " to " + component);
            }
        }
        @Override
        public void putNull(Object object, int index) {
            API.putObject(object, base+index*SCALE, null );
        }
        @Override
        public Object create(int size) {
            return Array.newInstance(component, size);
        }
        @Override
        public int length(Object array) {
            return (Array.getLength(array) * scale) / SCALE;
        }
    }

}
