/*******************************************************************************
 * Copyright (c) 2000, 2007 QNX Software Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     QNX Software Systems - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.debug.mi.core.cdi.model;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.cdt.debug.core.cdi.CDIException;
import org.eclipse.cdt.debug.core.cdi.ICDIFormat;
import org.eclipse.cdt.debug.core.cdi.model.ICDITarget;
import org.eclipse.cdt.debug.core.cdi.model.ICDIValue;
import org.eclipse.cdt.debug.core.cdi.model.ICDIVariable;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIArrayType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIBoolType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDICharType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIDoubleType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIEnumType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIFloatType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIFunctionType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIIntType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDILongLongType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDILongType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIPointerType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIReferenceType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIShortType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIStructType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIType;
import org.eclipse.cdt.debug.core.cdi.model.type.ICDIWCharType;
import org.eclipse.cdt.debug.mi.core.MIException;
import org.eclipse.cdt.debug.mi.core.MIPlugin;
import org.eclipse.cdt.debug.mi.core.MISession;
import org.eclipse.cdt.debug.mi.core.cdi.CdiResources;
import org.eclipse.cdt.debug.mi.core.cdi.ExpressionManager;
import org.eclipse.cdt.debug.mi.core.cdi.Format;
import org.eclipse.cdt.debug.mi.core.cdi.MI2CDIException;
import org.eclipse.cdt.debug.mi.core.cdi.MemoryManager;
import org.eclipse.cdt.debug.mi.core.cdi.RegisterManager;
import org.eclipse.cdt.debug.mi.core.cdi.Session;
import org.eclipse.cdt.debug.mi.core.cdi.VariableManager;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.ArrayValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.BoolValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.CharValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.DoubleValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.EnumValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.FloatValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.FunctionValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.IncompleteType;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.IntValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.LongLongValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.LongValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.PointerValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.ReferenceValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.ShortValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.StructValue;
import org.eclipse.cdt.debug.mi.core.cdi.model.type.WCharValue;
import org.eclipse.cdt.debug.mi.core.command.CommandFactory;
import org.eclipse.cdt.debug.mi.core.command.MIVarAssign;
import org.eclipse.cdt.debug.mi.core.command.MIVarCreate;
import org.eclipse.cdt.debug.mi.core.command.MIVarInfoExpression;
import org.eclipse.cdt.debug.mi.core.command.MIVarInfoType;
import org.eclipse.cdt.debug.mi.core.command.MIVarListChildren;
import org.eclipse.cdt.debug.mi.core.command.MIVarSetFormat;
import org.eclipse.cdt.debug.mi.core.command.MIVarShowAttributes;
import org.eclipse.cdt.debug.mi.core.event.MIVarChangedEvent;
import org.eclipse.cdt.debug.mi.core.output.MIInfo;
import org.eclipse.cdt.debug.mi.core.output.MIVar;
import org.eclipse.cdt.debug.mi.core.output.MIVarCreateInfo;
import org.eclipse.cdt.debug.mi.core.output.MIVarInfoExpressionInfo;
import org.eclipse.cdt.debug.mi.core.output.MIVarInfoTypeInfo;
import org.eclipse.cdt.debug.mi.core.output.MIVarListChildrenInfo;
import org.eclipse.cdt.debug.mi.core.output.MIVarShowAttributesInfo;

/**
 */
public abstract class Variable extends VariableDescriptor implements ICDIVariable {
	private static final ICDIVariable[] NO_CHILDREN = new ICDIVariable[0];
	protected MIVarCreate fVarCreateCMD;
	protected MIVar fMIVar;
	Value value;
	public ICDIVariable[] children = NO_CHILDREN;
	String editable = null;
	String language;
	boolean isFake = false;
	boolean isUpdated = true;
	private String hexAddress;

	public Variable(VariableDescriptor obj, MIVarCreate var) {
		super(obj);
		fVarCreateCMD = var;
	}

	public Variable(Target target, Thread thread, StackFrame frame, String n, String q, int pos, int depth, MIVar miVar) {
		super(target, thread, frame, n, q, pos, depth);
		fMIVar = miVar;
	}

	public void setUpdated(boolean update) {
		isUpdated = update;
	}

	public boolean isUpdated() {
		return isUpdated;
	}

	public void update() throws CDIException {
		Session session = (Session)getTarget().getSession();
		VariableManager mgr = session.getVariableManager();
		mgr.update(this);
	}

	public MIVar getMIVar() throws CDIException {
		if (fMIVar == null) {

			// Oops! what's up here, we should use Assert
			if (fVarCreateCMD == null) {
				throw new CDIException("Incomplete initialization of variable"); //$NON-NLS-1$
			}

			try {
				MISession mi = ((Target)getTarget()).getMISession();
				MIVarCreateInfo info = null;
				// Wait for the response or timedout
				synchronized (fVarCreateCMD) {
					// RxThread will set the MIOutput on the cmd
					// when the response arrive.
					while ((info = fVarCreateCMD.getMIVarCreateInfo()) == null) {
						try {
							fVarCreateCMD.wait(mi.getCommandTimeout());
							info = fVarCreateCMD.getMIVarCreateInfo();
							if (info == null) {
								throw new MIException(MIPlugin.getResourceString("src.MISession.Target_not_responding")); //$NON-NLS-1$
							}
						} catch (InterruptedException e) {
						}
					}
				}
				
				if (info == null) {
					throw new CDIException(CdiResources.getString("cdi.Common.No_answer")); //$NON-NLS-1$
				}
				fMIVar = info.getMIVar();
			} catch (MIException e) {
				throw new MI2CDIException(e);
			}
		}
		return fMIVar;
	}

	private String getHexAddress() throws CDIException {
		if (hexAddress != null) {
			return hexAddress;
		}
		VariableManager vm = ((Session)((Target)getTarget()).getSession()).getVariableManager();
		String qualName = "&(" + getQualifiedName() + ")"; //$NON-NLS-1$ //$NON-NLS-2$
		VariableDescriptor desc = createDescriptor((Target)getTarget(), (Thread)getThread(), (StackFrame)getStackFrame(), getName(), qualName, getPosition(), getStackDepth());
		Variable v = vm.createVariable( desc );
		v.setFormat(ICDIFormat.HEXADECIMAL);
		hexAddress = v.getValue().getValueString();		
		return hexAddress;
	}

	public Variable getChild(String name) {
		for (int i = 0; i < children.length; i++) {
			Variable variable = (Variable) children[i];
			try {
				if (name.equals(variable.getMIVar().getVarName())) {
					return variable;
				}
				// Look also in the grandchildren.
				Variable grandChild = variable.getChild(name);
				if (grandChild != null) {
					return grandChild;
				}
			} catch (CDIException e) {
				// ignore;
			}
		}
		return null;
	}

	String getLanguage() throws CDIException {
		if (language == null) {
			MISession mi = ((Target)getTarget()).getMISession();
			CommandFactory factory = mi.getCommandFactory();
			MIVarInfoExpression var = factory.createMIVarInfoExpression(getMIVar().getVarName());
			try {
				mi.postCommand(var);
				MIVarInfoExpressionInfo info = var.getMIVarInfoExpressionInfo();
				if (info == null) {
					throw new CDIException(CdiResources.getString("cdi.Common.No_answer")); //$NON-NLS-1$
				}
				language = info.getLanguage();
			} catch (MIException e) {
				throw new MI2CDIException(e);
			}
		}
		return (language == null) ? "" : language; //$NON-NLS-1$
	}

	boolean isCPPLanguage() throws CDIException {
		return getLanguage().equalsIgnoreCase("C++"); //$NON-NLS-1$
	}

	void setIsFake(boolean f) {
		isFake = f;
	}

	boolean isFake() {
		return isFake;
	}

	public ICDIVariable[] getChildren() throws CDIException {
		// Use the default timeout.
		return getChildren(-1);
	}

	/**
	 * This can be a potentially long operation for GDB.
	 * allow the override of the timeout.
	 */
	public ICDIVariable[] getChildren(int timeout) throws CDIException {
		MISession mi = ((Target)getTarget()).getMISession();
		CommandFactory factory = mi.getCommandFactory();
		MIVarListChildren var = factory.createMIVarListChildren(getMIVar().getVarName());
		try {
			if (timeout >= 0) {
				mi.postCommand(var, timeout);
			} else {
				mi.postCommand(var);
			}
			MIVarListChildrenInfo info = var.getMIVarListChildrenInfo();
			if (info == null) {
				throw new CDIException(CdiResources.getString("cdi.Common.No_answer")); //$NON-NLS-1$
			}
			MIVar[] vars = info.getMIVars();
			List childrenList = new ArrayList(vars.length);
			for (int i = 0; i < vars.length; i++) {
				String fn = getQualifiedName();
				String childName = vars[i].getExp();
				ICDIType childType = null;
				boolean childFake = false;
				ICDIType t = getType();
				if (t instanceof ICDIArrayType) {
					// For Array gdb varobj only return the index, override here.
					int index = castingIndex + i;
					fn = "(" + fn + ")[" + index + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					childName = getName() + "[" + index + "]"; //$NON-NLS-1$ //$NON-NLS-2$
				} else if (t instanceof ICDIPointerType) {
					ICDIType subType = ((ICDIPointerType)t).getComponentType();
					if (subType instanceof ICDIStructType || t instanceof IncompleteType) {
						if (isCPPLanguage()) {
							if (!isFake() || (isFake() && !isAccessQualifier(fName))) {
								childFake = true;
								childType = t;
							} else {
								fn = "(" + fn + ")->" + vars[i].getExp(); //$NON-NLS-1$ //$NON-NLS-2$
							}
						} else { // If not C++ language
							fn = "(" + fn + ")->" + vars[i].getExp(); //$NON-NLS-1$ //$NON-NLS-2$
						}
					} else {
						fn = "*(" + fn + ")"; //$NON-NLS-1$ //$NON-NLS-2$
					}
				} else if (t instanceof ICDIReferenceType) {
					ICDIType subType = ((ICDIReferenceType)t).getComponentType();
					if (subType instanceof ICDIStructType || t instanceof IncompleteType) {
						if (isCPPLanguage()) {
							if (!isFake() || (isFake() && !isAccessQualifier(fName))) {
								childFake = true;
								childType = t;
							} else {
								fn = "(" + fn + ")." + vars[i].getExp(); //$NON-NLS-1$ //$NON-NLS-2$
							}
						} else { // If not C++ language
							fn = "(" + fn + ")." + vars[i].getExp(); //$NON-NLS-1$ //$NON-NLS-2$
						}
					} else {
						fn = "(" + fn + ")"; //$NON-NLS-1$ //$NON-NLS-2$
					}
				} else if (t instanceof ICDIStructType || t instanceof IncompleteType) {
					if (isCPPLanguage()) {
						if (!isFake() || (isFake() && !isAccessQualifier(fName))) {
							childFake = true;
							childType = t;
						} else {
							fn = "(" + fn + ")." + vars[i].getExp(); //$NON-NLS-1$ //$NON-NLS-2$
						}
					} else { // If not C++ language
						fn = "(" + fn + ")." + vars[i].getExp(); //$NON-NLS-1$ //$NON-NLS-2$
					}
				}
				Variable v = createVariable((Target)getTarget(), (Thread)getThread(), (StackFrame)getStackFrame(),
						childName, fn, getPosition(), getStackDepth(), vars[i]);
				if (childType != null) {
					// Hack to reset the typename to a known value
					v.fType = childType;
				}
				v.setIsFake(childFake);
				if (childFake && isAccessQualifier(childName)) {
					// don't add these, add their kids
					ICDIVariable[] grandchildren = v.getChildren();
					for (int j = 0; j < grandchildren.length; ++j)
						childrenList.add(grandchildren[j]);
				} else
					childrenList.add(v);
			}
			
			children = (ICDIVariable[])childrenList.toArray(new ICDIVariable[childrenList.size()]);
		} catch (MIException e) {
			throw new MI2CDIException(e);
		}
		return children;
	}

	boolean isAccessQualifier(String foo) {
	    return foo.equals("private") || foo.equals("public") || foo.equals("protected");  //$NON-NLS-1$  //$NON-NLS-2$  //$NON-NLS-3$ 
    }

	protected abstract Variable createVariable(Target target, Thread thread, StackFrame frame,
			String name, String fullName, int pos, int depth, MIVar miVar);
	
	public int getChildrenNumber() throws CDIException {
		return getMIVar().getNumChild();
	}

	/**
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariable#getValue()
	 */
	public ICDIValue getValue() throws CDIException {
		if (value == null) {
			ICDIType t = getType();
			if (t instanceof ICDIBoolType) {
				value = new BoolValue(this);
			} else if (t instanceof ICDICharType) {
				value = new CharValue(this);
			} else if (t instanceof ICDIWCharType) {
				value = new WCharValue(this);
			} else if (t instanceof ICDIShortType) {
				value = new ShortValue(this);
			} else if (t instanceof ICDIIntType) {
				value = new IntValue(this);
			} else if (t instanceof ICDILongType) {
				value = new LongValue(this);
			} else if (t instanceof ICDILongLongType) {
				value = new LongLongValue(this);
			} else if (t instanceof ICDIEnumType) {
				value = new EnumValue(this);
			} else if (t instanceof ICDIFloatType) {
				value = new FloatValue(this);
			} else if (t instanceof ICDIDoubleType) {
				value = new DoubleValue(this);
			} else if (t instanceof ICDIFunctionType) {
				value = new FunctionValue(this);
			} else if (t instanceof ICDIPointerType) {
				value = new PointerValue(this);
			} else if (t instanceof ICDIReferenceType) {
				value = new ReferenceValue(this, getHexAddress());
			} else if (t instanceof ICDIArrayType) {
				value = new ArrayValue(this, getHexAddress());
			} else if (t instanceof ICDIStructType) {
				value = new StructValue(this);
			} else {
				value = new Value(this);
			}
		}
		return value;
	}

	/**
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariable#setValue(ICDIValue)
	 */
	public void setValue(ICDIValue value) throws CDIException {
		setValue(value.getValueString());
	}

	/**
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariable#setValue(String)
	 */
	public void setValue(String expression) throws CDIException {
		Target target = (Target)getTarget();
		MISession miSession = target.getMISession();
		CommandFactory factory = miSession.getCommandFactory();
		MIVarAssign var = factory.createMIVarAssign(getMIVar().getVarName(), expression);
		try {
			miSession.postCommand(var);
			MIInfo info = var.getMIInfo();
			if (info == null) {
				throw new CDIException(CdiResources.getString("cdi.Common.No_answer")); //$NON-NLS-1$
			}
		} catch (MIException e) {
			throw new MI2CDIException(e);
		}

		// If the assign was succesfull fire a MIVarChangedEvent() for the variable
		// Note GDB will not fire an event for the changed variable we have to do it manually.
		MIVarChangedEvent change = new MIVarChangedEvent(miSession, var.getToken(), getMIVar().getVarName());
		miSession.fireEvent(change);

		// Changing values may have side effects i.e. affecting other variables
		// if the manager is on autoupdate check all the other variables.
		// Note: This maybe very costly.
		// assigning may have side effects i.e. affecting other registers.

		// If register was on autoupdate, update all the other registers
		RegisterManager regMgr = ((Session)target.getSession()).getRegisterManager();
		if (regMgr.isAutoUpdate()) {
			regMgr.update(target);
		}
		
		// If expression manager is on autoupdate, update all expressions
		ExpressionManager expMgr = ((Session)target.getSession()).getExpressionManager();
		if (expMgr.isAutoUpdate()) {
			expMgr.update(target);
		}
		
		// If variable manager is on autoupdate, update all variables
		VariableManager varMgr = ((Session)target.getSession()).getVariableManager();
		if (varMgr.isAutoUpdate()) {
			varMgr.update(target);
		}

		// If memory manager is on autoupdate, update all memory blocks
		MemoryManager memMgr = ((Session)target.getSession()).getMemoryManager();
		if (memMgr.isAutoUpdate()) {
			memMgr.update(target);
		}
	}

	/**
	 * Overload the implementation of VariableDescriptor and let gdb
	 * handle it.
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariable#isEditable()
	 */
	public boolean isEditable() throws CDIException {
		if (editable == null) {
			MISession mi = ((Target) getTarget()).getMISession();
			CommandFactory factory = mi.getCommandFactory();
			MIVarShowAttributes var = factory.createMIVarShowAttributes(getMIVar().getVarName());
			try {
				mi.postCommand(var);
				MIVarShowAttributesInfo info = var.getMIVarShowAttributesInfo();
				if (info == null) {
					throw new CDIException(CdiResources.getString("cdi.Common.No_answer")); //$NON-NLS-1$
				}
				editable = String.valueOf(info.isEditable());
			} catch (MIException e) {
				throw new MI2CDIException(e);
			}
		}
		return (editable == null) ? false : editable.equalsIgnoreCase("true"); //$NON-NLS-1$
	}

	/**
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariable#setFormat()
	 */
	public void setFormat(int format) throws CDIException {
		int fmt = Format.toMIFormat(format);
		MISession mi = ((Target) getTarget()).getMISession();
		CommandFactory factory = mi.getCommandFactory();
		MIVarSetFormat var = factory.createMIVarSetFormat(getMIVar().getVarName(), fmt);
		try {
			mi.postCommand(var);
			MIInfo info = var.getMIInfo();
			if (info == null) {
				throw new CDIException(CdiResources.getString("cdi.Common.No_answer")); //$NON-NLS-1$
			}
		} catch (MIException e) {
			throw new MI2CDIException(e);
		}
	}

	/**
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariable#equals()
	 */
	public boolean equals(ICDIVariable var) {
		if (var instanceof Variable) {
			Variable variable = (Variable) var;
			return equals(variable);
		}
		return super.equals(var);
	}

	/**
	 * @param variable
	 * @return
	 */
	public boolean equals(Variable variable) {
		try {
			return getMIVar().getVarName().equals(variable.getMIVar().getVarName());
		} catch (CDIException e) {
			// ignore.
		}
		return super.equals(variable);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariable#dispose()
	 */
	public void dispose() throws CDIException {
		ICDITarget target = getTarget();
		VariableManager varMgr = ((Session)target.getSession()).getVariableManager();
		varMgr.destroyVariable(this);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.cdi.model.ICDIVariableDescriptor#getTypeName()
	 */
	public String getTypeName() throws CDIException {
		if (fTypename == null) {
			fTypename = getMIVar().getType();
			if (fTypename == null || fTypename.length() == 0) {
				MISession mi = ((Target) getTarget()).getMISession();
				CommandFactory factory = mi.getCommandFactory();
				MIVarInfoType infoType = factory.createMIVarInfoType(getMIVar().getVarName());
				try {
					mi.postCommand(infoType);
					MIVarInfoTypeInfo info = infoType.getMIVarInfoTypeInfo();
					if (info == null) {
						throw new CDIException(CdiResources.getString("cdi.Common.No_answer")); //$NON-NLS-1$
					}
					fTypename = info.getType();
				} catch (MIException e) {
					throw new MI2CDIException(e);
				}
			}
		}
		return fTypename;
	}

	public void setMIVarCreate(MIVarCreate miVar) {
		fVarCreateCMD = miVar;
	}

	abstract protected VariableDescriptor createDescriptor(Target target, Thread thread, StackFrame frame, String n, String fn, int pos, int depth); 
}
