package railo.runtime.type;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import railo.commons.lang.SizeOf;
import railo.runtime.PageContext;
import railo.runtime.config.NullSupportHelper;
import railo.runtime.dump.DumpData;
import railo.runtime.dump.DumpProperties;
import railo.runtime.dump.DumpUtil;
import railo.runtime.engine.ThreadLocalPageContext;
import railo.runtime.exp.DatabaseException;
import railo.runtime.exp.DeprecatedException;
import railo.runtime.exp.ExpressionException;
import railo.runtime.exp.PageException;
import railo.runtime.exp.PageRuntimeException;
import railo.runtime.op.Caster;
import railo.runtime.op.Duplicator;
import railo.runtime.op.Operator;
import railo.runtime.op.ThreadLocalDuplication;
import railo.runtime.op.date.DateCaster;
import railo.runtime.reflection.Reflector;
import railo.runtime.reflection.pairs.MethodInstance;
import railo.runtime.type.dt.DateTime;
import railo.runtime.type.it.EntryIterator;
import railo.runtime.type.it.KeyIterator;
import railo.runtime.type.it.StringIterator;
import railo.runtime.type.scope.Undefined;
import railo.runtime.type.util.CollectionUtil;
import railo.runtime.type.util.QueryUtil;
import railo.runtime.util.ArrayIterator;

/**
 * implementation of the query column
 */
public class QueryColumnImpl implements QueryColumnPro,Sizeable,Objects {

	private static final long serialVersionUID = -5544446523204021493L;
	private static final int CAPACITY=32;
    
	protected int type;
	protected int size;
	protected Object[] data;
    
    protected boolean typeChecked=false;
    protected QueryImpl query;
    protected Collection.Key key;

	/**
	 * constructor with type
	 * @param query
	 * @param key 
	 * @param type
	 */
    public QueryColumnImpl(QueryImpl query, Collection.Key key, int type) {
		this.data=new Object[CAPACITY];
		this.type=type;
        this.key=key;
        this.query=query;
	}

	/**
	 * constructor with array
	 * @param query
	 * @param array
	 * @param type
	 */
    public QueryColumnImpl(QueryImpl query, Collection.Key key, Array array,int type) {
		data=array.toArray();
	    size=array.size();
		this.type=type;
		this.query=query;
		this.key=key;
	}

	/**
	 * @param query
	 * @param type type as (java.sql.Types.XYZ) int
	 * @param size 
	 */
    public QueryColumnImpl(QueryImpl query, Collection.Key key, int type, int size) {
		this.data=new Object[size];
		this.type=type;
		this.size=size;
		this.query=query;
		this.key=key;
	}
	

	/**
	 * Constructor of the class
	 * for internal usage only
	 */
	public QueryColumnImpl() {
	}

	@Override
	public int size() {
		return size;
	}

	@Override
	public Collection.Key[] keys() {
		Collection.Key[] k=new Collection.Key[size()];
        int len=k.length;
		for(int i=1;i<=len;i++) {
			k[i-1]=KeyImpl.init(Caster.toString(i));
		}
		return k;
	}

	@Override
	public Object remove(Collection.Key key) throws PageException {
		resetType();
		return set(Caster.toIntValue(key.getString()),"");
	}

	@Override
	public Object remove(int row) throws DatabaseException {
        // query.disconnectCache();
        resetType();
		return set(row,"");
	}
	

	@Override
	public Object removeEL(Collection.Key key) {
        // query.disconnectCache();
        resetType();
		return setEL(Caster.toIntValue(key.getString(),-1),"");
	}

	@Override
	public Object removeEL(int row) {
        // query.disconnectCache();
        resetType();
		return setEL(row,"");
	}

	@Override
	public synchronized void clear() {
        // query.disconnectCache();
        resetType();
		data=new Object[CAPACITY];
		size=0;
	}
	
	@Override
	public Object remove(PageContext pc) throws PageException {
        return remove(query.getCurrentrow(pc.getId()));
	}

	public Object removeEL(PageContext pc) {
        return removeEL(query.getCurrentrow(pc.getId()));
	}
	
	@Override
	public Object get(String key) throws PageException {
        return get(KeyImpl.init(key));
	}
	
	@Override
	public Object get(Key key) throws PageException {
		return get((PageContext)null,key);
	}

	@Override
	public Object get(PageContext pc, Collection.Key key) throws PageException {
		int row=Caster.toIntValue(key.getString(),Integer.MIN_VALUE);
		if(row==Integer.MIN_VALUE) {
			Object child=getChildElement(pc,key,NullSupportHelper.NULL());
	    	if(child!=NullSupportHelper.NULL()) return child;
            throw new DatabaseException("key ["+key+"] not found",null,null,null);
        }
	    return QueryUtil.getValue(this,row);
	}

    private Object getChildElement(PageContext pc,Key key, Object defaultValue) {// pc maybe null
    	// column and query has same name
    	if(key.equals(this.key)) {
        	return query.get(key,defaultValue);
    	}
    	// get it from undefined scope
		pc = ThreadLocalPageContext.get(pc);
		if(pc!=null){
			Undefined undefined = pc.undefinedScope();
			boolean old = undefined.setAllowImplicidQueryCall(false);
			Object sister = undefined.get(this.key,NullSupportHelper.NULL());
			undefined.setAllowImplicidQueryCall(old);
			if(sister!=NullSupportHelper.NULL()){
				try {
					return pc.get(sister, key);
				} catch (PageException e) {
					return defaultValue;
				}
			}
		}
    	return defaultValue;
	}

    /**
     * touch the given line on the column at given row
     * @param row
     * @return new row or existing
     * @throws DatabaseException
     */
    public Object touch(int row) {
    	if(row<1 || row>size) return NullSupportHelper.full()?null:"";
        Object o=data[row-1];
        if(o!=null) return o;
        return setEL(row,new StructImpl());
    }
    
    /**
     * touch the given line on the column at given row
     * @param row
     * @return new row or existing
     * @throws DatabaseException
     */
    public Object touchEL(int row) {
    	return touch(row);
    }
    
	@Override
	public Object get(Key key, Object defaultValue) {
		return get(null,key,defaultValue);
	}

	@Override
	public Object get(PageContext pc, Collection.Key key, Object defaultValue) {// pc maybe null
	    int row=Caster.toIntValue(key.getString(),Integer.MIN_VALUE);
	    if(row==Integer.MIN_VALUE) {
	    	return getChildElement(pc,key, defaultValue);
	    }
	    return get(row,defaultValue);
	}

	@Override
	public Object get(String key, Object defaultValue) {
	    return get(KeyImpl.init(key),defaultValue);
	}

	@Override
    public Object get(int row) throws DeprecatedException {
		throw new DeprecatedException("this method is no longer supported, use instead get(int,Object)");
		//return QueryUtil.getValue(this,row);
    }

	@Override
    public Object get(int row, Object emptyValue) {
    	if(row<1 || row>size) return emptyValue;
    	return data[row-1]==null?emptyValue:data[row-1];
	}

	@Override
	public Object set(String key, Object value) throws PageException {
	    int row=Caster.toIntValue(key,Integer.MIN_VALUE);
	    if(row==Integer.MIN_VALUE)return query.set(key,value);
	    return set(row,value);
	}

	public Object set(Collection.Key key, Object value) throws PageException {
	    int row=Caster.toIntValue(key.getString(),Integer.MIN_VALUE);
	    if(row==Integer.MIN_VALUE)return query.set(key,value);
	    return set(row,value);
	}

    @Override
	public synchronized Object set(int row, Object value) throws DatabaseException {
        // query.disconnectCache();
        if(row<1) throw new DatabaseException("invalid row number ["+row+"]","valid row numbers a greater or equal to one",null,null);
	    if(row>size) {
	    	if(size==0)throw new DatabaseException("cannot set a value to a empty query, you first have to add a row",null,null,null);
	    	throw new DatabaseException("invalid row number ["+row+"]","valid row numbers goes from 1 to "+size,null,null);
	    }
	    
	    value=reDefineType(value);
	    data[row-1]=value;
	    return value;
	}
	@Override
	public synchronized Object setEL(String key, Object value) {
	    int index=Caster.toIntValue(key,Integer.MIN_VALUE);
		if(index==Integer.MIN_VALUE) query.setEL(key,value);
	    return setEL(index, value);
		
	}

	public Object setEL(Collection.Key key, Object value) {
	    int index=Caster.toIntValue(key.getString(),Integer.MIN_VALUE);
		if(index==Integer.MIN_VALUE) query.setEL(key,value);
	    return setEL(index, value);
	}

	@Override
	public synchronized Object setEL(int row, Object value) {
        // query.disconnectCache();
        if(row<1 || row>size) return value;
	    
	    value=reDefineType(value);
	    data[row-1]=value;
	    return value;
	}

    @Override
	public synchronized void add(Object value) {
        // query.disconnectCache();
        if(data.length<=size) growTo(size);
	    data[size++]=value;
	}

    @Override
    public synchronized void cutRowsTo(int maxrows) {
        // query.disconnectCache();
        if(maxrows>-1 && maxrows<size)size=maxrows;
    }

	@Override
	public synchronized void addRow(int count) {	    
        // query.disconnectCache();
        if(data.length<(size+count)) growTo(size+count);
	    for(int i=0;i<count;i++)size++;
	}

    public synchronized Object removeRow(int row) throws DatabaseException {
        // query.disconnectCache();
        if(row<1 || row>size) 
            throw new DatabaseException("invalid row number ["+row+"]","valid rows goes from 1 to "+size,null,null);
        Object o=data[row-1];
        for(int i=row;i<size;i++) {
            data[i-1]=data[i];
        }
        size--;
        if(NullSupportHelper.full()) return o;
        return o==null?"":o;
    }

	@Override
	public int getType() {
	    reOrganizeType();
		return type;
	}
	
	
    @Override
	public String getTypeAsString() {
		return QueryImpl.getColumTypeName(getType());
	}


	@Override
	public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties dp) {
		return DumpUtil.toDumpData(QueryUtil.getValue(this,query.getCurrentrow(pageContext.getId())), pageContext,maxlevel,dp);
	}	
	
    private synchronized void growTo(int row) {
        
        int newSize=(data.length+1)*2;
        while(newSize<=row) {
            //print.ln(newSize+"<="+row);
            newSize*=2;
        }
        
        Object[] newData=new Object[newSize];
        for(int i=0;i<data.length;i++) {
            newData[i]=data[i];
        }
        data=newData;
    }
    
    private Object reDefineType(Object value) {
        return QueryColumnUtil.reDefineType(this,value);
    }
    
    private synchronized void resetType() {
        QueryColumnUtil.resetType(this);
    }
    
    private synchronized void reOrganizeType() {
        QueryColumnUtil.reOrganizeType(this);
    }

    public Collection.Key getKey() {
        return key;
    }
    public void setKey(Collection.Key key) {
        this.key = key;
    }

	@Override
	public String getKeyAsString() throws PageException {
		return key.getLowerString();// TODO ist das OK?
	}

    @Override
    public Object get(PageContext pc) {
        return QueryUtil.getValue(this,query.getCurrentrow(pc.getId()));
    }
    
    @Override
    public Object get(PageContext pc, Object defaultValue) {
        return get(query.getCurrentrow(pc.getId()),defaultValue);
    }

    @Override
    public Object touch(PageContext pc) throws PageException {
        return touch(query.getCurrentrow(pc.getId()));
    }

    public Object touchEL(PageContext pc) {
        return touchEL(query.getCurrentrow(pc.getId()));
    }

    @Override
    public Object set(PageContext pc, Object value) throws PageException {
        return set(query.getCurrentrow(pc.getId()),value);
    }

    @Override
    public Object setEL(PageContext pc, Object value) {
        return setEL(query.getCurrentrow(pc.getId()),value);
    }

    @Override
    public Object getParent() {
        return query;
    }

    @Override
    public String castToString() throws PageException {
        return Caster.toString(get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),null));
    }

	@Override
	public String castToString(String defaultValue) {
		Object value = get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),NullSupportHelper.NULL());
		if(value==NullSupportHelper.NULL()) return defaultValue;
		return Caster.toString(value,defaultValue);
	}

    @Override
    public boolean castToBooleanValue() throws PageException {
        return Caster.toBooleanValue(get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),null));
    }
    
    @Override
    public Boolean castToBoolean(Boolean defaultValue) {
    	Object value = get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),NullSupportHelper.NULL());
		if(value==NullSupportHelper.NULL()) return defaultValue;
		return Caster.toBoolean(value,defaultValue);
    }

    @Override
    public double castToDoubleValue() throws PageException {
        return Caster.toDoubleValue(get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),null));
    }
    
    @Override
    public double castToDoubleValue(double defaultValue) {
    	Object value = get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),NullSupportHelper.NULL());
		if(value==NullSupportHelper.NULL()) return defaultValue;
		return Caster.toDoubleValue(value,defaultValue);
    }

    @Override
    public DateTime castToDateTime() throws PageException {
        return DateCaster.toDateAdvanced(get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),null),null);
    }
    
    @Override
    public DateTime castToDateTime(DateTime defaultValue) {
    	Object value = get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),NullSupportHelper.NULL());
		if(value==NullSupportHelper.NULL()) return defaultValue;
		return DateCaster.toDateAdvanced(value,true,null,defaultValue);
    }


	@Override
	public int compareTo(boolean b) throws PageException {
		return Operator.compare(castToBooleanValue(), b);
	}

	@Override
	public int compareTo(DateTime dt) throws PageException {
		return Operator.compare((Date)castToDateTime(), (Date)dt);
	}

	@Override
	public int compareTo(double d) throws PageException {
		return Operator.compare(castToDoubleValue(), d);
	}

	@Override
	public int compareTo(String str) throws PageException {
		return Operator.compare(castToString(), str);
	}

    @Override
    public synchronized Object clone() {
        return duplicate(true);
    }

    public synchronized Collection duplicate(boolean deepCopy) {
        return cloneColumn(query,deepCopy);
    }
    
    public synchronized QueryColumnPro cloneColumn(Query query, boolean deepCopy) {
        return cloneColumnImpl(deepCopy);
    }
    
    public synchronized QueryColumnImpl cloneColumnImpl(boolean deepCopy) {
        QueryColumnImpl clone=new QueryColumnImpl();
        populate(this, clone, deepCopy);
        return clone;
    }
    
    protected static void populate(QueryColumnImpl src,QueryColumnImpl trg, boolean deepCopy) {
        
        ThreadLocalDuplication.set(src, trg);
        try{
	        trg.key=src.key;
	        trg.query=src.query;
	        trg.size=src.size;
	        trg.type=src.type;
	        trg.key=src.key;
	        
	        trg.data=new Object[src.data.length];
	        for(int i=0;i<src.data.length;i++) {
	            trg.data[i]=deepCopy?Duplicator.duplicate(src.data[i],true):src.data[i];
	        }
        }
        finally {
        	// ThreadLocalDuplication.remove(src); removed "remove" to catch sisters and brothers
        }
    }
	

    @Override
    public String toString() {
        try {
            return Caster.toString(get(query.getCurrentrow(ThreadLocalPageContext.get().getId()),null));
        } catch (PageException e) {
            return super.toString();
        }
    }

    @Override
    public boolean containsKey(String key) {
        return containsKey(KeyImpl.init(key));
    }

	@Override
	public boolean containsKey(Collection.Key key) {
        return get(key,NullSupportHelper.NULL())!=NullSupportHelper.NULL();
	}

	@Override
	public long sizeOf() {
		return SizeOf.size(key)+SizeOf.size(data);
	}
	
	public Iterator iterator() {
		return keyIterator();
	}

	@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());
	}

	@Override
	public Iterator<Object> valueIterator() {
		return new ArrayIterator(data,0,size);
	}
	
	@Override
	public Object callWithNamedValues(PageContext pc, Key methodName,Struct args) throws PageException {
		
        throw new ExpressionException("No matching Method/Function ["+methodName+"] for call with named arguments found");
		//return pc.getFunctionWithNamedValues(get(query.getCurrentrow()), methodName, Caster.toFunctionValues(args));
	}

	@Override
	public Object call(PageContext pc, Key methodName, Object[] arguments) throws PageException {
		MethodInstance mi = Reflector.getMethodInstanceEL(this,this.getClass(), methodName, arguments);
		if(mi!=null) {
			try {
				return mi.invoke(this);
			} catch (Throwable t) {
				try {
					return pc.getFunction(QueryUtil.getValue(this,query.getCurrentrow(pc.getId())), methodName, arguments);
				} catch (PageException pe) {
					throw Caster.toPageException(t);
				}
			}
		}
		return pc.getFunction(QueryUtil.getValue(this,query.getCurrentrow(pc.getId())), methodName, arguments);
	}

	@Override
	public Object set(PageContext pc, Key propertyName, Object value) throws PageException {
		return set(propertyName, value);
	}

	@Override
	public Object setEL(PageContext pc, Key propertyName, Object value) {
		return setEL(propertyName, value);
	}

	public void add(int index, Object element) {
		throwNotAllowedToAlter();
		//setEL(index+1, element);
	}

	private void throwNotAllowedToAlter() {
		throw new PageRuntimeException(new DatabaseException(
				"Query columns do not support methods that would alter the structure of a query column" 
				,"you must use an analogous method on the query"
				,null
				,null));
		
	}

	public boolean addAll(java.util.Collection<? extends Object> c) {
		throwNotAllowedToAlter();
		return false;
		/*Iterator<? extends Object> it = c.iterator();
		while(it.hasNext()){
			add(it.next());
		}
		return true;*/
	}

	public boolean addAll(int index, java.util.Collection<? extends Object> c) {
		throwNotAllowedToAlter();
		return false;
		/*Iterator<? extends Object> it = c.iterator();
		while(it.hasNext()){
			setEL(++index,it.next());
		}
		return true;*/
	}

	public boolean contains(Object o) {
		return indexOf(o)!=-1;
	}

	public boolean containsAll(java.util.Collection<?> c) {
		Iterator<? extends Object> it = c.iterator();
		while(it.hasNext()){
			if(indexOf(it.next())==-1) return false;
		}
		return true;
	}

	public int indexOf(Object o) {
		for(int i=0;i<size;i++){
			try {
				if(Operator.compare(o, data[i])==0) return i;
			} 
			catch (PageException e) {}
		}
		return -1;
	}

	public int lastIndexOf(Object o) {
		for(int i=size-1;i>=0;i--){
			try {
				if(Operator.compare(o, data[i])==0) return i;
			} 
			catch (PageException e) {}
		}
		return -1;
	}

	public boolean isEmpty() {
		return size()==0;
	}

	public boolean removeAll(java.util.Collection<?> c) {
		throwNotAllowedToAlter();
		return false;
		/*boolean hasChanged=false;
		Iterator<? extends Object> it = c.iterator();
		while(it.hasNext()){
			if(remove(it.next())) {
				hasChanged=true;
			}
		}
		return hasChanged;*/
	}

	public boolean retainAll(java.util.Collection<?> c) {
		throwNotAllowedToAlter();
		return false;
		/*boolean hasChanged=false;
		Iterator it = valueIterator();
		while(it.hasNext()){
			if(!c.contains(it.next())){
				hasChanged=true;
				it.remove();
			}
		}
		return hasChanged;*/
	}

	public List<Object> subList(int fromIndex, int toIndex) {
		ArrayList<Object> list=new ArrayList<Object>();
		for(int i=fromIndex;i<toIndex;i++){
			list.add(data[i]);
		}
		return list;
	}

	public Object[] toArray() {
		return toArray(new Object[size()]);
	}

	public  Object[] toArray(Object[] trg) {
		System.arraycopy(data, 0, trg, 0, data.length>trg.length?trg.length:data.length);
		return trg;
	}

	@Override
	public QueryColumnPro toDebugColumn() {
		return _toDebugColumn();
	}
	
	public DebugQueryColumn _toDebugColumn() {
		return new DebugQueryColumn(data,key,query,size,type,typeChecked);
	}
	
	@Override
	public java.util.Iterator<String> getIterator() {
    	return keysAsStringIterator();
    }
	
	@Override
	public boolean equals(Object obj){
		if(!(obj instanceof Collection)) return false;
		return CollectionUtil.equals(this,(Collection)obj);
	}

	@Override
	public int getDimension() {
		return 1;
	}

	@Override
	public Object getE(int row) throws PageException {
		return get(row);
	}

	@Override
	public Object setE(int key, Object value) throws PageException {
		return set(key, value);
	}

	@Override
	public int[] intKeys() {
		int[] keys=new int[size()];
        int len=keys.length;
		for(int i=1;i<=len;i++) {
			keys[i-1]=i;
		}
		return keys;
	}

	@Override
	public boolean insert(int key, Object value) throws PageException {
		throwNotAllowedToAlter();
		return false;
	}

	@Override
	public Object append(Object o) throws PageException {
		throwNotAllowedToAlter();
		return o;
	}

	@Override
	public Object appendEL(Object o) {
		throwNotAllowedToAlter();
		return o;
	}

	@Override
	public Object prepend(Object o) throws PageException {
		throwNotAllowedToAlter();
		return o;
	}

	@Override
	public void resize(int to) throws PageException {
		throwNotAllowedToAlter();
	}

	@Override
	public void sort(String sortType, String sortOrder) throws PageException {
		throwNotAllowedToAlter();
	}

	@Override
	public void sort(Comparator comp) throws PageException {
		throwNotAllowedToAlter();
	}

	@Override
	public List toList() {
		Iterator<Object> it = valueIterator();
		ArrayList list=new ArrayList();
        while(it.hasNext()){
        	list.add(it.next());
        }
        return list;
	}

	@Override
	public Object removeE(int key) throws PageException {
		throwNotAllowedToAlter();
		return null;
	}

	@Override
	public boolean containsKey(int key) {
		return get(key,NullSupportHelper.NULL())!=NullSupportHelper.NULL();
	}
	
	/*@Override
	public int hashCode() {
		return CollectionUtil.hashCode(this);
	}*/
}