/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

package org.teiid.query.metadata;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.teiid.core.util.LRUCache;
import org.teiid.metadata.AbstractMetadataRecord.DataModifiable;
import org.teiid.metadata.AbstractMetadataRecord.Modifiable;
import org.teiid.query.mapping.relational.QueryNode;
import org.teiid.query.sql.lang.CacheHint;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.sql.symbol.Symbol;

/**
 * This class represents a temporary metadata ID.  A temporary metadata ID 
 * does not exist in a real metadata source.  Rather, it is used temporarily 
 * in context of processing a single query.  This metadata ID can be used to 
 * represent either a group or an element depending on the constructor used.
 * 
 * TODO: we should be using the real metadata objects, but internal and
 * designer legacy keep us on the temp framework
 */
public class TempMetadataID implements Serializable, Modifiable, DataModifiable {
    
	private static final long serialVersionUID = -1879211827339120135L;
	private static final int LOCAL_CACHE_SIZE = 8;
	
	private static final int MOD_COUNT_FOR_COST_UPDATE = 8;
	
	public static class TableData {
		Collection<TempMetadataID> accessPatterns;
		List<TempMetadataID> elements;
		long cardinality = QueryMetadataInterface.UNKNOWN_CARDINALITY;
		List<TempMetadataID> primaryKey;
		QueryNode queryNode;
		Map<Object, Object> localCache;
		volatile CacheHint cacheHint;
		long cacheHintUpdated;
		List<List<TempMetadataID>> keys;
		List<TempMetadataID> indexes;
		volatile long lastDataModification;
		volatile long lastModified = System.currentTimeMillis();
		int modCount;
		private LinkedHashMap<Expression, Integer> functionBasedExpressions;
		private Object model;
		
		public long getLastDataModification() {
			return lastDataModification;
		}
		
		public void removed() {
			this.lastModified = -1;
		}
		
		public void dataModified(long updateCount) {
			if (updateCount == 0) {
				return;
			}
			long ts = System.currentTimeMillis();
			modCount += updateCount;
			if (modCount > MOD_COUNT_FOR_COST_UPDATE) {
				this.lastModified = ts;
				modCount = 0;
			}
			this.lastDataModification = ts;
		}
		
		public long getLastModified() {
			return lastModified;
		}

		public void setFunctionBasedExpressions(
				LinkedHashMap<Expression, Integer> newExprs) {
			this.functionBasedExpressions = newExprs;
		}
		
		public LinkedHashMap<Expression, Integer> getFunctionBasedExpressions() {
			return functionBasedExpressions;
		}

		public void setModel(Object mid) {
			this.model = mid;
		}
		
		public Object getModel() {
			return model;
		}

		public synchronized boolean updateCacheHint(long time) {
			if (time >= cacheHintUpdated) {
				cacheHintUpdated = time;
				return true;
			}
			return false;
		}
		
	}
	
	private static TableData DUMMY_DATA = new TableData();
	
	public enum Type {
		VIRTUAL,
		TEMP,
		SCALAR,
		XML,
		INDEX
	}
	
    private String ID;      // never null, upper cased fully-qualified string
    private String name;
    private Type metadataType = Type.VIRTUAL;
    private Object originalMetadataID;
    
    private TableData data;
    
	//Column metadata
    private int position;
    private Class<?> type;     // type of this element, only for element
    private boolean autoIncrement;
    private boolean notNull;
    private boolean updatable;
    private boolean accessed;
    
    /**
     * Constructor for group form of metadata ID.
     * @param ID Fully-qualified, upper-case name of ID
     * @param elements List of TempMetadataID representing elements
     */
    public TempMetadataID(String ID, List<TempMetadataID> elements) {
        this(ID, elements, Type.VIRTUAL);
    }
    
    /**
     * Constructor for group form of metadata ID.
     * @param ID Fully-qualified, upper-case name of ID
     * @param elements List of TempMetadataID representing elements
     * @param isVirtual whether or not the group is a virtual group
     */
    public TempMetadataID(String ID, List<TempMetadataID> elements, Type type) {
        this.data = new TableData();
        this.ID = ID;
        this.data.elements = elements;
        int pos = 1;
        for (TempMetadataID tempMetadataID : elements) {
			tempMetadataID.setPosition(pos++);
		}
        this.name = ID;
        this.metadataType = type;
    }
    
    /**
     * Constructor for element form of metadata ID.
     * @param ID Fully-qualified, upper-case name of ID
     * @param type Type of elements List of TempMetadataID representing elements
     */
    public TempMetadataID(String ID, Class<?> type) { 
        this.ID = ID;
        this.type = type;
    }
    
    /**
     * Constructor for element form of metadata ID with the underlying element.
     * @param ID Fully-qualified, upper-case name of ID
     * @param type Type of elements List of TempMetadataID representing elements
     * @param metadataID the orginal metadataID
     */
    public TempMetadataID(String ID, Class<?> type, Object metadataID) { 
        this.ID = ID;
        this.type = type;
        this.originalMetadataID = metadataID;
    }
    
    @Override
    public long getLastDataModification() {
    	return getTableData().getLastDataModification();
    }
    
    @Override
    public long getLastModified() {
    	return getTableData().getLastModified();
    }

    /**
     * Get ID value 
     * @return ID value
     */
    public String getID() { 
        return this.ID;
    }
    
    /**
     * Get type - only valid for elements
     * @return Type for elements, null for groups
     */
    public Class<?> getType() { 
        return this.type;
    }
    
    /**
     * Get elements - only valid for groups
     * @return List of TempMetadataID for groups, null for elements
     */
    public List<TempMetadataID> getElements() { 
        return this.getTableData().elements;
    }
    
    /**
     * add a element to the temp table.
     * @param elem
     */
    protected void addElement(TempMetadataID elem) {
        if (this.getTableData().elements != null) {
            this.getTableData().elements.add(elem);
            elem.setPosition(this.getTableData().elements.size());
        }
        if (this.getTableData().localCache != null) {
        	this.getTableData().localCache.clear();
        }
    }

    /**
     * Check whether this group is virtual
     * @return True if virtual
     */
    public boolean isVirtual() {
        return metadataType == Type.VIRTUAL;    
    }
    
    /**
     * Whether it is a temporary table  
     * @return
     * @since 5.5
     */
    public boolean isTempTable() {
        return this.metadataType == Type.TEMP;
    }
    
    /**
     * Return string representation of ID
     * @return String representation
     */                
    public String toString() { 
        return ID;
    }
    
    /**
     * Compare this temp metadata ID with another object.
     * @return True if obj is another TempMetadataID with same ID value
     */
    public boolean equals(Object obj) { 
        if(this == obj) { 
            return true;
        }
        
        if(!(obj instanceof TempMetadataID)) { 
            return false;
        }
        return this.getID().equals( ((TempMetadataID) obj).getID());
    }       
    
    /**
     * Return hash code
     * @return Hash code value for object
     */
    public int hashCode() {
        return this.ID.hashCode();
    }
    
	public void setOriginalMetadataID(Object metadataId) {
        this.originalMetadataID = metadataId;
    }
    
    /** 
     * @return Returns the originalMetadataID.
     * @since 4.3
     */
    public Object getOriginalMetadataID() {
        return this.originalMetadataID;
    }

    public Collection<TempMetadataID> getAccessPatterns() {
        if (this.getTableData().accessPatterns == null) {
            return Collections.emptyList();
        }
        return this.getTableData().accessPatterns;
    }

    public void setAccessPatterns(Collection<TempMetadataID> accessPatterns) {
        this.getTableData().accessPatterns = accessPatterns;
    }

    public long getCardinality() {
        return this.getTableData().cardinality;
    }

    public void setCardinality(long cardinality) {
        this.getTableData().cardinality = cardinality;
    }

    public void setTempTable(boolean isTempTable) {
        if (isTempTable) {
        	this.metadataType = Type.TEMP;
        } else {
        	this.metadataType = Type.VIRTUAL;
        }
    }

    Object getProperty(Object key) {
    	if (this.getTableData().localCache != null) {
    		return this.getTableData().localCache.get(key);
    	}
    	return null;
    }
    
    Object setProperty(Object key, Object value) {
		if (this.getTableData().localCache == null) {
			this.getTableData().localCache = Collections.synchronizedMap(new LRUCache<Object, Object>(LOCAL_CACHE_SIZE));
    	}
		return this.getTableData().localCache.put(key, value);
    }

	public boolean isScalarGroup() {
		return this.metadataType == Type.SCALAR;
	}
	
	public void setMetadataType(Type metadataType) {
		this.metadataType = metadataType;
	}
	
	public Type getMetadataType() {
		return metadataType;
	}

	public List<TempMetadataID> getPrimaryKey() {
		return getTableData().primaryKey;
	}
	
	public void setPrimaryKey(List<TempMetadataID> primaryKey) {
		this.getTableData().primaryKey = primaryKey;
	}
	
	public int getPosition() {
		return position;
	}
	
	public void setPosition(int position) {
		this.position = position;
	}
	
	public QueryNode getQueryNode() {
		return getTableData().queryNode;
	}
	
	public void setQueryNode(QueryNode queryNode) {
		this.getTableData().queryNode = queryNode;
	}
	
	public CacheHint getCacheHint() {
		return getTableData().cacheHint;
	}
	
	public void setCacheHint(CacheHint cacheHint) {
		this.getTableData().cacheHint = cacheHint;
	}
	
	public List<TempMetadataID> getIndexes() {
		return getTableData().indexes;
	}
	
	public void addIndex(Object originalMetadataId, List<TempMetadataID> index) {
		if (this.getTableData().indexes == null) {
			this.getTableData().indexes = new LinkedList<TempMetadataID>();
		}
		TempMetadataID id = new TempMetadataID(ID, Collections.EMPTY_LIST, Type.INDEX);
		id.getTableData().elements = index;
		id.setOriginalMetadataID(originalMetadataId);
		this.getTableData().indexes.add(id);
	}
	
	public List<List<TempMetadataID>> getUniqueKeys() {
		return getTableData().keys;
	}
	
	public void addUniqueKey(List<TempMetadataID> key) {
		if (this.getTableData().keys == null) {
			this.getTableData().keys = new LinkedList<List<TempMetadataID>>();
		}
		this.getTableData().keys.add(key);
	}

	public TableData getTableData() {
		if (data == null) {
			return DUMMY_DATA;
		}
		return data;
	}

	public boolean isAutoIncrement() {
		return autoIncrement;
	}
	
	public void setAutoIncrement(boolean autoIncrement) {
		this.autoIncrement = autoIncrement;
	}

	public boolean isNotNull() {
		return notNull;
	}
	
	public void setNotNull(boolean notNull) {
		this.notNull = notNull;
	}
	
	public void setUpdatable(boolean updatable) {
		this.updatable = updatable;
	}
	
	public boolean isUpdatable() {
		return updatable;
	}

	public String getName() {
		if (this.name == null) {
			this.name = Symbol.getShortName(this.ID);
		}
		return this.name;
	}
	
	public void setAccessed(boolean accessed) {
		this.accessed = accessed;
	}
	
	public boolean isAccessed() {
		return accessed;
	}
	
}
