/******************************************************************************
 * Product: Adempiere ERP & CRM Smart Business Solution                       *
 * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved.                *
 * This program is free software; you can redistribute it and/or modify it    *
 * under the terms version 2 of the GNU General Public License as published   *
 * by the Free Software Foundation. This program 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 General Public License for more details.                       *
 * You should have received a copy of the GNU General Public License along    *
 * with this program; if not, write to the Free Software Foundation, Inc.,    *
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.                     *
 * For the text or an alternative of this public license, you may reach us    *
 * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA        *
 * or via info@compiere.org or http://www.compiere.org/license.html           *
 *****************************************************************************/
package org.compiere.model;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.adempiere.util.ContextRunnable;
import org.compiere.Adempiere;
import org.compiere.util.CCache;
import org.compiere.util.CLogMgt;
import org.compiere.util.DB;
import org.compiere.util.DisplayType;
import org.compiere.util.Env;
import org.compiere.util.KeyNamePair;
import org.compiere.util.NamePair;
import org.compiere.util.Util;
import org.compiere.util.ValueNamePair;
/**
 *	An intelligent MutableComboBoxModel, which determines what can be cached.
 *  
 *      Validated   - SQL is final / not dynamic
 *      AllLoaded   - All Records are loaded
 *
 *		Get Info about Lookup
 *		-	SQL
 *		-	KeyColumn
 *		-	Zoom Target
 *   
 * 	@author 	Jorg Janke
 * 	@version 	$Id: MLookup.java,v 1.4 2006/10/07 00:58:57 jjanke Exp $
 */
public final class MLookup extends Lookup implements Serializable
{
	/**
	 * 
	 */
	private static final long serialVersionUID = 3339750658316918418L;
	/**
	 *  MLookup Constructor
	 *  @param info info
	 *  @param TabNo tab no
	 */
	public MLookup (MLookupInfo info, int TabNo)
	{
		super(info.DisplayType, info.WindowNo);
		m_info = info;
		m_tabNo = TabNo;
		if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn);
		//  Don't load Search or CreatedBy/UpdatedBy
		if (m_info.DisplayType == DisplayType.Search || m_info.DisplayType == DisplayType.SearchUU
			|| m_info.IsCreadedUpdatedBy)
			return;
		//  Don't load Parents/Keys
		if (m_info.IsParent || m_info.IsKey)
		{
			m_hasInactive = true;		//	creates focus listener for dynamic loading
			return;						//	required when parent needs to be selected (e.g. price from product)
		}
	}	//	MLookup
	/** Inactive Marker Start       */
	public static final String  INACTIVE_S = "~";
	/** Inactive Marker End         */
	public static final String  INACTIVE_E = "~";
	/**	Indicator for Null			*/
	private static Integer 		MINUS_ONE = Integer.valueOf(-1);
	/** The Lookup Info Value Object        */
	private MLookupInfo         m_info = null;
	private int 				m_tabNo = 0;
	/** Storage of data  Key-NamePair	*/
	private volatile LinkedHashMap	m_lookup = new LinkedHashMap();
	/** The Data Loader                 */
	private MLoader				m_loader;
	//
	/** All Data loaded                 */
	private boolean             m_allLoaded = false;
	/** Inactive records exists         */
	private boolean 		    m_hasInactive = false;
	/*  Refreshing    */
	private boolean             m_refreshing = false;
	/*  Refresh cache(if exists) */
	private boolean				m_refreshCache = false;
	/** Next Read for Parent			*/
	private long				m_nextRead = 0;
	/** Not in short List item Marker Start       IDEMPIERE 90 */
	public static final String  SHORTLIST_S = "*";
	/** Not in short List item Marker End         IDEMPIERE 90 */
	public static final String  SHORTLIST_E = "*";
	private boolean 		    m_hasShortListItems = false;	// IDEMPIERE 90
	private final static int MAX_NAMEPAIR_CACHE_SIZE = 1000;
	
	/**
	 *  Dispose
	 */
	public void dispose()
	{
		if (m_info != null)
			if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn + ": dispose");
		if (m_loaderFuture != null && !m_loaderFuture.isDone())
			m_loaderFuture.cancel(true);
		m_loader = null;
		m_loaderFuture = null;
		//
		if (m_lookup != null)
			m_lookup.clear();
		m_lookup = null;
		if (m_lookupDirect != null)
			m_lookupDirect.clear();
		m_lookupDirect = null;
		//
		m_info = null;
		//
		super.dispose();
	}   //  dispose
	/**
	 *  Wait until asynchronous Load Complete
	 */
	public void loadComplete()
	{
		if (m_loaderFuture != null && !m_loaderFuture.isDone())
		{
			try 
			{
				m_loaderFuture.get();
			}
			catch (Exception ie)
			{
				log.log(Level.SEVERE, m_info.KeyColumn + ": Interrupted", ie);
			}				
		    m_loader = null;
		    m_loaderFuture = null;
		}
	}   //  loadComplete
	/**
	 *	Get NamePair/KeyNamePair for key.
	 *  If not found return null.
	 *  @param key key value (Integer for Keys or String for Lists)
	 *  @return NamePair/KeyNamePair
	 */
	public NamePair get (Object key, boolean includeDirect)
	{
		if (key == null || MINUS_ONE.equals(key))	//	indicator for null
			return null;
		//auto refresh parent lookup
		if (m_info.IsParent )
		{
			if (m_nextRead > 0 && m_nextRead < System.currentTimeMillis()) 
			{
				m_lookup.clear();
				if (m_lookupDirect != null)
					m_lookupDirect.clear();				
			}			
			m_nextRead = System.currentTimeMillis() + 5000;	//	5 sec
		} 
		
		//	try cache
		NamePair retValue = (NamePair)m_lookup.get(key);
		if (retValue != null)
			return retValue;
		//	Not found and waiting for loader
		if (m_loaderFuture != null && !m_loaderFuture.isDone())
		{
			if (log.isLoggable(Level.FINER)) log.finer((m_info.KeyColumn==null ? "ID="+m_info.Column_ID : m_info.KeyColumn) + ": waiting for Loader");
			loadComplete();
			// try again after loading completed
			retValue = (NamePair)m_lookup.get(key);
			if (retValue != null)
				return retValue;
		}
		if (includeDirect) {
			//	Try to get it directly from DB
			boolean cacheLocal = m_info.IsValidated ; 
			return getDirect(key, false, cacheLocal);	//	do NOT cache	
		}
		return null;
	}	//	get
	/**
	 * Get NamePair/KeyNamePair for key.
	 * @param key
	 * @return NamePair/KeyNamePair or null
	 */
	public NamePair get(Object key) {
		return get(key, true);
	}
	/**
	 * Get NamePair/KeyNamePair for key. true if this object is the same as the obj
	 *          argument; false otherwise.
	 */
	@Override
	public boolean equals(Object obj)
	{
		if (obj instanceof MLookup)
		{
			MLookup ll = (MLookup)obj;
			if (ll.m_info.Column_ID == this.m_info.Column_ID)
				return true;
		}
		return false;
	}	//	equals
	
	@Override
	public int hashCode()
	{
	  assert false : "hashCode not designed";
	  return 42; // any arbitrary constant will do
	}
	/**
	 *	Return Size
	 *  @return size
	 */
	public int size()
	{
		return m_lookup.size();
	}	//	size
	/**
	 *	Is it all loaded
	 *  @return true if all loaded
	 */
	public boolean isAllLoaded()
	{
		return m_allLoaded;
	}	//	isAllLoaded
	/**
	 *	Is the List fully Validated
	 *  @return true if validated (i.e validation code have been parsed)
	 */
	@Override
	public boolean isValidated()
	{
		if (m_info == null)
			return false;
		return isValidated(m_info);
	}	//	isValidated
	/**
	 *  Get Validation SQL
	 *  @return Validation SQL
	 */
	@Override
	public String getValidation()
	{
		return m_info.ValidationCode;
	}   //  getValidation
	/**
	 *  Get Reference Value ID
	 *  @return AD_Reference_Value_ID
	 */
	public int getAD_Reference_Value_ID()
	{
		return m_info.AD_Reference_Value_ID;
	}   //  getAD_Reference_Value_ID
	/**
	 *  Has inactive elements in list
	 *  @return true if list contains inactive values
	 */
	@Override
	public boolean hasInactive()
	{
		return m_hasInactive;
	}   //  hasInactive
	/**
	 * @return AD_InfoWindow_ID
	 */
	public int getAD_InfoWindow_ID()
	{
		return m_info.InfoWindowId;
	}
	
	/**
	 *	Return data as ArrayList containing Value/KeyNamePair.
	 *  @param onlyValidated true to reload lookup data if validation code have not been parsed or needs re-parse
	 * 	@param loadParent true to load data for lookup with IsParent=true
	 *  @return List
	 */
	private ArrayList getData (boolean onlyValidated, boolean loadParent)
	{
		if (m_loaderFuture != null && !m_loaderFuture.isDone())
		{
			if (log.isLoggable(Level.FINE)) log.fine((m_info.KeyColumn==null ? "ID="+m_info.Column_ID : m_info.KeyColumn) 
				+ ": waiting for Loader");
			loadComplete();
		}
		//	Never Loaded (correctly)
		if (!m_allLoaded || m_lookup.size() == 0)
			loadData (loadParent);
		//	already validation included
		boolean validated = this.isValidated(m_info);
		if (validated)
			return new ArrayList(m_lookup.values());
		
		if (!validated && onlyValidated)
		{
			loadData (loadParent);
			if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn + ": Validated - #" + m_lookup.size());
		}
		return new ArrayList(m_lookup.values());
	}	//	getData
	/**
	 *	Return data as Array containing Value/KeyNamePair
	 *  @param mandatory if not mandatory, an additional empty value is inserted
	 *  @param onlyValidated only validated
	 *  @param onlyActive only active
	 * 	@param temporary force load for temporary display
	 *  @param shortlist
	 *  @return list
	 */
	@Override
	public ArrayList getData (boolean mandatory, boolean onlyValidated, boolean onlyActive, boolean temporary, boolean shortlist) // idempiere 90
	{
		//	create list
		ArrayList list = getData (onlyValidated, true);
		
		//  Remove inactive choices
		if (onlyActive && m_hasInactive)
		{
			//  list from the back
			for (int i = list.size(); i > 0; i--)
			{
				Object o = list.get(i-1);
				if (o != null)
				{
					String s = o.toString();
					if (s.startsWith(INACTIVE_S) && s.endsWith(INACTIVE_E))
						list.remove(i-1);
				}
			}
		}
		// Remove non short list items IDEMPIERE 90
		if (shortlist && m_hasShortListItems)
		{
			//  list from the back
			for (int i = list.size(); i > 0; i--)
			{
				Object o = list.get(i-1);
				if (o != null)
				{
					String s = o.toString();
					if (s.startsWith(SHORTLIST_S) && s.endsWith(SHORTLIST_E))
						list.remove(i-1);
				}
			}
		}
		// End Remove non short list items IDEMPIERE 90
		//	Add Optional (empty) selection
		if (!mandatory)
		{
			NamePair p = null;
			if (m_info.KeyColumn != null && m_info.KeyColumn.endsWith("_ID"))
				p = new KeyNamePair (-1, "");
			else
				p = new ValueNamePair ("", "");
			list.add(0, p);
		}
		return list;
	}	//	getData
	/**	Save getDirect last return value */	
	private HashMap	m_lookupDirect = null;
	private Future> m_loaderFuture;
	@Override
	public NamePair getDirect (Object key, boolean saveInCache, boolean cacheLocal)
	{
		return getDirect(key, saveInCache, cacheLocal, null);
	}	//	getDirect
	/**
	 *	Get Data Direct from DB.
	 *  @param key key
	 *  @param saveInCache true save in local lookup cache
	 * 	@param cacheLocal true to save in direct lookup cache
	 *  @return NamePair/KeyNamePir
	 */
	public NamePair getDirect (Object key, boolean saveInCache, boolean cacheLocal, String trxName)
	{
		//	Nothing to query
		if (key == null || m_info.QueryDirect == null || m_info.QueryDirect.length() == 0)
			return null;
		if (key.toString().trim().length() == 0)
			return null;
		//
		NamePair directValue = null;
		if (m_lookupDirect != null)		//	Lookup cache
		{
			directValue = (NamePair)m_lookupDirect.get(key);
			if (directValue != null)
				return directValue;
		}
		if (log.isLoggable(Level.FINER)) log.finer(m_info.KeyColumn + ": " + key 
				+ ", SaveInCache=" + saveInCache + ",Local=" + cacheLocal);
		
		String cacheKey = m_info.TableName+"|"+m_info.KeyColumn+"|"+m_info.AD_Reference_Value_ID+"|"+Env.getAD_Language(Env.getCtx());
		boolean isNumber = m_info.KeyColumn.endsWith("_ID");
		CCache knpCache = null;
		CCache vnpCache = null;
		if (isNumber)
		{
			knpCache = getDirectKeyNamePairCache(m_info, cacheKey);
			KeyNamePair knp = knpCache.get(Integer.parseInt(key.toString()));
			if (knp != null)
				return knp;
		}
		else
		{
			vnpCache = getDirectValueNamePairCache(m_info, cacheKey);
			ValueNamePair vnp = vnpCache.get(key.toString());
			if (vnp != null)
				return vnp;
		}
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			//	SELECT Key, Value, Name FROM ...
			pstmt = DB.prepareStatement(m_info.QueryDirect, trxName);
			if (isNumber)
				pstmt.setInt(1, Integer.parseInt(key.toString()));
			else
				pstmt.setString(1, key.toString());
			rs = pstmt.executeQuery();
			if (rs.next())
			{
				StringBuilder name = new StringBuilder().append(rs.getString(3));
				boolean isActive = rs.getString(4).equals("Y");
				if (!isActive)
				{
					name.insert(0, INACTIVE_S).append(INACTIVE_E);
				}
				if (isNumber)
				{
					int keyValue = rs.getInt(1);
					KeyNamePair p = new KeyNamePair(keyValue, name.toString());
					if (saveInCache)		//	save if
						m_lookup.put(Integer.valueOf(keyValue), p);
					directValue = p;
					knpCache.put(p.getKey(), p);
				}
				else
				{
					String value;
					if (m_info.KeyColumn.endsWith("_UU"))
						value = rs.getString(1);
					else
						value = rs.getString(2);
					ValueNamePair p = new ValueNamePair(value, name.toString());
					if (saveInCache)		//	save if
						m_lookup.put(value, p);
					directValue = p;
					vnpCache.put(p.getValue(), p);
				}
				if (rs.next()) {
					Level level = Level.SEVERE;
					if (MChangeLog.Table_Name.equals(m_info.TableName))
						level = Level.INFO;
					if (log.isLoggable(level))
						log.log(level, m_info.KeyColumn + ": Not unique (first returned) for " + key + " SQL=" + m_info.QueryDirect);
				}
			}
			else
			{
				directValue = null;
			}
			if (log.isLoggable(Level.FINEST)) log.finest(m_info.KeyColumn + ": " + directValue + " - " + m_info);
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, m_info.KeyColumn + ": SQL=" + m_info.QueryDirect + "; Key=" + key, e);
			directValue = null;
		}
		finally
		{
			DB.close(rs, pstmt);
			rs = null;
			pstmt = null;
		}
		//	Cache Local if not added to R/W cache
		if (cacheLocal  && !saveInCache && directValue != null)
		{
			if (m_lookupDirect == null)
			{
				m_lookupDirect = new HashMap();
			}
			else if (!m_lookupDirect.containsKey(key))
			{
				m_lookupDirect.clear();
				m_lookupDirect.put(key, directValue);
			}
		}
		m_hasInactive = true;
		return directValue;
	}	//	getDirect
	
	@Override
	public NamePair[] getDirect(Object[] keys) 
	{
		List list = new ArrayList();
		String cacheKey = m_info.TableName+"|"+m_info.KeyColumn+"|"+m_info.AD_Reference_Value_ID+"|"+Env.getAD_Language(Env.getCtx());
		boolean isNumber = m_info.KeyColumn.endsWith("_ID");
		CCache knpCache = null;
		CCache vnpCache = null;
		Map notInCaches = new HashMap();
		for (int i = 0; i < keys.length; i++)
		{
			Object key = keys[i];
			if (isNumber)
			{
				KeyNamePair knp = null;
				int id = Integer.parseInt(key.toString());
				knpCache = getDirectKeyNamePairCache(m_info, cacheKey);
				knp = knpCache.get(id);
				if (knp == null) 
				{
					knp = new KeyNamePair(id, null);
					notInCaches.put(id, i);
				}
				list.add(knp);				
			}
			else
			{
				ValueNamePair vnp = null;
				vnpCache = getDirectValueNamePairCache(m_info, cacheKey);
				vnp = vnpCache.get(key.toString());
				if (vnp == null)
				{
					vnp = new ValueNamePair(key.toString(), null);
					notInCaches.put(key.toString(), i);
				}
				list.add(vnp);				
			}			
		}
				
		if (notInCaches.size() > 0)
		{
			StringBuilder builder = new StringBuilder();
			for(int i = 0; i < notInCaches.size(); i++)
			{
				if (builder.length() > 0)
					builder.append(" UNION ALL ");
				builder.append(m_info.QueryDirect);
			}
			
			try (PreparedStatement pstmt = DB.prepareStatement(builder.toString(), null))
			{
				Set keySet = notInCaches.keySet();
				int i = 0;
				for(Object id : keySet)
				{
					i++;
					if (id instanceof Integer)
					{
						pstmt.setInt(i, (int) id);
					}
					else
					{
						pstmt.setString(i, id.toString());
					}
				}
				ResultSet rs = pstmt.executeQuery();
				while (rs.next())
				{
					StringBuilder name = new StringBuilder().append(rs.getString(3));
					boolean isActive = rs.getString(4).equals("Y");
					if (!isActive)
					{
						name.insert(0, INACTIVE_S).append(INACTIVE_E);
					}
					if (isNumber)
					{
						int keyValue = rs.getInt(1);
						KeyNamePair p = new KeyNamePair(keyValue, name.toString());
						knpCache.put(p.getKey(), p);
						Integer idx  = notInCaches.get(p.getKey());
						if (idx != null)
							list.set(idx.intValue(), p);
					}
					else
					{
						String value;
						if (m_info.KeyColumn.endsWith("_UU"))
							value = rs.getString(1);
						else
							value = rs.getString(2);
						ValueNamePair p = new ValueNamePair(value, name.toString());
						vnpCache.put(p.getValue(), p);
						Integer idx  = notInCaches.get(p.getValue());
						if (idx != null)
							list.set(idx.intValue(), p);
					}
				}
			} catch (SQLException e) {
				log.log(Level.SEVERE, e.getMessage(), e);
			}
			
			for(int i = list.size()-1; i >= 0; i--) 
			{
				NamePair np = list.get(i);
				if (np.getName() == null)
					list.remove(i);
			}
		}
		
		return list.toArray(new NamePair[0]);
	}
	/**
	 *	Get Zoom
	 *  @return Zoom AD_Window_ID
	 */
	public int getZoom()
	{
		return m_info.ZoomWindow;
	}	//	getZoom
	/**
	 *	Get Zoom
	 * 	@param query query
	 *  @return Zoom Window
	 */
	@Override
	public int getZoom(MQuery query)
	{
		if (m_info.ZoomWindowPO == 0 || query == null)
			return m_info.ZoomWindow;
		//	Need to check SO/PO
		boolean isSOTrx = DB.isSOTrx(m_info.TableName, query.getWhereClause(false), m_info.WindowNo);
		//
		return getZoom(isSOTrx);
	}	//	getZoom
	@Override
	public int getZoom(boolean isSOTrx)
	{
		if (m_info.ZoomWindowPO == 0)
			return m_info.ZoomWindow;
		
		return isSOTrx ? m_info.ZoomWindow : m_info.ZoomWindowPO;
	}
	
	/**
	 *	Get Zoom Query String
	 *  @return Zoom SQL Where Clause
	 */
	@Override
	public MQuery getZoomQuery()
	{
		return m_info.ZoomQuery;
	}	//	getZoom
	/**
	 *	Get underlying fully qualified Table.Column Name
	 *  @return Key Column
	 */
	@Override
	public String getColumnName()
	{
		return m_info.KeyColumn;
	}	//	g2etColumnName
	/**
	 *	Refresh and return number of items read.
	 *  @return no of items read
	 */
	@Override
	public int refresh ()
	{
		if (m_refreshing) return 0;
		return refresh(true);
	}	//	refresh
	/**
	 * @return number of items read
	 */
	public int refreshItemsAndCache()
	{
		if (m_refreshing) return 0;
		m_refreshCache = true;
		try
		{
			return refresh();
		} 
		finally 
		{
			m_refreshCache = false;
		}
	}
	
	/**
	 *	Refresh and return number of items read
	 * 	@param loadParent true to load data of lookup with IsParent=true
	 *  @return no of items refresh
	 */
	public int refresh (boolean loadParent)
	{
		if (m_refreshing) return 0;
		if (!loadParent && m_info.IsParent)
			return 0;
		//  Don't load Search or CreatedBy/UpdatedBy
		if (m_info.DisplayType == DisplayType.Search 
			|| m_info.IsCreadedUpdatedBy) 
		{
			//clear direct cache
			removeAllElements();
			return 0;
		}
		
		m_refreshing = true;
		try
		{
			//force refresh
			m_lookup.clear();
			MReference ref = m_info.AD_Reference_Value_ID > 0 ? MReference.get(Env.getCtx(),m_info.AD_Reference_Value_ID) : null;
			boolean onlyActive = ref == null || !ref.isShowInactiveRecords();
			fillComboBox(isMandatory(), true, onlyActive, false, isShortList()); // idempiere 90		
			return m_lookup.size();
		}
		finally
		{
			m_refreshing = false;
		}
	}	//	refresh
	/**
	 * Do the actual loading from database
	 * @param loadParent true to load data for lookup with IsParent=true
	 * @return number of records loaded
	 */
	private int loadData(boolean loadParent)
	{
		if (!loadParent && m_info.IsParent)
			return 0;
		//  Don't load Search or CreatedBy/UpdatedBy
		if (m_info.DisplayType == DisplayType.Search 
			|| m_info.IsCreadedUpdatedBy)
			return 0;
		if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn + ": start");
		
		m_loader = new MLoader();
		m_loaderFuture = Adempiere.getThreadPoolExecutor().submit(m_loader);
		loadComplete();
		if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn + ": #" + m_lookup.size());
		
		return m_lookup.size();
	}	//	refresh
	
	/**
	 * 	Remove All cached Elements
	 *	@see org.compiere.model.Lookup#removeAllElements()
	 */
	@Override
	public void removeAllElements()
	{
		super.removeAllElements ();
		m_lookup.clear();
		if (m_lookupDirect != null)
			m_lookupDirect.clear();
	}	//	removeAllElements
	
	/**
	 * @param info
	 * @return true if validation code have been parsed and doesn't need re-parse
	 */
	private boolean isValidated(MLookupInfo info)
	{
		if (info.IsValidated) return true;
		if (info.ValidationCode.length() == 0) return true;
		String validation = Env.parseContext(m_info.ctx, m_info.WindowNo, m_tabNo, m_info.ValidationCode, false);
		if (validation.equals(info.parsedValidationCode)) return true;
		return false;
	}
	
	/**
	 * @return lookup info
	 */
	public MLookupInfo getLookupInfo() 
	{
		return m_info;
	}
	
	private final static CCache>> s_keyNamePairCache = new CCache>>(null, "MLookup.KeyNamePairCache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, 500);
	private final static CCache>> s_valueNamePairCache = new CCache>>(null, "MLookup.ValueNamePairCache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, 500);
	
	private final static CCache> s_directKeyNamePairCache = new CCache>(null, "MLookup.DirectKeyNamePairCache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, 500);
	private final static CCache> s_directValueNamePairCache = new CCache>(null, "MLookup.DirectValueNamePairCache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, 500);
	
	private synchronized static List getKeyNamePairCache(MLookupInfo lookupInfo, String cacheKey) 
	{
		CCache> knpCache = s_keyNamePairCache.get(lookupInfo.TableName);
		if (knpCache == null)
		{
			knpCache = new CCache>(lookupInfo.TableName, cacheKey + " KeyNamePair Cache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, 500);
			s_keyNamePairCache.put(lookupInfo.TableName, knpCache);
		}
		List list = knpCache.get(cacheKey);
		if (list == null)
		{
			list = new ArrayList();
			knpCache.put(cacheKey, list);
		}
		return list;
	}
	
	private synchronized static List getValueNamePairCache(MLookupInfo lookupInfo, String cacheKey) 
	{
		CCache> vnpCache = s_valueNamePairCache.get(lookupInfo.TableName);
		if (vnpCache == null)
		{
			vnpCache = new CCache>(lookupInfo.TableName, cacheKey + " ValueNamePair Cache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, 500);
			s_valueNamePairCache.put(lookupInfo.TableName, vnpCache);
		}
		List list = vnpCache.get(cacheKey);
		if (list == null)
		{
			list = new ArrayList();
			vnpCache.put(cacheKey, list);
		}
		return list;
	}
	
	private synchronized static CCache getDirectKeyNamePairCache(MLookupInfo lookupInfo, String cacheKey)
	{
		CCache knpCache = s_directKeyNamePairCache.get(cacheKey);
		if (knpCache == null)
		{
			knpCache = new CCache(lookupInfo.TableName, cacheKey + " DirectKeyNamePairCache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, MAX_NAMEPAIR_CACHE_SIZE);
			s_directKeyNamePairCache.put(cacheKey, knpCache);
		}
		return knpCache;
	}
	
	private synchronized static CCache getDirectValueNamePairCache(MLookupInfo lookupInfo, String cacheKey)
	{
		CCache vnpCache = s_directValueNamePairCache.get(cacheKey);
		if (vnpCache == null)
		{
			vnpCache = new CCache(lookupInfo.TableName, cacheKey + " DirectValueNamePairCache", 100, CCache.DEFAULT_EXPIRE_MINUTE, false, MAX_NAMEPAIR_CACHE_SIZE);
			s_directValueNamePairCache.put(cacheKey, vnpCache);
		}
		return vnpCache;
	}
	
	/**
	 * Get Lookup
	 * @param tableID
	 * @param windowNo
	 * @param tabNo
	 * @return null if tableID <= 0 or the table doesn't have any key column, else {@link MLookup}
	 */
	public static MLookup getRecordsLookup(int tableID, int windowNo, int tabNo) {
		return getRecordsLookup(tableID, windowNo, tabNo, false);
	}
	
	/**
	 * Get Lookup
	 * @param tableID
	 * @param windowNo
	 * @param tabNo
	 * @param useUUIDKey - default false
	 * @return null if tableID <= 0 or the table doesn't have any key column, else {@link MLookup}
	 */
	public static MLookup getRecordsLookup(int tableID, int windowNo, int tabNo, boolean useUUIDKey) {
		if(tableID <= 0)	
			return null;
		MTable mTable = MTable.get(Env.getCtx(), tableID, null);
		
		// load key column
		String keyColumn = "";
		if(!useUUIDKey) {
			String[] keyColumns = mTable.getKeyColumns();
			// the table has a single key column
			if(keyColumns != null && keyColumns.length == 1) 
				keyColumn = keyColumns[0];
		}
		if(Util.isEmpty(keyColumn)) {
			keyColumn = PO.getUUIDColumnName(mTable.getTableName());
		}
		
		if(Util.isEmpty(keyColumn))
			return null;
		MColumn mColumn = MColumn.get(Env.getCtx(), mTable.getTableName(), keyColumn);
		MLookupInfo lookupInfo = MLookupFactory.getLookupInfo (Env.getCtx(), windowNo, tabNo, mColumn.getAD_Column_ID(), DisplayType.Search);
		return new MLookup(lookupInfo, tabNo);
	}
	/**
	 * Get Identifier String from AD_Table_ID and Record_ID
	 * @param tableID
	 * @param recordID
	 * @return String
	 */
	public static String getIdentifier(int tableID, Serializable recordID) {
		return getIdentifier(tableID, recordID, 0, 0);
	}
	
	/**
	 * Get Identifier String from AD_Table_ID and Record_ID
	 * @param tableID
	 * @param recordID
	 * @param windowNo
	 * @param tabNo
	 * @return String
	 */
	public static String getIdentifier(int tableID, Serializable recordID, int windowNo, int tabNo) {
		return getIdentifier(tableID, recordID, windowNo, tabNo, false);
	}
	
	/**
	 * Get Identifier String from AD_Table_ID and Record_ID
	 * @param tableID
	 * @param recordID
	 * @param windowNo
	 * @param tabNo
	 * @param useUUIDKey - default false
	 * @return String
	 */
	public static String getIdentifier(int tableID, Serializable recordID, int windowNo, int tabNo, boolean useUUIDKey) {
		MLookup lookup = getRecordsLookup(tableID, windowNo, tabNo, useUUIDKey);
		return lookup != null ? lookup.getDisplay(recordID) : "";
	}
	
	/**
	 *	Background Data Loader
	 */
	protected class MLoader extends ContextRunnable implements Serializable
	{
		/**
		 * 
		 */
		private static final long serialVersionUID = -5752931726580011885L;
		/**
		 * 	MLoader Constructor
		 */
		public MLoader()
		{
			super();
		}	//	Loader
		
		private long m_startTime = System.currentTimeMillis();
		/**
		 *	Load data
		 */
		protected void doRun()
		{
			/** Number of max rows to load	*/
			int	MAX_ROWS = MSysConfig.getIntValue(MSysConfig.MAX_ROWS_IN_TABLE_COMBOLIST, 10000, Env.getAD_Client_ID(Env.getCtx()));
			if (MAX_ROWS > 50000) {
				log.warning("SysConfig MAX_ROWS_IN_TABLE_COMBOLIST set back to maximum allowed value of 50.000");
				MAX_ROWS = 50000;  // impose hardcoded limit of 50.000
			}
			long startTime = System.currentTimeMillis();
			StringBuilder sql = new StringBuilder().append(m_info.Query);
			// IDEMPIERE 90
			if (isShortList())
			{
				// Adding ", IsShortList" to the sql SELECT clause
				int posFirstPoint = sql.indexOf(".");
				String tableName = sql.substring(7, posFirstPoint);
				int posFirstFrom = sql.indexOf(tableName+".IsActive FROM "+tableName) + tableName.length() + 9 ; // 9 = .IsActive
				String ClauseFromWhereOrder = sql.substring(posFirstFrom, sql.length());
				sql = new StringBuilder(sql.substring(0, posFirstFrom)  + ", " + tableName + ".IsShortList" + ClauseFromWhereOrder);				
			} // IDEMPIERE 90
			//	not validated
			if (!m_info.IsValidated)
			{
				String validation = Env.parseContext(m_info.ctx, m_info.WindowNo, m_tabNo, m_info.ValidationCode, false);
				m_info.parsedValidationCode = validation;
				if (validation.length() == 0 && m_info.ValidationCode.length() > 0)
				{
					if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn + ": Loader NOT Validated: " + m_info.ValidationCode);
					// Bug 1843862 - Lookups not working on Report Viewer window
					// globalqss - when called from Viewer window ignore error about not parseable context variables
					// there is no context in report viewer windows
					boolean isReportViewer = Env.getContext(m_info.ctx, m_info.WindowNo, "_WinInfo_IsReportViewer").equals("Y");
					if (!isReportViewer) {
						m_lookup.clear();
						return;
					}
				}
				else
				{					
					if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn + ": Loader Validated: " + validation);
					int posFrom = sql.lastIndexOf(" FROM ");
					boolean hasWhere = sql.indexOf(" WHERE ", posFrom) != -1;
					//
					int posOrder = sql.lastIndexOf(" ORDER BY ");
					if (posOrder != -1)
						sql = new StringBuilder(sql.substring(0, posOrder)) 
							.append((hasWhere ? " AND " : " WHERE ")) 
							.append(validation)
							.append(sql.substring(posOrder));
					else
						sql.append((hasWhere ? " AND " : " WHERE ")) 
							.append(validation);
					if (CLogMgt.isLevelFinest())
						if (log.isLoggable(Level.FINE)) log.fine(m_info.KeyColumn + ": Validation=" + validation);
				}
			}
			//	check
			if (Thread.interrupted())
			{
				log.log(Level.WARNING, m_info.KeyColumn + ": Loader interrupted");
				return;
			}
			//
			if (log.isLoggable(Level.FINER)) log.finer(m_info.Column_ID + ", " + m_info.KeyColumn + ": " + sql.toString());
			if (log.isLoggable(Level.FINEST)) log.finest(m_info.KeyColumn + ": " + sql);
			
			//	Reset
			m_lookup.clear();
			boolean isNumber = m_info.KeyColumn.endsWith("_ID");
			
			String cacheKey = sql.toString();
			List knpCache =  null;
			List vnpCache = null;
			if (isNumber) 
			{
				knpCache = getKeyNamePairCache(m_info, cacheKey);
				if (knpCache.size() > 0) 
				{
					if (m_refreshCache)
					{
						knpCache.clear();
					}
					else
					{
						for(KeyNamePair knp : knpCache) 
						{
							m_lookup.put(knp.getKey(), knp);
							String name = knp.getName();
							if (name.startsWith(INACTIVE_S) && name.endsWith(INACTIVE_E))
								m_hasInactive  = true;
						}
						return;
					}
				}
			} 
			else 
			{
				vnpCache = getValueNamePairCache(m_info, cacheKey);
				if (vnpCache.size() > 0)
				{
					if (m_refreshCache)
					{
						vnpCache.clear();
					}
					else
					{
						for(ValueNamePair vnp : vnpCache)
						{
							m_lookup.put(vnp.getValue(), vnp);
							String name = vnp.getName();
							if (name.startsWith(INACTIVE_S) && name.endsWith(INACTIVE_E))
								m_hasInactive  = true;
						}
						return;
					}
			
				}
			}
			
			m_hasInactive = false;
			int rows = 0;
			PreparedStatement pstmt = null;
			ResultSet rs = null;
			try
			{
				//	SELECT Key, Value, Name, IsActive FROM ...
				String sqlFirstRows = DB.getDatabase().addPagingSQL(sql.toString(), 1, MAX_ROWS+1);
				pstmt = DB.prepareStatement(sqlFirstRows, null);
				if (! DB.getDatabase().isPagingSupported())
					pstmt.setMaxRows(MAX_ROWS+1);
				int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, GridTable.DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx()));
				if (timeout > 0)
					pstmt.setQueryTimeout(timeout);
				rs = pstmt.executeQuery();
				//	Get first ... rows
				m_allLoaded = true;
				while (rs.next())
				{
					if (rows++ > MAX_ROWS)
					{
						logLookup(Level.WARNING, "Too many records");
						break;
					}
					//  check for interrupted every 20 rows
					if (rows % 20 == 0 && Thread.interrupted())
						break;
					//  load data
					StringBuilder name = new StringBuilder().append(rs.getString(3));
					boolean isActive = rs.getString(4).equals("Y");
					if (!isActive)
					{
						name.insert(0, INACTIVE_S).append(INACTIVE_E);
						m_hasInactive = true;
					}
					// IDEMPIERE 90
					if (isShortList())
					{
						boolean isShortListItem = rs.getString(5).equals("Y");
						if (!isShortListItem)
						{
							name = new StringBuilder(SHORTLIST_S).append(name).append(SHORTLIST_S);
							m_hasShortListItems = true;
						}
					}	
					// IDEMPIERE 90
					if (isNumber)
					{
						int key = rs.getInt(1);
						KeyNamePair p = new KeyNamePair(key, name.toString());
						m_lookup.put(Integer.valueOf(key), p);
						knpCache.add(p);
					}
					else
					{
						String value;
						if (m_info.KeyColumn.endsWith("_UU"))
							value = rs.getString(1);
						else
							value = rs.getString(2);
						ValueNamePair p = new ValueNamePair(value, name.toString());
						m_lookup.put(value, p);
						vnpCache.add(p);
					}
				}				
			}
			catch (SQLException e)
			{
				m_allLoaded = false;
				if (DB.getDatabase().isQueryTimeout(e))
					logLookup(Level.WARNING, "Too slow query");
				else
					logLookup(Level.SEVERE, e.getLocalizedMessage());
			}
			finally {
				DB.close(rs, pstmt);
			}
			int size = m_lookup.size();
			if (log.isLoggable(Level.FINER)) log.finer(m_info.KeyColumn
					+ " (" + m_info.Column_ID + "):"
					+ " - Loader complete #" + size + " - all=" + m_allLoaded
					+ " - ms=" + String.valueOf(System.currentTimeMillis()-m_startTime)
					+ " (" + String.valueOf(System.currentTimeMillis()-startTime) + ")");
		}	//	run
		/**
		 * Log a warning for the lookup problem found
		 * @param problem
		 */
		private void logLookup(Level level, String problem) {
			if (log.isLoggable(level)) {
				StringBuilder msg = new StringBuilder().append(m_info.KeyColumn).append(": Loader - ").append(problem);
				if (m_info.Column_ID > 0) {
					MColumn mColumn = MColumn.get(m_info.ctx, m_info.Column_ID);
					String column = mColumn.getColumnName();
					msg.append(", Column=").append(column);
					String tableName = MTable.getTableName(m_info.ctx, mColumn.getAD_Table_ID());
					msg.append(", Table=").append(tableName);
				}
				log.log(level, msg.toString());
			}
		}
	}	//	Loader
}	//	MLookup