/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdi.internal;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.jdi.internal.jdwp.JdwpCommandPacket;
import org.eclipse.jdi.internal.jdwp.JdwpID;
import org.eclipse.jdi.internal.jdwp.JdwpObjectID;
import org.eclipse.jdi.internal.jdwp.JdwpReplyPacket;

import com.sun.jdi.ArrayReference;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.InternalException;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.Mirror;
import com.sun.jdi.ObjectCollectedException;
import com.sun.jdi.Type;
import com.sun.jdi.Value;

/**
 * this class implements the corresponding interfaces declared by the JDI
 * specification. See the com.sun.jdi package for more information.
 *
 */
public class ArrayReferenceImpl extends ObjectReferenceImpl implements
		ArrayReference {
	/** JDWP Tag. */
	public static final byte tag = JdwpID.ARRAY_TAG;

	private int fLength = -1;

	/**
	 * Creates new ArrayReferenceImpl.
	 * @param vmImpl the VM
	 * @param objectID the object ID
	 */
	public ArrayReferenceImpl(VirtualMachineImpl vmImpl, JdwpObjectID objectID) {
		super("ArrayReference", vmImpl, objectID); //$NON-NLS-1$
	}

	/**
	 * @returns tag.
	 */
	@Override
	public byte getTag() {
		return tag;
	}

	/**
	 * Returns the {@link Value} from the given index
	 * @param index the index
	 * @return the {@link Value} at the given index
	 * @throws IndexOutOfBoundsException if the index is outside the bounds of the array
	 * @returns Returns an array component value.
	 */
	@Override
	public Value getValue(int index) throws IndexOutOfBoundsException {
		return getValues(index, 1).get(0);
	}

	/**
	 * @return all of the components in this array.
	 */
	@Override
	public List<Value> getValues() {
		return getValues(0, -1);
	}

	/**
	 * Gets all of the values starting at firstIndex and ending at firstIndex+length
	 *
	 * @param firstIndex the start
	 * @param length the  number of values to return
	 * @return the list of {@link Value}s
	 * @throws IndexOutOfBoundsException if the index is outside the bounds of the array
	 * @returns Returns a range of array components.
	 */
	@Override
	public List<Value> getValues(int firstIndex, int length)
			throws IndexOutOfBoundsException {

		int arrayLength = length();

		if (firstIndex < 0 || firstIndex >= arrayLength) {
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Invalid_index_1);
		}

		if (length == -1) {
			// length == -1 means all elements to the end.
			length = arrayLength - firstIndex;
		} else if (length < -1) {
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Invalid_number_of_value_to_get_from_array_1);
		} else if (firstIndex + length > arrayLength) {
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Attempted_to_get_more_values_from_array_than_length_of_array_2);
		}

		// Note that this information should not be cached.
		initJdwpRequest();
		try {
			ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
			DataOutputStream outData = new DataOutputStream(outBytes);
			write(this, outData); // arrayObject
			writeInt(firstIndex, "firstIndex", outData); //$NON-NLS-1$
			writeInt(length, "length", outData); //$NON-NLS-1$

			JdwpReplyPacket replyPacket = requestVM(
					JdwpCommandPacket.AR_GET_VALUES, outBytes);
			switch (replyPacket.errorCode()) {
			case JdwpReplyPacket.INVALID_INDEX:
				throw new IndexOutOfBoundsException(
						JDIMessages.ArrayReferenceImpl_Invalid_index_of_array_reference_given_1);
			}
			defaultReplyErrorHandler(replyPacket.errorCode());

			DataInputStream replyData = replyPacket.dataInStream();

			/*
			 * NOTE: The JDWP documentation is not clear on this: it turns out
			 * that the following is received from the VM: - type tag; - length
			 * of array; - values of elements.
			 */

			int type = readByte("type", JdwpID.tagMap(), replyData); //$NON-NLS-1$
			int readLength = readInt("length", replyData); //$NON-NLS-1$
			// See also ValueImpl.
			switch (type) {
			// Multidimensional array.
			case ArrayReferenceImpl.tag:
				// Object references.
			case ClassLoaderReferenceImpl.tag:
			case ClassObjectReferenceImpl.tag:
			case StringReferenceImpl.tag:
			case ObjectReferenceImpl.tag:
			case ThreadGroupReferenceImpl.tag:
			case ThreadReferenceImpl.tag:
				return readObjectSequence(readLength, replyData);

				// Primitive type.
			case BooleanValueImpl.tag:
			case ByteValueImpl.tag:
			case CharValueImpl.tag:
			case DoubleValueImpl.tag:
			case FloatValueImpl.tag:
			case IntegerValueImpl.tag:
			case LongValueImpl.tag:
			case ShortValueImpl.tag:
				return readPrimitiveSequence(readLength, type, replyData);

			case VoidValueImpl.tag:
			case 0:
			default:
				throw new InternalException(
						JDIMessages.ArrayReferenceImpl_Invalid_ArrayReference_Value_tag_encountered___2
								+ type);
			}
		} catch (IOException e) {
			defaultIOExceptionHandler(e);
			return null;
		} finally {
			handledJdwpRequest();
		}
	}

	/**
	 * Reads the given length of objects from the given stream
	 * @param length the number of objects to read
	 * @param in the stream to read from
	 * @return the {@link List} of {@link ValueImpl}s
	 * @throws IOException if the reading fails
	 * @returns Returns sequence of object reference values.
	 */
	private List<Value> readObjectSequence(int length, DataInputStream in)
			throws IOException {
		List<Value> elements = new ArrayList<>(length);
		for (int i = 0; i < length; i++) {
			ValueImpl value = ObjectReferenceImpl
					.readObjectRefWithTag(this, in);
			elements.add(value);
		}
		return elements;
	}

	/**
	 * @param length
	 *            the number of primitives to read
	 * @param type
	 *            the type
	 * @param in
	 *            the input stream
	 * @return Returns sequence of values of primitive type.
	 * @throws IOException
	 *             if reading from the stream encounters a problem
	 */
	private List<Value> readPrimitiveSequence(int length, int type, DataInputStream in)
			throws IOException {
		List<Value> elements = new ArrayList<>(length);
		for (int i = 0; i < length; i++) {
			ValueImpl value = ValueImpl.readWithoutTag(this, type, in);
			elements.add(value);
		}
		return elements;
	}

	/**
	 * @return Returns the number of components in this array.
	 */
	@Override
	public int length() {
		if (fLength == -1) {
			initJdwpRequest();
			try {
				JdwpReplyPacket replyPacket = requestVM(
						JdwpCommandPacket.AR_LENGTH, this);
				defaultReplyErrorHandler(replyPacket.errorCode());
				DataInputStream replyData = replyPacket.dataInStream();
				fLength = readInt("length", replyData); //$NON-NLS-1$
			} catch (IOException e) {
				defaultIOExceptionHandler(e);
				return 0;
			} finally {
				handledJdwpRequest();
			}
		}
		return fLength;
	}

	/**
	 * Replaces an array component with another value.
	 *
	 * @param index
	 *            the index to set the value in
	 * @param value
	 *            the new value
	 * @throws InvalidTypeException
	 *             thrown if the types of the replacements do not match the
	 *             underlying type of the {@link ArrayReference}
	 * @throws ClassNotLoadedException
	 *             thrown if the class type for the {@link ArrayReference} is
	 *             not loaded or has been GC'd
	 * @see #setValues(int, List, int, int)
	 */
	@Override
	public void setValue(int index, Value value) throws InvalidTypeException,
			ClassNotLoadedException {
		ArrayList<Value> list = new ArrayList<>(1);
		list.add(value);
		setValues(index, list, 0, 1);
	}

	/**
	 * Replaces all array components with other values.
	 *
	 * @param values
	 *            the new values to set in the array
	 * @throws InvalidTypeException
	 *             thrown if the types of the replacements do not match the
	 *             underlying type of the {@link ArrayReference}
	 * @throws ClassNotLoadedException
	 *             thrown if the class type for the {@link ArrayReference} is
	 *             not loaded or has been GC'd
	 * @see #setValues(int, List, int, int)
	 */
	@Override
	public void setValues(List<? extends Value> values) throws InvalidTypeException,
			ClassNotLoadedException {
		setValues(0, values, 0, -1);
	}

	/**
	 * Replaces a range of array components with other values.
	 *
	 * @param index
	 *            offset in this array to start replacing values at
	 * @param values
	 *            replacement values
	 * @param srcIndex
	 *            the first offset where values are copied from the given
	 *            replacement values
	 * @param length
	 *            the number of values to replace in this array
	 * @throws InvalidTypeException
	 *             thrown if the types of the replacements do not match the
	 *             underlying type of the {@link ArrayReference}
	 * @throws ClassNotLoadedException
	 *             thrown if the class type for the {@link ArrayReference} is
	 *             not loaded or has been GC'd
	 */
	@Override
	public void setValues(int index, List<? extends Value> values, int srcIndex, int length)
			throws InvalidTypeException, ClassNotLoadedException {
		if (values == null || values.size() == 0) {
			// trying to set nothing should do no work
			return;
		}
		int valuesSize = values.size();
		int arrayLength = length();

		if (index < 0 || index >= arrayLength) {
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Invalid_index_1);
		}
		if (srcIndex < 0 || srcIndex >= valuesSize) {
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Invalid_srcIndex_2);
		}

		if (length < -1) {
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Invalid_number_of_value_to_set_in_array_3);
		} else if (length == -1) {
			// length == -1 indicates as much values as possible.
			length = arrayLength - index;
			int lengthTmp = valuesSize - srcIndex;
			if (lengthTmp < length) {
				length = lengthTmp;
			}
		} else if (index + length > arrayLength) {
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Attempted_to_set_more_values_in_array_than_length_of_array_3);
		} else if (srcIndex + length > valuesSize) {
			// Check if enough values are given.
			throw new IndexOutOfBoundsException(
					JDIMessages.ArrayReferenceImpl_Attempted_to_set_more_values_in_array_than_given_4);
		}

		// check and convert the values if needed.
		List<Value> checkedValues = checkValues(
				values.subList(srcIndex, srcIndex + length),
				((ArrayTypeImpl) referenceType()).componentType());

		// Note that this information should not be cached.
		initJdwpRequest();
		try {
			ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
			DataOutputStream outData = new DataOutputStream(outBytes);
			write(this, outData);
			writeInt(index, "index", outData); //$NON-NLS-1$
			writeInt(length, "length", outData); //$NON-NLS-1$
			Iterator<Value> iterValues = checkedValues.iterator();
			while (iterValues.hasNext()) {
				ValueImpl value = (ValueImpl) iterValues.next();
				if (value != null) {
					value.write(this, outData);
				} else {
					ValueImpl.writeNull(this, outData);
				}
			}

			JdwpReplyPacket replyPacket = requestVM(
					JdwpCommandPacket.AR_SET_VALUES, outBytes);
			switch (replyPacket.errorCode()) {
			case JdwpReplyPacket.TYPE_MISMATCH:
				throw new InvalidTypeException();
			case JdwpReplyPacket.INVALID_CLASS:
				throw new ClassNotLoadedException(type().name());
			}
			defaultReplyErrorHandler(replyPacket.errorCode());
		} catch (IOException e) {
			defaultIOExceptionHandler(e);
		} finally {
			handledJdwpRequest();
		}
	}

	/**
	 * Check the type and the VM of the values. If the given type is a primitive
	 * type, the values may be converted to match this type.
	 *
	 * @param values
	 *            the value(s) to check
	 * @param type
	 *            the type to compare the values to
	 * @return the list of values converted to the given type
	 * @throws InvalidTypeException
	 *             if the underlying type of an object in the list is not
	 *             compatible
	 *
	 * @see ValueImpl#checkValue(Value, Type, VirtualMachineImpl)
	 */
	private List<Value> checkValues(List<? extends Value> values, Type type)
			throws InvalidTypeException {
		List<Value> checkedValues = new ArrayList<>(values.size());
		Iterator<? extends Value> iterValues = values.iterator();
		while (iterValues.hasNext()) {
			checkedValues.add(ValueImpl.checkValue(iterValues.next(),
					type, virtualMachineImpl()));
		}
		return checkedValues;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdi.internal.ObjectReferenceImpl#toString()
	 */
	@Override
	public String toString() {
		try {
			StringBuilder buf = new StringBuilder(type().name());
			// Insert length of string between (last) square braces.
			buf.insert(buf.length() - 1, length());
			// Append space and idString.
			buf.append(' ');
			buf.append(idString());
			return buf.toString();
		} catch (ObjectCollectedException e) {
			return JDIMessages.ArrayReferenceImpl__Garbage_Collected__ArrayReference_5
					+ "[" + length() + "] " + idString(); //$NON-NLS-1$ //$NON-NLS-2$
		} catch (Exception e) {
			return fDescription;
		}
	}

	/**
	 * Reads JDWP representation and returns new instance.
	 *
	 * @param target
	 *            the target {@link Mirror} object
	 * @param in
	 *            the input stream to read from
	 * @return Reads JDWP representation and returns new instance.
	 * @throws IOException
	 *             if there is a problem reading from the stream
	 */
	public static ArrayReferenceImpl read(MirrorImpl target, DataInputStream in)
			throws IOException {
		VirtualMachineImpl vmImpl = target.virtualMachineImpl();
		JdwpObjectID ID = new JdwpObjectID(vmImpl);
		ID.read(in);
		if (target.fVerboseWriter != null) {
			target.fVerboseWriter.println("arrayReference", ID.value()); //$NON-NLS-1$
		}

		if (ID.isNull()) {
			return null;
		}

		ArrayReferenceImpl mirror = new ArrayReferenceImpl(vmImpl, ID);
		return mirror;
	}
}
