package railo.runtime.type;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map.Entry;

import railo.commons.lang.SizeOf;
import railo.runtime.PageContext;
import railo.runtime.converter.LazyConverter;
import railo.runtime.dump.DumpData;
import railo.runtime.dump.DumpProperties;
import railo.runtime.dump.DumpTable;
import railo.runtime.dump.DumpUtil;
import railo.runtime.dump.SimpleDumpData;
import railo.runtime.exp.ExpressionException;
import railo.runtime.exp.PageException;
import railo.runtime.op.Caster;
import railo.runtime.op.Duplicator;
import railo.runtime.op.ThreadLocalDuplication;
import railo.runtime.type.it.EntryIterator;
import railo.runtime.type.it.KeyIterator;
import railo.runtime.type.it.StringIterator;
import railo.runtime.type.util.ArraySupport;
import railo.runtime.type.util.ArrayUtil;



/**
 * CFML array object
 */
public final class ArrayImplNS extends ArraySupport implements Array,Sizeable {
	
	private Object[] arr;
	private int dimension=1;
	private final int cap=32;
	private int size=0;
	private int offset=0;
	private int offCount=0;
	
	/**
	 * constructor with definiton of the dimension
	 * @param dimension dimension goes from one to 3
	 * @throws ExpressionException
	 */
	public ArrayImplNS(int dimension) throws ExpressionException {
		if(dimension>3 || dimension<1)
			throw new ExpressionException("Array Dimension must be between 1 and 3");
		this.dimension=dimension;
		arr=new Object[offset+cap];
	}
	
	/**
	 * constructor with default dimesnion (1)
	 */
	public ArrayImplNS() {
		arr=new Object[offset+cap];
	}
	
	/**
	 * constructor with to data to fill
	 * @param objects Objects array data to fill
	 */
	public ArrayImplNS(Object[] objects) {
		arr=objects;
		size=arr.length;
		offset=0;
	}
	
	/**
	 * return dimension of the array
	 * @return dimension of the array
	 */
	public int getDimension() {
		return dimension;
	}
	
	@Override
	public Object get(String key) throws ExpressionException {
        return getE(Caster.toIntValue(key));
	}	

	@Override
	public Object get(Collection.Key key) throws ExpressionException {
        return getE(Caster.toIntValue(key.getString()));
	}	
	
	@Override
	public Object get(String key, Object defaultValue) {
		double index=Caster.toIntValue(key,Integer.MIN_VALUE);
		if(index==Integer.MIN_VALUE) return defaultValue;
	    return get((int)index,defaultValue);
	}	
	
	@Override
	public Object get(Collection.Key key, Object defaultValue) {
		double index=Caster.toIntValue(key.getString(),Integer.MIN_VALUE);
		if(index==Integer.MIN_VALUE) return defaultValue;
	    return get((int)index,defaultValue);
	}		

	@Override
	public Object get(int key, Object defaultValue) {
		if(key>size || key<1) {
			if(dimension>1) {
				ArrayImplNS ai = new ArrayImplNS();
				ai.dimension=dimension-1;
				return setEL(key,ai);
			}
			return defaultValue;
		}
		Object o=arr[(offset+key)-1];
		
		if(o==null) {
			if(dimension>1) {
				ArrayImplNS ai = new ArrayImplNS();
				ai.dimension=dimension-1;
				return setEL(key,ai);
			}
			return defaultValue;
		}
		return o;
	}
	
	@Override
	public Object getE(int key) throws ExpressionException {
		if(key<1) {
			throw invalidPosition(key);
		}
		else if(key>size) {
			if(dimension>1)return setE(key,new ArrayImplNS(dimension-1));
			throw invalidPosition(key);
		}
		
		Object o=arr[(offset+key)-1];
		
		if(o==null) {
			if(dimension>1) return setE(key,new ArrayImplNS(dimension-1));
			throw invalidPosition(key);
		}
		return o;
	}
	
	/**
	 * Exception method if key doesn't exist at given position
	 * @param pos
	 * @return exception
	 */
	private ExpressionException invalidPosition(int pos) {
		return new ExpressionException("Element at position ["+pos+"] doesn't exist in array");
	}
	
	@Override
	public Object setEL(String key, Object value) {
		try {
			return setEL(Caster.toIntValue(key), value);
		} catch (ExpressionException e) {
			return null;
		}
	}
	
	@Override
	public Object setEL(Collection.Key key, Object value) {
		try {
			return setEL(Caster.toIntValue(key.getString()), value);
		} catch (ExpressionException e) {
			return null;
		}
	}
	
	@Override
	public Object set(String key, Object value) throws ExpressionException {
		return setE(Caster.toIntValue(key),value);
	}
	
	@Override
	public Object set(Collection.Key key, Object value) throws ExpressionException {
		return setE(Caster.toIntValue(key.getString()),value);
	}

	@Override
	public Object setEL(int key, Object value) {
		if(offset+key>arr.length)enlargeCapacity(key);
		if(key>size)size=key;
		arr[(offset+key)-1]=checkValueEL(value);
		return value;
	}
	
	/**
	 * set value at defined position
	 * @param key 
	 * @param value
	 * @return defined value
	 * @throws ExpressionException
	 */
	public Object setE(int key, Object value) throws ExpressionException {
		if(offset+key>arr.length)enlargeCapacity(key);
		if(key>size)size=key;
		arr[(offset+key)-1]=checkValue(value);
		return value;		
	}	
	
	/**
     * !!! all methods that use this method must be sync
	 * enlarge the inner array to given size
	 * @param key min size of the array
	 */
	private void enlargeCapacity(int key) {
		int diff=offCount-offset;
		
		int newSize=arr.length;
		if(newSize<1) newSize=1;
		while(newSize<key+offset+diff) {
			newSize*=2;
		}
		if(newSize>arr.length) {
			Object[] na=new Object[newSize];
			for(int i=offset;i<offset+size;i++) {
				na[i+diff]=arr[i];
			}
			arr=na;
			offset+=diff;
		}
	}
	
	/**
	 * !!! all methods that use this method must be sync
     * enlarge the offset if 0
	 */
	private void enlargeOffset() {
		if(offset==0) {
			offCount=offCount==0?1:offCount*2;
			offset=offCount;
			Object[] narr=new Object[arr.length+offset];
			for(int i=0;i<size;i++) {
				narr[offset+i]=arr[i];
			}
			arr=narr;
		}
	}

	/**
	 * !!! all methods that use this method must be sync
     * check if value is valid to insert to array (to a multidimesnional array only array with one smaller dimension can be inserted)
	 * @param value value to check
	 * @return checked value
	 * @throws ExpressionException
	*/
	private Object checkValue(Object value) throws ExpressionException {
		// is a 1 > Array
		if(dimension>1)	{
			if(value instanceof Array)	{
				if(((Array)value).getDimension()!=dimension-1)
					throw new ExpressionException("You can only Append an Array with "+(dimension-1)+" Dimension","array has wrong dimension, now is "+(((Array)value).getDimension())+ " but it must be "+(dimension-1));
			}
			else 
				throw new ExpressionException("You can only Append an Array with "+(dimension-1)+" Dimension","now is a object of type "+Caster.toClassName(value));
		}
		return value;
	}
	
	/**
	 * !!! all methods that use this method must be sync
     * check if value is valid to insert to array (to a multidimesnional array only array with one smaller dimension can be inserted), if value is invalid return null;
	 * @param value value to check
	 * @return checked value
	*/
	private Object checkValueEL(Object value) {
		// is a 1 > Array
		if(dimension>1)	{
			if(value instanceof Array)	{
				if(((Array)value).getDimension()!=dimension-1)
					return null;
			}
			else 
				return null;
		}
		return value;
	}

	@Override
	public int size() {
		return size;
	}
	
	@Override
	public Collection.Key[] keys() {
		
		ArrayList<Collection.Key> lst=new ArrayList<Collection.Key>();
		int count=0;
		for(int i=offset;i<offset+size;i++) {
			Object o=arr[i];
			count++;
			if(o!=null) lst.add(KeyImpl.getInstance(count+""));
		}
		return  lst.toArray(new Collection.Key[lst.size()]);
	}
	
	@Override
	public int[] intKeys() {
		ArrayList<Integer> lst=new ArrayList<Integer>();		
		int count=0;
		for(int i=offset;i<offset+size;i++) {
			Object o=arr[i];
			count++;
			if(o!=null) lst.add(Integer.valueOf(count));
		}

		int[] ints=new int[lst.size()];
		
		for(int i=0;i<ints.length;i++){
			ints[i]=lst.get(i).intValue();
		}
		return ints;
	}
	
	@Override
	public Object remove(Collection.Key key) throws ExpressionException {
		return removeE(Caster.toIntValue(key.getString()));
	}

	public Object removeEL(Collection.Key key) {
		return removeEL(Caster.toIntValue(key.getString(),-1));
	}
	
	@Override
	public Object removeE(int key) throws ExpressionException {
		if(key>size || key<1) throw invalidPosition(key);
		Object obj=get(key,null);
		for(int i=(offset+key)-1;i<(offset+size)-1;i++) {
			arr[i]=arr[i+1];
		}
		size--;
		return obj;
	}
	
	@Override
	public Object removeEL(int key) {
	    if(key>size || key<1) return null;
		Object obj=get(key,null);
	    
		for(int i=(offset+key)-1;i<(offset+size)-1;i++) {
			arr[i]=arr[i+1];
		}
		size--;
		return obj;
	}
	
	@Override
	public void clear() {
	    if(size()>0) {
			arr=new Object[cap];
			size=0;
			offCount=1;
			offset=0;
	    }
	}
	
	@Override
	public boolean insert(int key, Object value) throws ExpressionException {
		if(key<1 || key>size+1) {
			throw new ExpressionException("can't insert value to array at position "+key+", array goes from 1 to "+size());
		}
		// Left
		if((size/2)>=key) {
			enlargeOffset();
			for(int i=offset;i<(offset+key)-1;i++) {
				arr[i-1]=arr[i];
			}
			offset--;
			arr[(offset+key)-1]=checkValue(value);
			size++;
		}
		// Right
		else {
			if((offset+key)>arr.length || size+offset>=arr.length)enlargeCapacity(arr.length+2);
			for(int i=size+offset;i>=key+offset;i--) {
				arr[i]=arr[i-1];
			}
			arr[(offset+key)-1]=checkValue(value);
			size++;
			
		}
		return true;
	}

	@Override
    public Object append(Object o) throws ExpressionException {
        if(offset+size+1>arr.length)enlargeCapacity(size+1);
        arr[offset+size]=checkValue(o);
        size++;
        return o;
    }
    
	/**
	 * append a new value to the end of the array
	 * @param o value to insert
	 * @return inserted value
	 */
    public Object appendEL(Object o) {
        
        if(offset+size+1>arr.length)enlargeCapacity(size+1);
        arr[offset+size]=o;
        size++;
        return o;
    }
    
    /**
     * adds a value and return this array
     * @param o
     * @return this Array
     */
    public boolean add(Object o) {
        if(offset+size+1>arr.length)enlargeCapacity(size+1);
        arr[offset+size]=o;
        size++;
        return true;
    }

	/**
	 * append a new value to the end of the array
	 * @param str value to insert
	 * @return inserted value
	 */
	public String _append(String str) {
		if(offset+size+1>arr.length)enlargeCapacity(size+1);
		arr[offset+size]=str;
		size++;
		return str;
	}
	
	/**
	 * add a new value to the begin of the array
	 * @param o value to insert
	 * @return inserted value
	 * @throws ExpressionException
	 */
	public Object prepend(Object o) throws ExpressionException {
		insert(1,o);
		return o;
	}
	
	/**
	 * resize array to defined size
	 * @param to new minimum size of the array
	 */
	public void resize(int to) {
		if(to>size) {
			enlargeCapacity(to);
			size=to;
		}
	}
	
	@Override
	public void sort(String sortType, String sortOrder) throws PageException {
		sort(ArrayUtil.toComparator(null, sortType, sortOrder, false));
	}

	@Override
	public synchronized void sort(Comparator comp) throws PageException {
		if(getDimension()>1)
			throw new ExpressionException("only 1 dimensional arrays can be sorted");
		Arrays.sort(arr,offset,offset+size,comp);	
	}
	
	/**
	 * @return return arra as native (Java) Object Array
	 */
	public Object[] toArray() {
		Object[] rtn=new Object[size];
		int count=0;
		for(int i=offset;i<offset+size;i++) {
			rtn[count++]=arr[i];
		}
		
		return rtn;
	}

	/**
	 * @return return array as ArrayList
	 */
	public ArrayList toArrayList() {
		ArrayList al=new ArrayList();
		for(int i=offset;i<offset+size;i++) {
			al.add(arr[i]);
		}
		return al;
	}
	
	@Override
	public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties dp) {
		DumpTable table = new DumpTable("array","#ff9900","#ffcc00","#000000");
		table.setTitle("Array");
		
		int length=size();
		maxlevel--;
		for(int i=1;i<=length;i++) {
			Object o=null;
			try {
				o = getE(i);
			} 
			catch (Exception e) {}
			table.appendRow(1,new SimpleDumpData(i),DumpUtil.toDumpData(o, pageContext,maxlevel,dp));
		}
		return table;
	}
	
	/**
	 * return code print of the array as plain text
	 * @return content as string
	*/
	public String toPlain() {
		
		StringBuffer sb=new StringBuffer();
		int length=size();
		for(int i=1;i<=length;i++) {
			sb.append(i);
			sb.append(": ");
			sb.append(get(i-1,null));
			sb.append("\n");
		}
		return sb.toString();
	}
	
	@Override
	public Object clone() {
		return duplicate(true);
	}
	
	@Override
	public Collection duplicate(boolean deepCopy) {
		ArrayImplNS arr=new ArrayImplNS();
		arr.dimension=dimension;
		Collection.Key[] keys=this.keys();
		
		ThreadLocalDuplication.set(this, arr);
		try {
			Collection.Key k;
			for(int i=0;i<keys.length;i++) {
				k=keys[i];
				arr.set(k,Duplicator.duplicate(this.get(k,null),deepCopy));
			}
		} 
		catch (ExpressionException e) {}
		finally{
			// ThreadLocalDuplication.remove(this); removed "remove" to catch sisters and brothers
		}
		
		return arr;
	}
	
	@Override
	public Iterator<Collection.Key> keyIterator() {
		return new KeyIterator(keys());
	}
    
	@Override
	public Iterator<String> keysAsStringIterator() {
    	return new StringIterator(keys());
    }
	
	@Override
	public Iterator<Entry<Key, Object>> entryIterator() {
		return new EntryIterator(this, keys());
	}
	
	public Iterator iterator() {
		ArrayList lst=new ArrayList();
		//int count=0;
		for(int i=offset;i<offset+size;i++) {
			Object o=arr[i];
			//count++;
			if(o!=null) lst.add(o);
		}
		return lst.iterator();
	}


	@Override
	public String toString() {
		return LazyConverter.serialize(this);
	}

	@Override
	public long sizeOf() {
		return SizeOf.size(arr)
		+SizeOf.size(dimension)
		+SizeOf.size(cap)
		+SizeOf.size(size)
		+SizeOf.size(offset)
		+SizeOf.size(offCount)
		+SizeOf.REF_SIZE;
	}
}