package org.jruby.java.util;

import java.lang.reflect.Array;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.exceptions.RaiseException;
import org.jruby.java.proxies.ArrayJavaProxy;
import org.jruby.javasupport.JavaArray;
import org.jruby.javasupport.JavaClass;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

/**
 * A collection of utilities for manipulating Java arrays.
 */
public class ArrayUtils {

    private ArrayUtils() { /* no instances */ }

    public static IRubyObject arefDirect(Ruby runtime, Object array, JavaUtil.JavaConverter javaConverter, int index) {
        try {
            return JavaUtil.convertJavaArrayElementToRuby(runtime, javaConverter, array, index);
        }
        catch (IndexOutOfBoundsException e) { throw mapIndexOutOfBoundsException(runtime, array, index); }
    }

    public static IRubyObject concatArraysDirect(ThreadContext context, Object original, Object additional) {
        int oldLength = Array.getLength(original);
        int addLength = Array.getLength(additional);

        ArrayJavaProxy proxy = newProxiedArray(context.runtime, original.getClass().getComponentType(), oldLength + addLength);
        Object newArray = proxy.getObject();

        System.arraycopy(original, 0, newArray, 0, oldLength);
        System.arraycopy(additional, 0, newArray, oldLength, addLength);

        return proxy;
    }

    public static ArrayJavaProxy newProxiedArray(Ruby runtime, Class<?> componentType, int size) {
        return newProxiedArray(runtime, componentType, JavaUtil.getJavaConverter(componentType), size);
    }

    public static ArrayJavaProxy newProxiedArray(Ruby runtime, Class<?> componentType, JavaUtil.JavaConverter converter, int size) {
        final Object array = Array.newInstance(componentType, size);
        RubyClass proxyClass = JavaClass.get(runtime, array.getClass()).getProxyClass();
        return new ArrayJavaProxy(runtime, proxyClass, array, converter);
    }

    public static IRubyObject emptyJavaArrayDirect(ThreadContext context, Class componentType) {
        return newProxiedArray(context.runtime, componentType, 0);
    }

    public static IRubyObject javaArraySubarrayDirect(ThreadContext context, Object fromArray, int index, int size) {
        int actualLength = Array.getLength(fromArray);
        if (index >= actualLength) return context.nil;

        if (index + size > actualLength) {
            size = actualLength - index;
        }

        ArrayJavaProxy proxy = ArrayUtils.newProxiedArray(context.runtime, fromArray.getClass().getComponentType(), size);
        Object newArray = proxy.getObject();
        System.arraycopy(fromArray, index, newArray, 0, size);

        return proxy;
    }

    public static IRubyObject concatArraysDirect(ThreadContext context, Object original, IRubyObject additional) {
        final Ruby runtime = context.runtime;
        final int oldLength = Array.getLength(original);
        final int addLength = RubyFixnum.fix2int(Helpers.invoke(context, additional, "length"));

        ArrayJavaProxy proxy = ArrayUtils.newProxiedArray(runtime, original.getClass().getComponentType(), oldLength + addLength);

        System.arraycopy(original, 0, proxy.getObject(), 0, oldLength);

        for (int i = 0; i < addLength; i++) {
            IRubyObject val = Helpers.invoke(context, additional, "[]", runtime.newFixnum(i));
            proxy.setValue(runtime, oldLength + i, val); // [ oldLen + i ] = val
        }

        return proxy;
    }

    public static IRubyObject asetDirect(Ruby runtime, Object array, JavaUtil.JavaConverter javaConverter, int index, IRubyObject value) {
        try {
            javaConverter.set(runtime, array, index, value);
        }
        catch (IndexOutOfBoundsException e) { throw mapIndexOutOfBoundsException(runtime, array, index); }
        catch (ArrayStoreException e) { throw mapArrayStoreException(runtime, array, value.getClass()); }
        catch (IllegalArgumentException e) { throw mapIllegalArgumentException(runtime, array, value.getClass()); }
        return value;
    }

    public static void setWithExceptionHandlingDirect(Ruby runtime, Object array, int index, Object javaValue) {
        try {
            Array.set(array, index, javaValue);
        }
        catch (IndexOutOfBoundsException e) { throw mapIndexOutOfBoundsException(runtime, array, index); }
        catch (ArrayStoreException e) { throw mapArrayStoreException(runtime, array, javaValue.getClass()); }
        catch (IllegalArgumentException e) { throw mapIllegalArgumentException(runtime, array, javaValue.getClass()); }
    }

    private static RaiseException mapIndexOutOfBoundsException(final Ruby runtime, final Object array, int index) {
        return runtime.newArgumentError("index out of bounds for java array (" +
                index + " for length " + Array.getLength(array) + ')');
    }

    private static RaiseException mapArrayStoreException(final Ruby runtime, final Object array, final Class<?> type) {
        return runtime.newTypeError("wrong element type " + type.getName() + " (array contains " +
                array.getClass().getComponentType().getName() + ')');
    }

    private static RaiseException mapIllegalArgumentException(final Ruby runtime, final Object array, final Class<?> type) {
        return runtime.newArgumentError("wrong element type " + type.getName() + " (array contains " +
                array.getClass().getComponentType().getName() + ')');
    }

    public static void copyDataToJavaArrayDirect(ThreadContext context,
        final RubyArray rubyArray, final Object javaArray) {
        Class targetType = javaArray.getClass().getComponentType();

        // 'premature' optimizations as reflected Array.set is (still) noticeably slower
        if ( ! targetType.isPrimitive() ) {
            copyDataToJavaArrayDirect(targetType, rubyArray, (Object[]) javaArray); return;
        }
        else if ( Integer.TYPE == targetType ) {
            copyDataToJavaArrayDirect(targetType, rubyArray, (int[]) javaArray); return;
        }
        else if ( Long.TYPE == targetType ) {
            copyDataToJavaArrayDirect(targetType, rubyArray, (long[]) javaArray); return;
        }
        else if ( Byte.TYPE == targetType ) {
            copyDataToJavaArrayDirect(targetType, rubyArray, (byte[]) javaArray); return;
        }

        int length = rubyArray.getLength();

        final int javaLength = Array.getLength(javaArray);
        if ( javaLength < length ) length = javaLength;

        for ( int i = 0; i < length; i++ ) {
            Array.set(javaArray, i, rubyArray.eltInternal(i).toJava(targetType));
        }
    }

    //public static void copyDataToJavaArrayDirect(ThreadContext context,
    //    final RubyArray rubyArray, final Object[] javaArray) {
    //    copyDataToJavaArrayDirect(javaArray.getClass().getComponentType(), rubyArray, javaArray)
    //}

    private static void copyDataToJavaArrayDirect(final Class<?> targetType,
        final RubyArray rubyArray, final Object[] javaArray) {
        int length = rubyArray.getLength();

        final int javaLength = javaArray.length;
        if ( javaLength < length ) length = javaLength;

        for ( int i = 0; i < length; i++ ) {
            javaArray[i] = rubyArray.eltInternal(i).toJava(targetType);
        }
    }

    private static void copyDataToJavaArrayDirect(final Class<?> targetType,
        final RubyArray rubyArray, final int[] javaArray) {
        int length = rubyArray.getLength();

        final int javaLength = javaArray.length;
        if ( javaLength < length ) length = javaLength;

        for ( int i = 0; i < length; i++ ) {
            javaArray[i] = (Integer) rubyArray.eltInternal(i).toJava(targetType);
        }
    }

    private static void copyDataToJavaArrayDirect(final Class<?> targetType,
        final RubyArray rubyArray, final long[] javaArray) {
        int length = rubyArray.getLength();

        final int javaLength = javaArray.length;
        if ( javaLength < length ) length = javaLength;

        for ( int i = 0; i < length; i++ ) {
            javaArray[i] = (Long) rubyArray.eltInternal(i).toJava(targetType);
        }
    }

    private static void copyDataToJavaArrayDirect(final Class<?> targetType,
        final RubyArray rubyArray, final byte[] javaArray) {
        int length = rubyArray.getLength();

        final int javaLength = javaArray.length;
        if ( javaLength < length ) length = javaLength;

        for ( int i = 0; i < length; i++ ) {
            javaArray[i] = (Byte) rubyArray.eltInternal(i).toJava(targetType);
        }
    }

    public static void copyDataToJavaArray(
            ThreadContext context, RubyArray rubyArray, int src, JavaArray javaArray, int dest, int length) {
        Class targetType = javaArray.getComponentType();

        int destLength = (int)javaArray.length().getLongValue();
        int srcLength = rubyArray.getLength();

        for (int i = 0; src + i < srcLength && dest + i < destLength && i < length; i++) {
            javaArray.setWithExceptionHandling(dest + i, rubyArray.entry(src + i).toJava(targetType));
        }
    }
}
