/*
 * 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.dqp.internal.process;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.teiid.adminapi.Admin;
import org.teiid.adminapi.impl.CacheStatisticsMetadata;
import org.teiid.cache.Cachable;
import org.teiid.cache.Cache;
import org.teiid.cache.CacheFactory;
import org.teiid.common.buffer.TupleBufferCache;
import org.teiid.core.util.Assertion;
import org.teiid.core.util.EquivalenceUtil;
import org.teiid.core.util.HashCodeUtil;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.logging.MessageLevel;
import org.teiid.metadata.AbstractMetadataRecord;
import org.teiid.metadata.AbstractMetadataRecord.DataModifiable;
import org.teiid.metadata.FunctionMethod.Determinism;
import org.teiid.query.parser.ParseInfo;
import org.teiid.vdb.runtime.VDBKey;


/**
 * This class is used to cache session aware objects
 */
public class SessionAwareCache<T> {
	public static final String REPL = "-repl"; //$NON-NLS-1$
	public static final int DEFAULT_MAX_SIZE_TOTAL = 512;
	public enum Type {
		RESULTSET,
		PREPAREDPLAN;
	}

	private Cache<CacheID, T> localCache;
	private Cache<CacheID, T> distributedCache;
	
	private long modTime;
	private Type type;
	
	private AtomicInteger cacheHit = new AtomicInteger();
	private AtomicInteger totalRequests = new AtomicInteger();
	private AtomicInteger cachePuts = new AtomicInteger();
	
	private TupleBufferCache bufferManager;
	
	public SessionAwareCache (String cacheName, final CacheFactory cacheFactory, final Type type, int maxStaleness) {
		assert (cacheFactory != null);
		
		this.localCache = cacheFactory.get(cacheName); 
		
		if (type == Type.PREPAREDPLAN) {
			this.distributedCache = localCache;
		}
		else {
			this.distributedCache = cacheFactory.get(cacheName+REPL); 
			if (this.distributedCache == null && this.localCache != null) {
				this.distributedCache = this.localCache;
			}
		}
		this.modTime = maxStaleness * 1000;
		this.type = type;
		
		assert (this.localCache != null);
		assert (this.distributedCache != null);
	}	
	
	public T get(CacheID id){

		this.totalRequests.getAndIncrement();
		
		id.setSessionId(id.originalSessionId);
		T result = localCache.get(id);
		
		if (result == null) {
			id.setSessionId(null);
			
			id.setUserName(id.originalUserName);
			result = distributedCache.get(id);
			
			if (result == null) {
				id.setUserName(null);
				result = distributedCache.get(id);
			}
			
			if (result instanceof Cachable) {
				Cachable c = (Cachable)result;
				if (!c.restore(this.bufferManager)) {
					result = null;
				}
			}
		}
		
		if (result != null) {
			if (result instanceof Cachable) {
				Cachable c = (Cachable)result;
				AccessInfo info = c.getAccessInfo();
				if (info != null && !info.validate(type == Type.RESULTSET, modTime)) {
					LogManager.logTrace(LogConstants.CTX_DQP, "Invalidating cache entry", id); //$NON-NLS-1$
					if (id.getSessionId() == null) {
						this.distributedCache.remove(id);
					} else {
						this.localCache.remove(id);
					}
					return null;
				}
			}
			LogManager.logTrace(LogConstants.CTX_DQP, "Cache hit for", id); //$NON-NLS-1$
			cacheHit.getAndIncrement();
		} else {
			LogManager.logTrace(LogConstants.CTX_DQP, "Cache miss for", id); //$NON-NLS-1$
		}
		return result;
	}
	
	public int getCacheHitCount() {
		return cacheHit.get();
	}
		
	public int getRequestCount() {
		return this.totalRequests.get();
	}
	
	public int getCachePutCount() {
		return cachePuts.get();
	}
	
	public int getTotalCacheEntries() {
		if (this.localCache == this.distributedCache) {
			return this.localCache.size();
		}
		return localCache.size() + distributedCache.size();
	}
	
	public T remove(CacheID id, Determinism determinismLevel){
		if (determinismLevel.compareTo(Determinism.SESSION_DETERMINISTIC) <= 0) {
			id.setSessionId(id.originalSessionId);
			LogManager.logTrace(LogConstants.CTX_DQP, "Removing from session/local cache", id); //$NON-NLS-1$
			return this.localCache.remove(id);
		} 
		id.setSessionId(null);
		
		if (determinismLevel == Determinism.USER_DETERMINISTIC) {
			id.setUserName(id.originalUserName);
		}
		else {
			id.setUserName(null);
		}
		
		LogManager.logTrace(LogConstants.CTX_DQP, "Removing from global/distributed cache", id); //$NON-NLS-1$
		return this.distributedCache.remove(id);
	}
	
	public void put(CacheID id, Determinism determinismLevel, T t, Long ttl){
		cachePuts.incrementAndGet();
		if (determinismLevel.compareTo(Determinism.SESSION_DETERMINISTIC) <= 0) {
			id.setSessionId(id.originalSessionId);
			LogManager.logTrace(LogConstants.CTX_DQP, "Adding to session/local cache", id); //$NON-NLS-1$
			ttl = computeTtl(id, t, ttl);
			if (ttl != null && ttl == 0) {
				return;
			}
			this.localCache.put(id, t, ttl);
		} 
		else {
			
			boolean insert = true;
			
			id.setSessionId(null);
			
			if (determinismLevel == Determinism.USER_DETERMINISTIC) {
				id.setUserName(id.originalUserName);
			}
			else {
				id.setUserName(null);
			}
			
			if (t instanceof Cachable) {
				Cachable c = (Cachable)t;
				ttl = computeTtl(id, t, ttl);
				if (ttl != null && ttl == 0) {
					return;
				}
				insert = c.prepare(this.bufferManager);
			}
			
			if (insert) {
				LogManager.logTrace(LogConstants.CTX_DQP, "Adding to global/distributed cache", id); //$NON-NLS-1$
				this.distributedCache.put(id, t, ttl);
			}
		}
	}

	Long computeTtl(CacheID id, T t, Long ttl) {
		if (!(t instanceof Cachable) || type != Type.RESULTSET) {
			return ttl;
		}
		Cachable c = (Cachable)t;
		AccessInfo info = c.getAccessInfo();
		if (info == null) {
			return ttl;
		}
		Set<Object> objects = info.getObjectsAccessed();
		if (objects == null) {
			return ttl;
		}
		long computedTtl = Long.MAX_VALUE;
		for (Object o : objects) {
			if (!(o instanceof AbstractMetadataRecord)) {
				continue;
			}
			AbstractMetadataRecord amr = (AbstractMetadataRecord)o;
			Long l = getDataTtl(amr);
			if (l == null || l < 0) {
				continue;
			}
			if (l == 0) {
				if (LogManager.isMessageToBeRecorded(LogConstants.CTX_DQP, MessageLevel.DETAIL)) {
					LogManager.logDetail(LogConstants.CTX_DQP, "Not adding cache entry", id, "since", amr.getFullName(), "has caching disabled"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				}
				return Long.valueOf(0);
			}
			computedTtl = Math.min(l, computedTtl);
		}
		if (ttl != null) {
			ttl = Math.min(ttl, computedTtl);
		} else if (computedTtl != Long.MAX_VALUE) {
			ttl = computedTtl;
		}
		return ttl;
	}
	
	static Long getDataTtl(AbstractMetadataRecord record) {
		String value = record.getProperty(DataModifiable.DATA_TTL, false);
		if (value != null) {
			try {
				return Long.valueOf(value);
			} catch (NumberFormatException e) {
				if (LogManager.isMessageToBeRecorded(LogConstants.CTX_RUNTIME, MessageLevel.DETAIL)) {
					LogManager.logDetail(LogConstants.CTX_RUNTIME, "Invalid data ttl specified for ", record.getFullName()); //$NON-NLS-1$
				}
			}
		}
		if (record.getParent() != null) {
			return getDataTtl(record.getParent());
		}
		return null;
	}
	
	/**
	 * Clear all the cached plans for all the clientConns
	 * @param clientConn ClientConnection
	 */
	public void clearAll(){
		this.localCache.clear();
		this.distributedCache.clear();
		this.totalRequests.set(0);
		this.cacheHit.set(0);
		this.cachePuts.set(0);
	}	
	
	public void clearForVDB(String vdbName, String version) {
		VDBKey vdbKey = new VDBKey(vdbName, version);
		clearForVDB(vdbKey);
	}

	public void clearForVDB(VDBKey vdbKey) {
		clearCache(this.localCache, vdbKey);
		clearCache(this.distributedCache, vdbKey);
	}
	
	private void clearCache(Cache<CacheID, T> cache, VDBKey vdbKey) {
		Set<CacheID> keys = cache.keySet();
		for (CacheID key:keys) {
			if (key.vdbInfo.equals(vdbKey)) {
				cache.remove(key);
			}
		}
	}
	
	public static class CacheID implements Serializable {
		private static final long serialVersionUID = 8261905111156764744L;
		private String sql;
		private VDBKey vdbInfo;
		private boolean ansiIdentifiers;
		private String sessionId;
		private String originalSessionId;
		private List<Serializable> parameters;
		private String userName;
		private String originalUserName;
		
		public CacheID(DQPWorkContext context, ParseInfo pi, String sql){
			this(pi, sql, context.getVdbName(), context.getVdbVersion(), context.getSessionId(), context.getUserName());
		}
		
		public CacheID(ParseInfo pi, String sql, String vdbName, String vdbVersion, String sessionId, String userName){
			Assertion.isNotNull(sql);
			this.sql = sql;
			this.vdbInfo = new VDBKey(vdbName, vdbVersion);
			this.ansiIdentifiers = pi.ansiQuotedIdentifiers;
			this.originalSessionId = sessionId;
			this.originalUserName = userName;
		}

		
		public String getSessionId() {
			return sessionId;
		}
		
		public String getUserName() {
			return userName;
		}
		
		private void setSessionId(String sessionId) {
			this.sessionId = sessionId;
		}
		
		/**
		 * Set the raw (non-Constant) parameter values.
		 * @param parameters
		 * @return
		 */
		public boolean setParameters(List<?> parameters) {
			if (parameters !=  null && !parameters.isEmpty()) {
				this.parameters = new ArrayList<Serializable>(parameters.size());
				for (Object obj:parameters) {
					if (obj == null) {
						this.parameters.add(null);
						continue;
					}
					if (!(obj instanceof Serializable)) {
						return false;
					}
					this.parameters.add((Serializable)obj);
				}
			}
			return true;
		}
		
		void setUserName(String name) {
			this.userName = name;
		}
		
		public VDBKey getVDBKey() {
			return vdbInfo;
		}
						
		public boolean equals(Object obj){
	        if(obj == this) {
	            return true;
	        } 
	        if(! (obj instanceof CacheID)) {
	            return false;
	        } 
        	CacheID that = (CacheID)obj;
            return ansiIdentifiers == that.ansiIdentifiers && this.vdbInfo.equals(that.vdbInfo) && this.sql.equals(that.sql) 
            	&& EquivalenceUtil.areEqual(this.userName, that.userName)            	
            	&& EquivalenceUtil.areEqual(this.sessionId, that.sessionId)
            	&& EquivalenceUtil.areEqual(this.parameters, that.parameters);
		}
		
	    public int hashCode() {
	        return HashCodeUtil.hashCode(0, vdbInfo, sql, this.userName, sessionId, parameters);
	    }
	    
	    @Override
	    public String toString() {
	    	return "Cache Entry<" + originalSessionId + "="+ originalUserName + "> params:" + parameters + " sql:" + sql; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	    }
	    
	}
	
    public void setTupleBufferCache(TupleBufferCache bufferManager) {
    	this.bufferManager = bufferManager;
    }
    
    public void setModTime(long modTime) {
		this.modTime = modTime;
	}
    
    public static Collection<String> getCacheTypes(){
    	ArrayList<String> caches = new ArrayList<String>();
    	caches.add(Admin.Cache.PREPARED_PLAN_CACHE.toString());
    	caches.add(Admin.Cache.QUERY_SERVICE_RESULT_SET_CACHE.toString());
    	return caches;
    }    
    
	public boolean isTransactional() {
		return this.localCache.isTransactional() || this.distributedCache.isTransactional();
	}
	
	public CacheStatisticsMetadata buildCacheStats(String name) {
		CacheStatisticsMetadata stats = new CacheStatisticsMetadata();
		stats.setName(name);
		stats.setHitRatio(this.getRequestCount() == 0?0:((double)this.getCacheHitCount()/this.getRequestCount())*100);
		stats.setTotalEntries(this.getTotalCacheEntries());
		stats.setRequestCount(this.getRequestCount());
		return stats;
	}
}
