/*******************************************************************************
 * Copyright (c) 2010, 2011 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.jsdt.debug.internal.core.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
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.jface.text.Document;
import org.eclipse.osgi.util.NLS;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.wst.jsdt.core.ToolFactory;
import org.eclipse.wst.jsdt.core.formatter.CodeFormatter;
import org.eclipse.wst.jsdt.debug.core.jsdi.ArrayReference;
import org.eclipse.wst.jsdt.debug.core.jsdi.BooleanValue;
import org.eclipse.wst.jsdt.debug.core.jsdi.FunctionReference;
import org.eclipse.wst.jsdt.debug.core.jsdi.NullValue;
import org.eclipse.wst.jsdt.debug.core.jsdi.NumberValue;
import org.eclipse.wst.jsdt.debug.core.jsdi.ObjectReference;
import org.eclipse.wst.jsdt.debug.core.jsdi.Property;
import org.eclipse.wst.jsdt.debug.core.jsdi.StringValue;
import org.eclipse.wst.jsdt.debug.core.jsdi.UndefinedValue;
import org.eclipse.wst.jsdt.debug.core.jsdi.Value;
import org.eclipse.wst.jsdt.debug.core.jsdi.VirtualMachine;
import org.eclipse.wst.jsdt.debug.core.model.IJavaScriptValue;
import org.eclipse.wst.jsdt.debug.core.model.JavaScriptDebugModel;
import org.eclipse.wst.jsdt.debug.internal.core.JavaScriptDebugPlugin;

/**
 * Default implementation of {@link IValue}
 * 
 * @since 1.0
 */
public class JavaScriptValue extends JavaScriptDebugElement implements IJavaScriptValue {

	public static final IVariable[] NO_VARIABLES = new IVariable[0];
	
	/**
	 * the '[proto]' value
	 */
	public static final String PROTO = "[proto]"; //$NON-NLS-1$
	/**
	 * the 'constructor' value
	 */
	public static final String CONSTRUCTOR = "constructor"; //$NON-NLS-1$
	/**
	 * The 'this' value
	 */
	public static final String THIS = "this"; //$NON-NLS-1$
	
	private Value value;
	private List properties;

	/**
	 * Constructor
	 * 
	 * @param target
	 * @param value
	 */
	public JavaScriptValue(JavaScriptDebugTarget target, Value value) {
		super(target);
		this.value = value;
	}
	
	/**
	 * Hook to return the underlying value for this {@link JavaScriptValue}
	 * 
	 * @return the underlying {@link Value}
	 */
	protected Value getUnderlyingValue() {
		return this.value;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.core.model.IJavaScriptValue#getDetailString()
	 */
	public String getDetailString() {
		if (this.value instanceof ArrayReference) {
			ArrayReference array = (ArrayReference) this.value;
			List values = array.getValues();
			Iterator iterator = values.iterator();
			if (! iterator.hasNext())
			    return "[]"; //$NON-NLS-1$
			StringBuffer buffer = new StringBuffer();
			buffer.append('[');
			for (;;) {
				Value jsValue = (Value) iterator.next();
				buffer.append(getValueString(jsValue));
				if (! iterator.hasNext())
					return buffer.append(']').toString();
				buffer.append(", "); //$NON-NLS-1$				
			}
		}

		if (this.value instanceof JavaScriptPrimitiveValue) {
			JavaScriptPrimitiveValue nvalue = (JavaScriptPrimitiveValue)this.value;
			return nvalue.stringValue();
		}
		if(this.value instanceof FunctionReference) {
			FunctionReference f = (FunctionReference) this.value;
			String src = f.functionBody();
			if(src != null) {
				CodeFormatter formatter = ToolFactory.createCodeFormatter(null);
				TextEdit edit = formatter.format(CodeFormatter.K_JAVASCRIPT_UNIT, src, 0, src.length(), 0, null);
				if(edit != null) {
					Document doc = new Document(src);
					try {
						edit.apply(doc);
						return doc.get();
					} catch (Exception e) {
						JavaScriptDebugPlugin.log(e);
					} 
				}
			}
			return f.valueString();
		}
		if(this.value != null) {
			return this.value.valueString();
		}
		return UNDEFINED;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IValue#getReferenceTypeName()
	 */
	public String getReferenceTypeName() throws DebugException {
		if (this.value != null) {
			if (this.value instanceof ObjectReference) {
				if (this.value instanceof NullValue) {
				return NULL;
				} else if (this.value instanceof ArrayReference) {
					return ARRAY;
				}else if (this.value instanceof FunctionReference) {
					return FUNCTION;
				}
				return OBJECT;
			} else if (this.value instanceof BooleanValue) {
				return BOOLEAN;
			} else if (this.value instanceof UndefinedValue) {
				return UNDEFINED;
			} else if (this.value instanceof NumberValue) {
				return NUMBER;
			} else if (this.value instanceof StringValue) {
				return STRING;
			}
		}
		return UNKNOWN;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IValue#getValueString()
	 */
	public String getValueString() throws DebugException {
		return getValueString(value);
	}
	
	private static String getValueString(Value jsValue) {
		if (jsValue instanceof ObjectReference) {
			ObjectReference ref = (ObjectReference) jsValue;
			StringBuffer buffer = new StringBuffer();
			Number id = ref.id();
			if(id == null) {
				buffer.append(jsValue.valueString());
			}
			else {
				buffer.append(NLS.bind(ModelMessages.JavaScriptValue_object_value_label, new String[] {jsValue.valueString(), id.toString()}));
			}
			return buffer.toString();
		}
		if (jsValue instanceof NumberValue) {
			NumberValue nvalue = (NumberValue) jsValue;
			if (!nvalue.isNaN()) {
				return JavaScriptDebugModel.numberToString(nvalue.value());
			}
		}
		if (jsValue instanceof StringValue) {
			StringBuffer buffer = new StringBuffer();
			buffer.append('"');
			buffer.append(jsValue.valueString().replaceAll("\"", "\\\\\"")); //$NON-NLS-1$//$NON-NLS-2$
			buffer.append('"');
			return buffer.toString();
		}
		
		return jsValue.valueString();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IValue#getVariables()
	 */
	public synchronized IVariable[] getVariables() throws DebugException {
		if (!hasVariables()) {
			return NO_VARIABLES;
		}
		if (this.properties == null) {
			final ObjectReference reference = (ObjectReference) this.value;
			List underlyingProperties = reference.properties();
			this.properties = new ArrayList(underlyingProperties.size()+1);
			for (Iterator iterator = underlyingProperties.iterator(); iterator.hasNext();) {
				Property property = (Property) iterator.next();
				JavaScriptProperty jsdiProperty = new JavaScriptProperty(this, property);
				this.properties.add(jsdiProperty);
			}
			
			// sort - array values (e.g integer names) first then alphabetically
			Collections.sort(properties, new Comparator() {		
				public int compare(Object arg0, Object arg1) {
					try {		
					String name0 = ((IVariable) arg0).getName();
					String name1 = ((IVariable) arg1).getName();
					
					if (Character.isDigit(name0.charAt(0))) {
						if (Character.isDigit(name1.charAt(0))) 
							return Integer.valueOf(name0).compareTo(Integer.valueOf(name1));
						return -1;
					}
					if (Character.isDigit(name1.charAt(0)))
						return 1;

					return name0.compareToIgnoreCase(name1);
					} catch (DebugException e) {
						return 0;
					}
				}
			});
			
			// add the prototype
			Property prototype = new Property() {
				public VirtualMachine virtualMachine() {
					return reference.virtualMachine();
				}
				public String name() {
					return PROTO;
				}
				public Value value() {
					return reference.prototype();
				}
			};
			properties.add(0, new JavaScriptProperty(this, prototype));
			
			//add the constructor
			Property constructor = new Property() {
				public VirtualMachine virtualMachine() {
					return reference.virtualMachine();
				}
				public String name() {
					return CONSTRUCTOR; 
				}
				public Value value() {
					return reference.constructor();
				}
			};
			properties.add(1, new JavaScriptProperty(this, constructor));
		}
		return (IVariable[]) this.properties.toArray(new IVariable[this.properties.size()]);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IValue#hasVariables()
	 */
	public boolean hasVariables() throws DebugException {
		if (this.value instanceof ObjectReference) {
			return !(this.value instanceof NullValue);
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.debug.core.model.IValue#isAllocated()
	 */
	public boolean isAllocated() throws DebugException {
		return this.properties != null;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.wst.jsdt.debug.core.model.IJavaScriptValue#isNull()
	 */
	public boolean isNull() {
		return this.value instanceof NullValue;
	}
	
	/**
	 * Delegate method to create the proper kind of {@link IJavaScriptValue}
	 * 
	 * @param target the target to create the value for
	 * @param value the value to wrap
	 * @return a new {@link IJavaScriptValue}
	 */
	public static IJavaScriptValue createValue(JavaScriptDebugTarget target, Value value) {
		if(value instanceof BooleanValue ||
				value instanceof NullValue ||
				value instanceof NumberValue) {
			return new JavaScriptPrimitiveValue(target, value);
		}
		return new JavaScriptValue(target, value);
	}
}
