/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript.typedarrays;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ExternalArrayData;
import org.mozilla.javascript.IdFunctionObject;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.RandomAccess;

/**
 * This class is the abstract parent for all of the various typed arrays. Each one
 * shows a view of a specific NativeArrayBuffer, and modifications here will affect the rest.
 */

public abstract class NativeTypedArrayView<T>
    extends NativeArrayBufferView
    implements List<T>, RandomAccess, ExternalArrayData
{
    /** The length, in elements, of the array */
    protected final int length;

    protected NativeTypedArrayView()
    {
        super();
        length = 0;
    }

    protected NativeTypedArrayView(NativeArrayBuffer ab, int off, int len, int byteLen)
    {
        super(ab, off, byteLen);
        length = len;
    }

    // Array properties implementation

    @Override
    public Object get(int index, Scriptable start)
    {
        return js_get(index);
    }

    @Override
    public boolean has(int index, Scriptable start)
    {
        return ((index > 0) && (index < length));
    }

    @Override
    public void put(int index, Scriptable start, Object val)
    {
        js_set(index, val);
    }

    @Override
    public void delete(int index)
    {
    }

    @Override
    public Object[] getIds()
    {
        Object[] ret = new Object[length];
        for (int i = 0; i < length; i++) {
            ret[i] = Integer.valueOf(i);
        }
        return ret;
    }

    // Actual functions

    protected boolean checkIndex(int index)
    {
       return ((index < 0) || (index >= length));
    }

    /**
     * Return the number of bytes represented by each element in the array. This can be useful
     * when wishing to manipulate the byte array directly from Java.
     */
    public abstract int getBytesPerElement();

    protected abstract NativeTypedArrayView construct(NativeArrayBuffer ab, int off, int len);
    protected abstract Object js_get(int index);
    protected abstract Object js_set(int index, Object c);
    protected abstract NativeTypedArrayView realThis(Scriptable thisObj, IdFunctionObject f);

    private NativeArrayBuffer makeArrayBuffer(Context cx, Scriptable scope, int length)
    {
        return (NativeArrayBuffer)cx.newObject(scope, NativeArrayBuffer.CLASS_NAME,
                                               new Object[] { length });
    }

    private NativeTypedArrayView js_constructor(Context cx, Scriptable scope, Object[] args)
    {
        if (!isArg(args, 0)) {
            return construct(NativeArrayBuffer.EMPTY_BUFFER, 0, 0);

        } else if ((args[0] instanceof Number) || (args[0] instanceof String)) {
            // Create a zeroed-out array of a certain length
            int length = ScriptRuntime.toInt32(args[0]);
            NativeArrayBuffer buffer = makeArrayBuffer(cx, scope, length * getBytesPerElement());
            return construct(buffer, 0, length);

        } else if (args[0] instanceof NativeTypedArrayView) {
            // Copy elements from the old array and convert them into our own
            NativeTypedArrayView src = (NativeTypedArrayView)args[0];
            NativeArrayBuffer na = makeArrayBuffer(cx, scope, src.length * getBytesPerElement());
            NativeTypedArrayView v = construct(na, 0, src.length);

            for (int i = 0; i < src.length; i++) {
                v.js_set(i, src.js_get(i));
            }
            return v;

        } else if (args[0] instanceof NativeArrayBuffer) {
            // Make a slice of an existing buffer, with shared storage
            NativeArrayBuffer na = (NativeArrayBuffer)args[0];
            int byteOff = isArg(args, 1) ? ScriptRuntime.toInt32(args[1]) : 0;

            int byteLen;
            if (isArg(args, 2)) {
                byteLen = ScriptRuntime.toInt32(args[2]) * getBytesPerElement();
            } else {
                byteLen = na.getLength() - byteOff;
            }

            if ((byteOff < 0) || (byteOff > na.buffer.length)) {
                throw ScriptRuntime.constructError("RangeError", "offset out of range");
            }
            if ((byteLen < 0) || ((byteOff + byteLen) > na.buffer.length)) {
                throw ScriptRuntime.constructError("RangeError", "length out of range");
            }
            if ((byteOff % getBytesPerElement()) != 0) {
                throw ScriptRuntime.constructError("RangeError", "offset must be a multiple of the byte size");
            }
            if ((byteLen % getBytesPerElement()) != 0) {
                throw ScriptRuntime.constructError("RangeError", "offset and buffer must be a multiple of the byte size");
            }

            return construct(na, byteOff, byteLen / getBytesPerElement());

        } else if (args[0] instanceof NativeArray) {
            // Copy elements of the array and convert them to the correct type
            List l = (List)args[0];
            NativeArrayBuffer na = makeArrayBuffer(cx, scope, l.size() * getBytesPerElement());
            NativeTypedArrayView v = construct(na, 0, l.size());
            int p = 0;
            for (Object o : l) {
                v.js_set(p, o);
                p++;
            }
            return v;

        } else {
            throw ScriptRuntime.constructError("Error", "invalid argument");
        }
    }

    private void setRange(NativeTypedArrayView v, int off)
    {
        if (off >= length) {
            throw ScriptRuntime.constructError("RangeError", "offset out of range");
        }

        if (v.length > (length - off)) {
            throw ScriptRuntime.constructError("RangeError", "source array too long");
        }

        if (v.arrayBuffer == arrayBuffer) {
            // Copy to temporary space first, as per spec, to avoid messing up overlapping copies
            Object[] tmp = new Object[v.length];
            for (int i = 0; i < v.length; i++) {
                tmp[i] = v.js_get(i);
            }
            for (int i = 0; i < v.length; i++) {
                js_set(i + off, tmp[i]);
            }
        } else {
            for (int i = 0; i < v.length; i++) {
                js_set(i + off, v.js_get(i));
            }
        }
    }

    private void setRange(NativeArray a, int off)
    {
        if (off > length) {
            throw ScriptRuntime.constructError("RangeError", "offset out of range");
        }
        if ((off + a.size()) > length) {
            throw ScriptRuntime.constructError("RangeError", "offset + length out of range");
        }

        int pos = off;
        for (Object val : a) {
            js_set(pos, val);
            pos++;
        }
    }

    private Object js_subarray(Context cx, Scriptable scope, int s, int e)
    {
        int start = (s < 0 ? length + s : s);
        int end = (e < 0 ? length + e : e);

        // Clamping behavior as described by the spec.
        start = Math.max(0, start);
        end = Math.min(length, end);
        int len = Math.max(0, (end - start));
        int byteOff = Math.min(start * getBytesPerElement(), arrayBuffer.getLength());

        return
            cx.newObject(scope, getClassName(),
                         new Object[]{arrayBuffer, byteOff, len});
    }

    // Dispatcher

    @Override
    public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope,
                             Scriptable thisObj, Object[] args)
    {
        if (!f.hasTag(getClassName())) {
            return super.execIdCall(f, cx, scope, thisObj, args);
        }
        int id = f.methodId();
        switch (id) {
        case Id_constructor:
            return js_constructor(cx, scope, args);

        case Id_get:
            if (args.length > 0) {
                return realThis(thisObj, f).js_get(ScriptRuntime.toInt32(args[0]));
            } else {
                throw ScriptRuntime.constructError("Error", "invalid arguments");
            }

        case Id_set:
            if (args.length > 0) {
                NativeTypedArrayView self = realThis(thisObj, f);
                if (args[0] instanceof NativeTypedArrayView) {
                    int offset = isArg(args, 1) ? ScriptRuntime.toInt32(args[1]) : 0;
                    self.setRange((NativeTypedArrayView)args[0], offset);
                    return Undefined.instance;
                }
                if (args[0] instanceof NativeArray) {
                    int offset = isArg(args, 1) ? ScriptRuntime.toInt32(args[1]) : 0;
                    self.setRange((NativeArray)args[0], offset);
                    return Undefined.instance;
                }
                if (args[0] instanceof Scriptable) {
                    // Tests show that we need to ignore a non-array object
                    return Undefined.instance;
                }
                if (isArg(args, 2)) {
                    return self.js_set(ScriptRuntime.toInt32(args[0]), args[1]);
                }
            }
            throw ScriptRuntime.constructError("Error", "invalid arguments");

        case Id_subarray:
            if (args.length > 0) {
                NativeTypedArrayView self = realThis(thisObj, f);
                int start = ScriptRuntime.toInt32(args[0]);
                int end = isArg(args, 1) ? ScriptRuntime.toInt32(args[1]) : self.length;
                return self.js_subarray(cx, scope, start, end);
            } else {
                throw ScriptRuntime.constructError("Error", "invalid arguments");
            }
        }
        throw new IllegalArgumentException(String.valueOf(id));
    }

    @Override
    protected void initPrototypeId(int id)
    {
        String s;
        int arity;
        switch (id) {
        case Id_constructor:        arity = 1; s = "constructor"; break;
        case Id_get:                arity = 1; s = "get"; break;
        case Id_set:                arity = 2; s = "set"; break;
        case Id_subarray:           arity = 2; s = "subarray"; break;
        default: throw new IllegalArgumentException(String.valueOf(id));
        }
        initPrototypeMethod(getClassName(), id, s, arity);
    }

    // #string_id_map#

    @Override
    protected int findPrototypeId(String s)
    {
        int id;
// #generated# Last update: 2014-12-04 18:21:01 PST
        L0: { id = 0; String X = null; int c;
            int s_length = s.length();
            if (s_length==3) {
                c=s.charAt(0);
                if (c=='g') { if (s.charAt(2)=='t' && s.charAt(1)=='e') {id=Id_get; break L0;} }
                else if (c=='s') { if (s.charAt(2)=='t' && s.charAt(1)=='e') {id=Id_set; break L0;} }
            }
            else if (s_length==8) { X="subarray";id=Id_subarray; }
            else if (s_length==11) { X="constructor";id=Id_constructor; }
            if (X!=null && X!=s && !X.equals(s)) id = 0;
            break L0;
        }
// #/generated#
        return id;
    }

    // Table of all functions
    private static final int
        Id_constructor          = 1,
        Id_get                  = 2,
        Id_set                  = 3,
        Id_subarray             = 4;

    protected static final int
        MAX_PROTOTYPE_ID        = Id_subarray;

// #/string_id_map#

    // Constructor properties

    @Override
    protected void fillConstructorProperties(IdFunctionObject ctor)
    {
        ctor.put("BYTES_PER_ELEMENT", ctor, ScriptRuntime.wrapInt(getBytesPerElement()));
    }

    // Property dispatcher

    @Override
    protected int getMaxInstanceId()
    {
        return MAX_INSTANCE_ID;
    }

    @Override
    protected String getInstanceIdName(int id)
    {
        switch (id) {
        case Id_length: return "length";
        case Id_BYTES_PER_ELEMENT: return "BYTES_PER_ELEMENT";
        default: return super.getInstanceIdName(id);
        }
    }

    @Override
    protected Object getInstanceIdValue(int id)
    {
        switch (id) {
        case Id_length:
            return ScriptRuntime.wrapInt(length);
        case Id_BYTES_PER_ELEMENT:
            return ScriptRuntime.wrapInt(getBytesPerElement());
        default:
            return super.getInstanceIdValue(id);
        }
    }

// #string_id_map#

    @Override
    protected int findInstanceIdInfo(String s)
    {
        int id;
// #generated# Last update: 2014-12-08 17:33:28 PST
        L0: { id = 0; String X = null;
            int s_length = s.length();
            if (s_length==6) { X="length";id=Id_length; }
            else if (s_length==17) { X="BYTES_PER_ELEMENT";id=Id_BYTES_PER_ELEMENT; }
            if (X!=null && X!=s && !X.equals(s)) id = 0;
            break L0;
        }
// #/generated#
        if (id == 0) {
            return super.findInstanceIdInfo(s);
        }
        return instanceIdInfo(READONLY | PERMANENT, id);
    }

    /*
     * These must not conflict with ids in the parent since we delegate there for property dispatching.
     */
    private static final int
        Id_length               = 10,
        Id_BYTES_PER_ELEMENT    = 11,
        MAX_INSTANCE_ID         = Id_BYTES_PER_ELEMENT;

// #/string_id_map#

    // External Array implementation

    @Override
    public Object getArrayElement(int index)
    {
        return js_get(index);
    }

    @Override
    public void setArrayElement(int index, Object value)
    {
        js_set(index, value);
    }

    @Override
    public int getArrayLength() {
        return length;
    }

    // Abstract List implementation

    @SuppressWarnings("unused")
    public int size()
    {
        return length;
    }

    @SuppressWarnings("unused")
    public boolean isEmpty()
    {
        return (length == 0);
    }

    @SuppressWarnings("unused")
    public boolean contains(Object o)
    {
        return (indexOf(o) >= 0);
    }

    @SuppressWarnings("unused")
    public boolean containsAll(Collection<?> objects)
    {
        for (Object o : objects) {
            if (!contains(o)) {
                return false;
            }
        }
        return true;
    }

    @SuppressWarnings("unused")
    public int indexOf(Object o)
    {
        for (int i = 0; i < length; i++) {
            if (o.equals(js_get(i))) {
                return i;
            }
        }
        return -1;
    }

    @SuppressWarnings("unused")
    public int lastIndexOf(Object o)
    {
        for (int i = length - 1; i >= 0; i--) {
            if (o.equals(js_get(i))) {
                return i;
            }
        }
        return -1;
    }

    @SuppressWarnings("unused")
    public Object[] toArray()
    {
        Object[] a = new Object[length];
        for (int i = 0; i < length; i++) {
            a[i] = js_get(i);
        }
        return a;
    }

    @SuppressWarnings("unused")
    public <U> U[] toArray(U[] ts)
    {
        U[] a;

        if (ts.length >= length) {
            a = ts;
        } else {
            a = (U[])Array.newInstance(ts.getClass().getComponentType(), length);
        }

        for (int i = 0; i < length; i++) {
            try {
                a[i] = (U)js_get(i);
            } catch (ClassCastException cce) {
                throw new ArrayStoreException();
            }
        }
        return a;
    }

    @Override
    public boolean equals(Object o)
    {
        try {
            NativeTypedArrayView<T> v = (NativeTypedArrayView<T>)o;
            if (length != v.length) {
                return false;
            }
            for (int i = 0; i < length; i++) {
                if (!js_get(i).equals(v.js_get(i))) {
                    return false;
                }
            }
            return true;
        } catch (ClassCastException cce) {
            return false;
        }
    }

    @Override
    public int hashCode()
    {
        int hc = 0;
        for (int i = 0; i < length; i++) {
            hc += js_get(i).hashCode();
        }
        return 0;
    }

    @SuppressWarnings("unused")
    public Iterator<T> iterator()
    {
        return new NativeTypedArrayIterator<T>(this, 0);
    }

    @SuppressWarnings("unused")
    public ListIterator<T> listIterator()
    {
        return new NativeTypedArrayIterator<T>(this, 0);
    }

    @SuppressWarnings("unused")
    public ListIterator<T> listIterator(int start)
    {
        if (checkIndex(start)) {
            throw new IndexOutOfBoundsException();
        }
        return new NativeTypedArrayIterator<T>(this, start);
    }

    @SuppressWarnings("unused")
    public List<T> subList(int i, int i2)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public boolean add(T aByte)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public void add(int i, T aByte)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public boolean addAll(Collection<? extends T> bytes)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public boolean addAll(int i, Collection<? extends T> bytes)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public void clear()
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public T remove(int i)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public boolean remove(Object o)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public boolean removeAll(Collection<?> objects)
    {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unused")
    public boolean retainAll(Collection<?> objects)
    {
        throw new UnsupportedOperationException();
    }
}
