/*******************************************************************************
 * 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.jdt.internal.debug.core.model;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IValue;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.debug.core.IJavaType;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.core.IJavaVariable;

import com.sun.jdi.ArrayReference;
import com.sun.jdi.ClassObjectReference;
import com.sun.jdi.Field;
import com.sun.jdi.ObjectCollectedException;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.PrimitiveValue;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StringReference;
import com.sun.jdi.Type;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.Value;
import com.sun.jdi.VoidValue;

/**
 * Represents the value of a java variable
 *
 * @see IJavaValue
 */
public class JDIValue extends JDIDebugElement implements IJavaValue {

	private Value fValue;
	private List<IJavaVariable> fVariables;

	/**
	 * A flag indicating if this value is still allocated (valid)
	 */
	private boolean fAllocated = true;

	/**
	 * When created for a logical structure we hold onto the original
	 * non-logical value for purposes of equality. This way a logical
	 * structure's children remain more stable in the variables view.
	 *
	 * This is <code>null</code> when not created for a logical structure.
	 */
	protected IJavaValue fLogicalParent;

	/**
	 * Constructor
	 *
	 * @param target
	 *            debug target that this value belongs to
	 * @param value
	 *            the underlying value this value represents
	 */
	public JDIValue(JDIDebugTarget target, Value value) {
		super(target);
		fValue = value;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.eclipse.jdt.internal.debug.core.model.JDIDebugElement#getAdapter(
	 * java.lang.Class)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == IJavaValue.class) {
			return (T) this;
		}
		return super.getAdapter(adapter);
	}

	/**
	 * Creates the appropriate kind of value - i.e. a primitive value, object,
	 * class object, array, <code>null</code> or void.
	 */
	public static JDIValue createValue(JDIDebugTarget target, Value value) {
		if (value == null) {
			return new JDINullValue(target);
		}
		if (value instanceof ArrayReference) {
			return new JDIArrayValue(target, (ArrayReference) value);
		}
		if (value instanceof ClassObjectReference) {
			return new JDIClassObjectValue(target, (ClassObjectReference) value);
		}
		if (value instanceof ObjectReference) {
			return new JDIObjectValue(target, (ObjectReference) value);
		}
		if (value instanceof PrimitiveValue) {
			return new JDIPrimitiveValue(target, value);
		}
		if (value instanceof VoidValue) {
			return new JDIVoidValue(target);
		}
		return new JDIValue(target, value);
	}

	/**
	 * @see IValue#getValueString()
	 */
	@Override
	public String getValueString() throws DebugException {
		if (fValue == null) {
			return JDIDebugModelMessages.JDIValue_null_4;
		}
		if (fValue instanceof StringReference) {
			try {
				return ((StringReference) fValue).value();
			} catch (ObjectCollectedException e) {
				return JDIDebugModelMessages.JDIValue_deallocated;
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIValue_exception_retrieving_value,
								new Object[] { e.toString() }), e);
				// execution will not reach this line, as
				// #targetRequestFailed will thrown an exception
				return null;
			}
		}
		if (fValue instanceof ObjectReference) {
			StringBuilder name = new StringBuilder();
			if (fValue instanceof ClassObjectReference) {
				name.append('(');
				name.append(((ClassObjectReference) fValue).reflectedType());
				name.append(')');
			}
			long id = 0;
			try {
				id = ((ObjectReference) fValue).uniqueID();
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIValue_exception_retrieving_unique_id,
								new Object[] { e.toString() }), e);
				// execution will not reach this line, as
				// #targetRequestFailed will thrown an exception
				return null;
			}
			name.append(" "); //$NON-NLS-1$
			name.append(MessageFormat.format(
					JDIDebugModelMessages.JDIValue_id_8,
					new Object[] { String.valueOf(id) }));
			return name.toString();
		}
		// see bug 43285
		return String.valueOf(fValue);
	}

	/**
	 * @see IValue#getReferenceTypeName()
	 */
	@Override
	public String getReferenceTypeName() throws DebugException {
		try {
			if (fValue == null) {
				return JDIDebugModelMessages.JDIValue_null_4;
			}
			return getUnderlyingType().name();
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIValue_exception_retrieving_reference_type_name,
							new Object[] { e.toString() }), e);
			// execution will not reach this line, as
			// #targetRequestFailed will thrown an exception
			return null;
		}
	}

	/**
	 * @see Object#hashCode()
	 */
	@Override
	public int hashCode() {
		if (fValue == null) {
			return getClass().hashCode();
		}
		return fValue.hashCode();
	}

	/**
	 * @see Object#equals(Object)
	 */
	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o instanceof JDIValue) {
			Value other = ((JDIValue) o).getUnderlyingValue();
			if (fValue == null) {
				return false;
			}
			if (other == null) {
				return false;
			}
			return fValue.equals(other);
		}
		return false;
	}

	/**
	 * @see IValue#getVariables()
	 */
	@Override
	public IVariable[] getVariables() throws DebugException {
		List<IJavaVariable> list = getVariablesList();
		return list.toArray(new IVariable[list.size()]);
	}

	/**
	 * Returns a list of variables that are children of this value. The result
	 * is cached.
	 *
	 * @return list of variable children
	 * @throws DebugException
	 */
	protected synchronized List<IJavaVariable> getVariablesList() throws DebugException {
		if (fVariables != null) {
			return fVariables;
		} else if (fValue instanceof ObjectReference) {
			ObjectReference object = (ObjectReference) fValue;
			fVariables = new ArrayList<>();
			if (isArray()) {
				try {
					int length = getArrayLength();
					for (int i = 0; i < length; i++) {
						fVariables.add(new JDIArrayEntryVariable(
								getJavaDebugTarget(), getArrayReference(), i,
								fLogicalParent));
					}
				} catch (DebugException e) {
					if (e.getCause() instanceof ObjectCollectedException) {
						return Collections.EMPTY_LIST;
					}
					throw e;
				}
			} else {
				List<Field> fields = null;
				try {
					ReferenceType refType = object.referenceType();
					fields = refType.allFields();
				} catch (ObjectCollectedException e) {
					return Collections.EMPTY_LIST;
				} catch (RuntimeException e) {
					targetRequestFailed(
							MessageFormat.format(
									JDIDebugModelMessages.JDIValue_exception_retrieving_fields,
									new Object[] { e.toString() }), e);
					// execution will not reach this line, as
					// #targetRequestFailed will thrown an exception
					return null;
				}
				Iterator<Field> list = fields.iterator();
				while (list.hasNext()) {
					Field field = list.next();
					fVariables.add(new JDIFieldVariable(
							(JDIDebugTarget) getDebugTarget(), field, object,
							fLogicalParent));
				}
				Collections.sort(fVariables, (a, b) -> sortChildren(a, b));
			}

			return fVariables;
		} else {
			return Collections.EMPTY_LIST;
		}
	}

	/**
	 * Group statics and instance variables, sort alphabetically within each
	 * group.
	 */
	protected int sortChildren(Object a, Object b) {
		IJavaVariable v1 = (IJavaVariable) a;
		IJavaVariable v2 = (IJavaVariable) b;

		try {
			boolean v1isStatic = v1.isStatic();
			boolean v2isStatic = v2.isStatic();
			if (v1isStatic && !v2isStatic) {
				return -1;
			}
			if (!v1isStatic && v2isStatic) {
				return 1;
			}
			return v1.getName().compareToIgnoreCase(v2.getName());
		} catch (DebugException de) {
			logError(de);
			return -1;
		}
	}

	/**
	 * Returns whether this value is an array
	 */
	protected boolean isArray() {
		return fValue instanceof ArrayReference;
	}

	/**
	 * Returns this value as an array reference, or <code>null</code>
	 */
	protected ArrayReference getArrayReference() {
		if (isArray()) {
			return (ArrayReference) fValue;
		}
		return null;
	}

	/**
	 * @see IValue#isAllocated()
	 */
	@Override
	public boolean isAllocated() throws DebugException {
		if (fAllocated) {
			if (fValue instanceof ObjectReference) {
				try {
					fAllocated = !((ObjectReference) fValue).isCollected();
				} catch (VMDisconnectedException e) {
					// if the VM disconnects, this value is not allocated
					fAllocated = false;
				} catch (RuntimeException e) {
					targetRequestFailed(
							MessageFormat.format(
									JDIDebugModelMessages.JDIValue_exception_is_collected,
									new Object[] { e.toString() }), e);
					// execution will fall through, as
					// #targetRequestFailed will thrown an exception
				}
			} else {
				JDIDebugTarget dt = (JDIDebugTarget) getDebugTarget();
				fAllocated = dt.isAvailable();
			}
		}
		return fAllocated;
	}

	/**
	 * @see IJavaValue#getSignature()
	 */
	@Override
	public String getSignature() throws DebugException {
		try {
			if (fValue != null) {
				return fValue.type().signature();
			}
			return null;
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIValue_exception_retrieving_type_signature,
							new Object[] { e.toString() }), e);
			// execution will not reach this line, as
			// #targetRequestFailed will thrown an exception
			return null;
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaValue#getGenericSignature()
	 */
	@Override
	public String getGenericSignature() throws DebugException {
		try {
			if (fValue != null) {
				Type type = fValue.type();
				if (type instanceof ReferenceType) {
					return ((ReferenceType) type).genericSignature();
				}
				return null;
			}
			return null;
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIValue_exception_retrieving_type_signature,
							new Object[] { e.toString() }), e);
			// execution will not reach this line, as
			// #targetRequestFailed will thrown an exception
			return null;
		}
	}

	/**
	 * @see IJavaValue#getArrayLength()
	 */
	public int getArrayLength() throws DebugException {
		if (isArray()) {
			try {
				return getArrayReference().length();
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIValue_exception_retrieving_length_of_array,
								new Object[] { e.toString() }), e);
			}
		}
		return -1;
	}

	/**
	 * Returns this value's underlying JDI value
	 */
	protected Value getUnderlyingValue() {
		return fValue;
	}

	/**
	 * @see IJavaValue#getJavaType()
	 */
	@Override
	public IJavaType getJavaType() throws DebugException {
		return JDIType.createType((JDIDebugTarget) getDebugTarget(),
				getUnderlyingType());
	}

	/**
	 * Returns this value's underlying type.
	 *
	 * @return type
	 * @exception DebugException
	 *                if this method fails. Reasons include:
	 *                <ul>
	 *                <li>Failure communicating with the VM. The
	 *                DebugException's status code contains the underlying
	 *                exception responsible for the failure.</li>
	 */
	protected Type getUnderlyingType() throws DebugException {
		try {
			return getUnderlyingValue().type();
		} catch (RuntimeException e) {
			targetRequestFailed(MessageFormat.format(
					JDIDebugModelMessages.JDIValue_exception_retrieving_type,
					new Object[] { e.toString() }), e);
			// execution will not fall through to here,
			// as #requestFailed will throw an exception
			return null;
		}
	}

	/**
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return getUnderlyingValue().toString();
	}

	/**
	 * @see IValue#hasVariables()
	 */
	@Override
	public boolean hasVariables() throws DebugException {
		return getVariablesList().size() > 0;
	}

	/**
	 * Sets the value that is the original non-logical value that this child
	 * value was computed for.
	 *
	 * @param logicalParent
	 *            parent value
	 */
	public void setLogicalParent(IJavaValue logicalParent) {
		fLogicalParent = logicalParent;
	}

	/**
	 * Returns the value that is the original non-logical value that this child
	 * value was computed for or <code>null</code> if none
	 *
	 * @param logicalParent
	 *            parent value or <code>null</code>
	 */
	public IJavaValue getLogicalParent() {
		return fLogicalParent;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaValue#isNull()
	 */
	@Override
	public boolean isNull() {
		return false;
	}

}
