/*******************************************************************************
 * 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
 *     Jesper Steen Møller <jesper@selskabet.org> - Bug 430839
 *******************************************************************************/
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.JdwpClassID;
import org.eclipse.jdi.internal.jdwp.JdwpClassObjectID;
import org.eclipse.jdi.internal.jdwp.JdwpCommandPacket;
import org.eclipse.jdi.internal.jdwp.JdwpID;
import org.eclipse.jdi.internal.jdwp.JdwpReplyPacket;

import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.ClassNotPreparedException;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.InvocationException;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
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 ClassTypeImpl extends ReferenceTypeImpl implements ClassType {

	/** JDWP Tag. */
	public static final byte typeTag = JdwpID.TYPE_TAG_CLASS;

	/** The following are the stored results of JDWP calls. */
	private ClassTypeImpl fSuperclass = null;

	/**
	 * Creates new ClassTypeImpl.
	 */
	public ClassTypeImpl(VirtualMachineImpl vmImpl, JdwpClassID classID) {
		super("ClassType", vmImpl, classID); //$NON-NLS-1$
	}

	/**
	 * Creates new ClassTypeImpl.
	 */
	public ClassTypeImpl(VirtualMachineImpl vmImpl, JdwpClassID classID,
			String signature, String genericSignature) {
		super("ClassType", vmImpl, classID, signature, genericSignature); //$NON-NLS-1$
	}

	/**
	 * @return Returns type tag.
	 */
	@Override
	public byte typeTag() {
		return typeTag;
	}

	/**
	 * @return Create a null value instance of the type.
	 */
	@Override
	public Value createNullValue() {
		return new ClassObjectReferenceImpl(virtualMachineImpl(),
				new JdwpClassObjectID(virtualMachineImpl()));
	}

	/**
	 * Flushes all stored Jdwp results.
	 */
	@Override
	public void flushStoredJdwpResults() {
		super.flushStoredJdwpResults();

		// For all classes that have this class cached as superclass, this cache
		// must be undone.
		Iterator<Object> itr = virtualMachineImpl().allCachedRefTypes();
		while (itr.hasNext()) {
			ReferenceTypeImpl refType = (ReferenceTypeImpl) itr.next();
			if (refType instanceof ClassTypeImpl) {
				ClassTypeImpl classType = (ClassTypeImpl) refType;
				if (classType.fSuperclass != null && classType.fSuperclass.equals(this)) {
					classType.flushStoredJdwpResults();
				}
			}
		}

		fSuperclass = null;
	}

	/**
	 * @return Returns a the single non-abstract Method visible from this class
	 *         that has the given name and signature.
	 */
	@Override
	public Method concreteMethodByName(String name, String signature) {
		/*
		 * Recursion is used to find the method: The methods of its own (own
		 * methods() command); The methods of it's superclass.
		 */

		Iterator<Method> methods = methods().iterator();
		Method method;
		while (methods.hasNext()) {
			method = methods.next();
			if (method.name().equals(name) && method.signature().equals(signature)) {
				if (method.isAbstract()) {
					return null;
				}
				return method;
			}
		}

		if (superclass() != null) {
			return superclass().concreteMethodByName(name, signature);
		}

		return null;
	}

	/* (non-Javadoc)
	 * @see com.sun.jdi.ClassType#invokeMethod(com.sun.jdi.ThreadReference, com.sun.jdi.Method, java.util.List, int)
	 */
	@Override
	public Value invokeMethod(ThreadReference thread, Method method, List<? extends Value> arguments, int options) throws InvalidTypeException,
			ClassNotLoadedException, IncompatibleThreadStateException,
			InvocationException {
		return invokeMethod(thread, method, arguments, options, JdwpCommandPacket.CT_INVOKE_METHOD);
	}

	/* (non-Javadoc)
	 * @see com.sun.jdi.ClassType#newInstance(com.sun.jdi.ThreadReference, com.sun.jdi.Method, java.util.List, int)
	 */
	@Override
	public ObjectReference newInstance(ThreadReference thread, Method method, List<? extends Value> arguments, int options) throws InvalidTypeException,
			ClassNotLoadedException, IncompatibleThreadStateException,
			InvocationException {
		checkVM(thread);
		checkVM(method);
		ThreadReferenceImpl threadImpl = (ThreadReferenceImpl) thread;
		MethodImpl methodImpl = (MethodImpl) method;

		// Perform some checks for IllegalArgumentException.
		if (!methods().contains(method))
			throw new IllegalArgumentException(
					JDIMessages.ClassTypeImpl_Class_does_not_contain_given_method_4);
		if (method.argumentTypeNames().size() != arguments.size())
			throw new IllegalArgumentException(
					JDIMessages.ClassTypeImpl_Number_of_arguments_doesn__t_match_5);
		if (!method.isConstructor())
			throw new IllegalArgumentException(
					JDIMessages.ClassTypeImpl_Method_is_not_a_constructor_6);

		List<Value> checkedArguments = ValueImpl.checkValues(arguments, method.argumentTypes(), virtualMachineImpl());

		initJdwpRequest();
		try {
			ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
			DataOutputStream outData = new DataOutputStream(outBytes);
			write(this, outData);
			threadImpl.write(this, outData);
			methodImpl.write(this, outData);

			writeInt(checkedArguments.size(), "size", outData); //$NON-NLS-1$
			Iterator<Value> iter = checkedArguments.iterator();
			while (iter.hasNext()) {
				Value elt = iter.next();
				if (elt instanceof ValueImpl) {
					checkVM(elt);
					((ValueImpl)elt).writeWithTag(this, outData);
				} else {
					ValueImpl.writeNullWithTag(this, outData);
				}
			}

			writeInt(optionsToJdwpOptions(options),
					"options", MethodImpl.getInvokeOptions(), outData); //$NON-NLS-1$

			JdwpReplyPacket replyPacket = requestVM(
					JdwpCommandPacket.CT_NEW_INSTANCE, outBytes);
			switch (replyPacket.errorCode()) {
			case JdwpReplyPacket.INVALID_METHODID:
				throw new IllegalArgumentException();
			case JdwpReplyPacket.TYPE_MISMATCH:
				throw new InvalidTypeException();
			case JdwpReplyPacket.INVALID_CLASS:
				throw new ClassNotLoadedException(name());
			case JdwpReplyPacket.INVALID_THREAD:
				throw new IncompatibleThreadStateException();
			case JdwpReplyPacket.THREAD_NOT_SUSPENDED:
				throw new IncompatibleThreadStateException();
			}
			defaultReplyErrorHandler(replyPacket.errorCode());
			DataInputStream replyData = replyPacket.dataInStream();
			ObjectReferenceImpl object = ObjectReferenceImpl
					.readObjectRefWithTag(this, replyData);
			ObjectReferenceImpl exception = ObjectReferenceImpl
					.readObjectRefWithTag(this, replyData);
			if (exception != null)
				throw new InvocationException(exception);
			return object;
		} catch (IOException e) {
			defaultIOExceptionHandler(e);
			return null;
		} finally {
			handledJdwpRequest();
		}
	}

	/**
	 * Assigns a value to a static field. .
	 */
	@Override
	public void setValue(Field field, Value value) throws InvalidTypeException,
			ClassNotLoadedException {
		// Note that this information should not be cached.
		initJdwpRequest();
		try {
			ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
			DataOutputStream outData = new DataOutputStream(outBytes);
			write(this, outData);
			writeInt(1, "size", outData); // We only set one field //$NON-NLS-1$
			checkVM(field);
			((FieldImpl) field).write(this, outData);

			// check the type and the VM of the value. Convert the value if
			// needed
			ValueImpl checkedValue = ValueImpl.checkValue(value, field.type(),
					virtualMachineImpl());

			if (checkedValue != null) {
				checkedValue.write(this, outData);
			} else {
				ValueImpl.writeNull(this, outData);
			}

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

	/* (non-Javadoc)
	 * @see com.sun.jdi.ClassType#subclasses()
	 */
	@Override
	public List<ClassType> subclasses() {
		// Note that this information should not be cached.
		List<ClassType> subclasses = new ArrayList<>();
		Iterator<ReferenceType> itr = virtualMachineImpl().allRefTypes();
		while (itr.hasNext()) {
			try {
				ReferenceType refType = itr.next();
				if (refType instanceof ClassTypeImpl) {
					ClassTypeImpl classType = (ClassTypeImpl) refType;
					if (classType.superclass() != null
							&& classType.superclass().equals(this)) {
						subclasses.add(classType);
					}
				}
			} catch (ClassNotPreparedException e) {
				continue;
			}
		}
		return subclasses;
	}

	/* (non-Javadoc)
	 * @see com.sun.jdi.ClassType#superclass()
	 */
	@Override
	public ClassType superclass() {
		if (fSuperclass != null)
			return fSuperclass;

		initJdwpRequest();
		try {
			JdwpReplyPacket replyPacket = requestVM(
					JdwpCommandPacket.CT_SUPERCLASS, this);
			defaultReplyErrorHandler(replyPacket.errorCode());
			DataInputStream replyData = replyPacket.dataInStream();
			fSuperclass = ClassTypeImpl.read(this, replyData);
			return fSuperclass;
		} catch (IOException e) {
			defaultIOExceptionHandler(e);
			return null;
		} finally {
			handledJdwpRequest();
		}
	}

	/*
	 * @return Reads ID and returns known ReferenceTypeImpl with that ID, or if
	 * ID is unknown a newly created ReferenceTypeImpl.
	 */
	public static ClassTypeImpl read(MirrorImpl target, DataInputStream in)
			throws IOException {
		VirtualMachineImpl vmImpl = target.virtualMachineImpl();
		JdwpClassID ID = new JdwpClassID(vmImpl);
		ID.read(in);
		if (target.fVerboseWriter != null)
			target.fVerboseWriter.println("classType", ID.value()); //$NON-NLS-1$

		if (ID.isNull())
			return null;

		ClassTypeImpl mirror = (ClassTypeImpl) vmImpl.getCachedMirror(ID);
		if (mirror == null) {
			mirror = new ClassTypeImpl(vmImpl, ID);
			vmImpl.addCachedMirror(mirror);
		}
		return mirror;
	}

	/*
	 * @return Reads ID and returns known ReferenceTypeImpl with that ID, or if
	 * ID is unknown a newly created ReferenceTypeImpl.
	 */
	public static ClassTypeImpl readWithSignature(MirrorImpl target,
			boolean withGenericSignature, DataInputStream in)
			throws IOException {
		VirtualMachineImpl vmImpl = target.virtualMachineImpl();
		JdwpClassID ID = new JdwpClassID(vmImpl);
		ID.read(in);
		if (target.fVerboseWriter != null)
			target.fVerboseWriter.println("classType", ID.value()); //$NON-NLS-1$

		String signature = target.readString("signature", in); //$NON-NLS-1$
		String genericSignature = null;
		if (withGenericSignature) {
			genericSignature = target.readString("generic signature", in); //$NON-NLS-1$
		}
		if (ID.isNull())
			return null;

		ClassTypeImpl mirror = (ClassTypeImpl) vmImpl.getCachedMirror(ID);
		if (mirror == null) {
			mirror = new ClassTypeImpl(vmImpl, ID);
			vmImpl.addCachedMirror(mirror);
		}
		mirror.setSignature(signature);
		mirror.setGenericSignature(genericSignature);
		return mirror;
	}

	@Override
	public boolean isEnum() {
		if (virtualMachineImpl().isJdwpVersionGreaterOrEqual(1, 5)) {
			// there is no modifier for this ... :(
			ClassType superClass = superclass();
			return superClass != null
					&& "<E:Ljava/lang/Enum<TE;>;>Ljava/lang/Object;Ljava/lang/Comparable<TE;>;Ljava/io/Serializable;".equals(superClass.genericSignature()); //$NON-NLS-1$
		}
		// jdwp 1.5 only option
		return false;
	}

}
