/******************************************************************************
 * 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           *
 * Contributor(s): Carlos Ruiz - globalqss                                    *
 *****************************************************************************/
package org.compiere.model;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import org.adempiere.base.IModelFactory;
import org.adempiere.base.IServiceReferenceHolder;
import org.adempiere.base.Service;
import org.adempiere.model.GenericPO;
import org.compiere.db.AdempiereDatabase;
import org.compiere.db.Database;
import org.compiere.db.partition.ITablePartitionService;
import org.compiere.util.CCache;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.compiere.util.KeyNamePair;
import org.compiere.util.Msg;
import org.compiere.util.Util;
import org.idempiere.cache.ImmutableIntPOCache;
import org.idempiere.cache.ImmutablePOSupport;
/**
 *	Persistent Table Model
 * 
 * Change log:
 * 
 * - 2007-02-01 - teo_sarca - [ 1648850 ] MTable.getClass works incorrect for table "Fact_Acct"
 * 
*
 * - 2007-08-30 - vpj-cd - [ 1784588 ] Use ModelPackage of EntityType to Find Model Class
 * 
*  @author Jorg Janke
 *  @author Teo Sarca, teo.sarca@gmail.com
 *  		BF [ 3017117 ] MTable.getClass returns bad class
 *  			https://sourceforge.net/p/adempiere/bugs/2433/
 *  @version $Id: MTable.java,v 1.3 2006/07/30 00:58:04 jjanke Exp $
 */
public class MTable extends X_AD_Table implements ImmutablePOSupport
{
	/**
	 * 
	 */
	private static final long serialVersionUID = 6774131577483620665L;
	public final static int MAX_OFFICIAL_ID = 999999;
	/**
	 * 	Get MTable from Cache (immutable)
	 *	@param AD_Table_ID id
	 *	@return MTable
	 */
	public static MTable get (int AD_Table_ID)
	{
		return get(Env.getCtx(), AD_Table_ID);
	}
	
	/** 
	 * 	Get MTable from Cache (immutable)
	 *	@param ctx context
	 *	@param AD_Table_ID id
	 *	@return MTable
	 */
	public static MTable get (Properties ctx, int AD_Table_ID)
	{
		return get(ctx, AD_Table_ID, null);
	}	//	get
	/**
	 * 	Get MTable from Cache (immutable)
	 *	@param ctx context
	 *	@param AD_Table_ID id
	 *	@param trxName transaction
	 *	@return MTable
	 */
	public static synchronized MTable get (Properties ctx, int AD_Table_ID, String trxName)
	{
		Integer key = Integer.valueOf(AD_Table_ID);
		MTable retValue = s_cache.get (ctx, key, e -> new MTable(ctx, e));
		if (retValue != null) 
			return retValue;
		
		retValue = new MTable (ctx, AD_Table_ID, trxName);
		if (retValue.get_ID () == AD_Table_ID) 
		{
			s_cache.put (key, retValue, e -> new MTable(Env.getCtx(), e));
			return retValue;
		}
		return null;
	}	//	get
	/**
	 * Get updateable copy of MTable from cache
	 * @param ctx
	 * @param AD_Table_ID
	 * @param trxName
	 * @return MTable
	 */
	public static MTable getCopy(Properties ctx, int AD_Table_ID, String trxName)
	{
		MTable table = get(ctx, AD_Table_ID, trxName);
		if (table != null)
			table = new MTable(ctx, table, trxName);
		return table;
	}
	
	/**
	 * 	Get MTable from Cache
	 *	@param ctx context
	 *	@param tableName case insensitive table name
	 *	@return MTable
	 */
	public static synchronized MTable get (Properties ctx, String tableName)
	{
		return get(ctx, tableName, null);
	}	//	get
	
	/**
	 * 	Get MTable from Cache
	 *	@param ctx context
	 *	@param tableName case insensitive table name
	 *  @param trxName
	 *	@return MTable
	 */
	public static synchronized MTable get (Properties ctx, String tableName, String trxName)
	{
		if (tableName == null)
			return null;
		MTable[] tables = s_cache.values().toArray(new MTable[0]);
		for (MTable retValue : tables)
		{
			if (tableName.equalsIgnoreCase(retValue.getTableName()))
			{
				return s_cache.get (ctx, retValue.get_ID(), e -> new MTable(ctx, e));
			}
		}
		//
		MTable retValue = null;
		String sql = "SELECT * FROM AD_Table WHERE UPPER(TableName)=?";
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			pstmt = DB.prepareStatement (sql, trxName);
			pstmt.setString(1, tableName.toUpperCase());
			rs = pstmt.executeQuery ();
			if (rs.next())
				retValue = new MTable (ctx, rs, trxName);
		}
		catch (Exception e)
		{
			s_log.log(Level.SEVERE, sql, e);
		}
		finally
		{
			DB.close(rs, pstmt);
			rs = null; pstmt = null;
		}
		if (retValue != null)
		{
			Integer key = Integer.valueOf(retValue.getAD_Table_ID());
			s_cache.put (key, retValue, e -> new MTable(Env.getCtx(), e));
		}
		return retValue;
	}	//	get
	/**
	 * 	Get Table Name
	 *	@param ctx context
	 *	@param AD_Table_ID table
	 *	@return table name
	 */
	public static String getTableName (Properties ctx, int AD_Table_ID)
	{
		return MTable.get(ctx, AD_Table_ID).getTableName();
	}	//	getTableName
	/**
	 * Get table accessible by current effective role (via window access).
	 * @param withEmptyElement if true, first element of the return array is an empty element with (-1,"")
	 * @param trxName optional transaction name
	 * @return table records (AD_Table_ID, translated Name), order by translated name
	 */
	public static KeyNamePair[] getWithWindowAccessKeyNamePairs(boolean withEmptyElement, String trxName)
	{
		final MRole role = MRole.getDefault(); 
		boolean trl = !Env.isBaseLanguage(Env.getCtx(), "AD_Table");
		String lang = Env.getAD_Language(Env.getCtx());
		String sql = "SELECT DISTINCT t.AD_Table_ID,"
				+ (trl ? "trl.Name" : "t.Name")
			+ " FROM AD_Table t INNER JOIN AD_Tab tab ON (tab.AD_Table_ID=t.AD_Table_ID)"
			+ " INNER JOIN AD_Window_Access wa ON (tab.AD_Window_ID=wa.AD_Window_ID) "
			+ (trl ? "LEFT JOIN AD_Table_Trl trl on (trl.AD_Table_ID=t.AD_Table_ID and trl.AD_Language=" + DB.TO_STRING(lang) + ")" : "") 
			+ " WHERE "+role.getIncludedRolesWhereClause("wa.AD_Role_ID", null) 
			+ " AND t.IsActive='Y' AND tab.IsActive='Y' "
			+ "ORDER BY 2";
		return DB.getKeyNamePairsEx(trxName, sql, withEmptyElement);			
	}
	
	/**	Cache						*/
	private static ImmutableIntPOCache s_cache = new ImmutableIntPOCache(Table_Name, Table_Name, 20, 0, false, 0);
	/**	Static Logger	*/
	private static CLogger	s_log	= CLogger.getCLogger (MTable.class);
	private static final CCache> s_modelFactoryCache = new CCache<>(null, "IModelFactory", 100, 120, false, 2000);
	/**
	 * 	Get Java Model Class for Table
	 *	@param tableName table name
	 *	@return Java model class or null
	 */
	public static Class> getClass (String tableName)
	{
		IServiceReferenceHolder cache = s_modelFactoryCache.get(tableName);
		if (cache != null)
		{
			IModelFactory service = cache.getService();
			if (service != null)
			{
				Class> clazz = service.getClass(tableName);
				if (clazz != null)
					return clazz;
			}
			s_modelFactoryCache.remove(tableName);
		}
		
		List> factoryList = Service.locator().list(IModelFactory.class).getServiceReferences();
		if (factoryList == null)
			return null;
		for(IServiceReferenceHolder factory : factoryList) {
			IModelFactory service = factory.getService();
			if (service != null) {
				Class> clazz = service.getClass(tableName);
				if (clazz != null)
				{
					s_modelFactoryCache.put(tableName, factory);
					return clazz;
				}
			}
		}
		return null;
	}	//	getClass
    /**
     * UUID based Constructor
     * @param ctx  Context
     * @param AD_Table_UU  UUID key
     * @param trxName Transaction
     */
    public MTable(Properties ctx, String AD_Table_UU, String trxName) {
        super(ctx, AD_Table_UU, trxName);
		if (Util.isEmpty(AD_Table_UU))
			setInitialDefaults();
    }
	/**
	 * 	Standard Constructor
	 *	@param ctx context
	 *	@param AD_Table_ID id
	 *	@param trxName transaction
	 */
	public MTable (Properties ctx, int AD_Table_ID, String trxName)
	{
		super (ctx, AD_Table_ID, trxName);
		if (AD_Table_ID == 0)
			setInitialDefaults();
	}	//	MTable
	/**
	 * Set the initial defaults for a new record
	 */
	private void setInitialDefaults() {
		setAccessLevel (ACCESSLEVEL_SystemOnly);	// 4
		setEntityType (ENTITYTYPE_UserMaintained);	// U
		setIsChangeLog (false);
		setIsDeleteable (false);
		setIsHighVolume (false);
		setIsSecurityEnabled (false);
		setIsView (false);	// N
		setReplicationType (REPLICATIONTYPE_Local);
	}
	/**
	 * 	Load Constructor
	 *	@param ctx context
	 *	@param rs result set
	 *	@param trxName transaction
	 */
	public MTable (Properties ctx, ResultSet rs, String trxName)
	{
		super(ctx, rs, trxName);
	}	//	MTable
	/**
	 * Copy constructor
	 * @param copy
	 */
	public MTable(MTable copy) 
	{
		this(Env.getCtx(), copy);
	}
	/**
	 * Copy constructor
	 * @param ctx
	 * @param copy
	 */
	public MTable(Properties ctx, MTable copy) 
	{
		this(ctx, copy, (String) null);
	}
	/**
	 * Copy constructor
	 * @param ctx
	 * @param copy
	 * @param trxName
	 */
	public MTable(Properties ctx, MTable copy, String trxName) 
	{
		//-1 to avoid infinite loop
		this(ctx, -1, trxName);
		copyPO(copy);
		this.m_columns = copy.m_columns != null ? Arrays.stream(copy.m_columns).map(e -> {return new MColumn(ctx, e, trxName);}).toArray(MColumn[]::new): null;
		this.m_columnNameMap = copy.m_columnNameMap != null ? new ConcurrentHashMap(copy.m_columnNameMap) : null;
		this.m_columnIdMap = copy.m_columnIdMap != null ? new ConcurrentHashMap(copy.m_columnIdMap) : null;
		this.m_viewComponents = copy.m_viewComponents != null ? Arrays.stream(copy.m_viewComponents).map(e -> {return new MViewComponent(ctx, e, trxName);}).toArray(MViewComponent[]::new) : null;
	}
	/**	Columns				*/
	private MColumn[]	m_columns = null;
	/** Key Columns					*/
	private String[]	m_KeyColumns = null;
	/** column name to column index map **/
	private ConcurrentMap m_columnNameMap;
	/** ad_column_id to column index map **/
	private ConcurrentMap m_columnIdMap;
	/** View Components		*/
	private MViewComponent[]	m_viewComponents = null;
	/**
	 * 	Get Columns
	 *	@param requery true to re-query from DB
	 *	@return array of column
	 */
	public synchronized MColumn[] getColumns (boolean requery)
	{
		if (m_columns != null && !requery)
			return m_columns;
		m_columnNameMap = new ConcurrentHashMap();
		m_columnIdMap = new ConcurrentHashMap();
		String sql = "SELECT * FROM AD_Column WHERE AD_Table_ID=? AND IsActive='Y' ORDER BY ColumnName";
		ArrayList list = new ArrayList();
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			pstmt = DB.prepareStatement (sql, get_TrxName());
			pstmt.setInt (1, getAD_Table_ID());
			rs = pstmt.executeQuery ();
			while (rs.next ()) {
				MColumn column = new MColumn (getCtx(), rs, get_TrxName());
				if (is_Immutable())
					column.markImmutable();
				list.add (column);
				m_columnNameMap.put(column.getColumnName().toUpperCase(), list.size() - 1);
				m_columnIdMap.put(column.getAD_Column_ID(), list.size() - 1);
			}
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, sql, e);
		}
		finally
		{
			DB.close(rs, pstmt);
			rs = null; pstmt = null;
		}
		//
		if (list.size() > 0 && is_Immutable())
			list.stream().forEach(e -> e.markImmutable());
		m_columns = new MColumn[list.size ()];
		list.toArray (m_columns);
		return m_columns;
	}	//	getColumns
	/**
	 * 	Get Column via column name
	 *	@param columnName (case insensitive)
	 *	@return MColumn if found, null otherwise
	 */
	public MColumn getColumn (String columnName)
	{
		if (columnName == null || columnName.length() == 0)
			return null;
		int idx = getColumnIndex(columnName);
		if (idx < 0)
			return null;
		return m_columns[idx];
	}	//	getColumn
	/**
	 *  Get Column Index via column name
	 *  @param ColumnName column name (case insensitive)
	 *  @return index of column with ColumnName or -1 if not found
	 */
	public synchronized int getColumnIndex (String ColumnName)
	{
		if (m_columns == null)
			getColumns(false);
		Integer i = m_columnNameMap.get(ColumnName.toUpperCase());
		if (i != null)
			return i.intValue();
		
		return -1;
	}   //  getColumnIndex
	/**
	 *  Is column exists and is not virtual ?
	 *  @param ColumnName column name (case insensitive)
	 *  @return true if column exists and is not virtual
	 */
	public synchronized boolean columnExistsInDB (String ColumnName)
	{
		MColumn column = getColumn(ColumnName);
		return column != null && ! column.isVirtualColumn();
	}   //  columnExistsInDB
	/**
	 *  Column exists?
	 *  @param ColumnName column name (case insensitive)
	 *  @return true if column exists in dictionary
	 */
	public synchronized boolean columnExistsInDictionary (String ColumnName)
	{
		return getColumnIndex(ColumnName) >= 0;
	}   //  columnExistsInDictionary
	/**
	 *  Get Column Index
	 *  @param AD_Column_ID column id
	 *  @return index of column with AD_Column_ID or -1 if not found
	 */
	public synchronized int getColumnIndex (int AD_Column_ID)
	{
		if (m_columns == null)
			getColumns(false);
		Integer i = m_columnIdMap.get(AD_Column_ID);
		if (i != null)
			return i.intValue();
		
		return -1;
	}   //  getColumnIndex
	/**
	 * 	Table is with single primary key
	 *	@return true if table has single primary key column
	 */
	public boolean isSingleKey()
	{
		String[] keys = getKeyColumns();
		return keys.length == 1;
	}	//	isSingleKey
	/**
	 * 	Get Key Columns of Table
	 *	@return array of key column name
	 */
	public String[] getKeyColumns()
	{
		if (m_KeyColumns != null)
			return m_KeyColumns;
		getColumns(false);
		ArrayList list = new ArrayList();
		//
		for (int i = 0; i < m_columns.length; i++)
		{
			MColumn column = m_columns[i];
			if (column.isKey()) {
				m_KeyColumns = new String[]{column.getColumnName()};
				return m_KeyColumns;
			}
			if (column.isParent())
				list.add(column.getColumnName());
		}
		//check uuid key
		if (list.isEmpty()) {
			MColumn uuColumn = getColumn(PO.getUUIDColumnName(getTableName()));
			if (uuColumn != null) {
				m_KeyColumns = new String[]{uuColumn.getColumnName()};
				return m_KeyColumns;
			}
		}
		String[] retValue = new String[list.size()];
		retValue = list.toArray(retValue);
		m_KeyColumns = retValue;
		return m_KeyColumns;
	}	//	getKeyColumns
	
	/**
	 * @return true if table has single key column and the key column name is the same as the table name plus _ID.
	 */
	public boolean isIDKeyTable()
	{
		String idColName = getTableName() + "_ID";
		return (getKeyColumns() != null && getKeyColumns().length == 1 && getKeyColumns()[0].equals(idColName));
	}
	/**
	 * @return true if table has single key column and the key column name ends with _UU.
	 */
	public boolean isUUIDKeyTable()
	{
		String uuColName = PO.getUUIDColumnName(getTableName());
		return (getKeyColumns() != null && getKeyColumns().length == 1 && getKeyColumns()[0].equals(uuColName));
	}
	
	/**
	 * @return true if table has a UUID column (column name ends with _UU)
	 */
	public boolean hasUUIDKey()
	{
		String uuColName = PO.getUUIDColumnName(getTableName());
		if (m_columns == null)
			getColumns(false);
		return m_columnNameMap.get(uuColName.toUpperCase()) != null;
	}
	/**
	 * 	Get Identifier Columns of Table (IsIdentifier=Y)
	 *	@return array of identifier column name
	 */
	public String[] getIdentifierColumns() {
		ArrayList listkn = new ArrayList();
		for (MColumn column : getColumns(false)) {
			if (column.isIdentifier())
				listkn.add(new KeyNamePair(column.getSeqNo(), column.getColumnName()));
		}
		// Order by SeqNo
		Collections.sort(listkn, new Comparator(){
			public int compare(KeyNamePair s1,KeyNamePair s2){
				if (s1.getKey() < s2.getKey())
					return -1;
				else if (s1.getKey() > s2.getKey())
					return 1;
				else
					return 0;
			}});
		String[] retValue = new String[listkn.size()];
		for (int i = 0; i < listkn.size(); i++) {
			retValue[i] = listkn.get(i).getName();
		}
		return retValue;
	}	//	getIdentifierColumns
	/**
	 * 	Get PO Instance for this table
	 *	@param Record_ID record id. 0 to create new record instance, > 0 to load existing record instance.
	 *	@param trxName
	 *	@return PO for Record_ID or null
	 */
	public PO getPO (int Record_ID, String trxName)
	{
		String tableName = getTableName();
		if (Record_ID != 0 && !isSingleKey())
		{
			log.log(Level.WARNING, "(id) - Multi-Key " + tableName);
			return null;
		}
		PO po = null;
		IServiceReferenceHolder cache = s_modelFactoryCache.get(tableName);
		if (cache != null)
		{
			IModelFactory service = cache.getService();
			if (service != null)
			{
				po = service.getPO(tableName, Record_ID, trxName);
				if (po != null)
				{
					if (po.get_ID() != Record_ID && Record_ID > 0)
						po = null;
					return po;
				}
			}
			s_modelFactoryCache.remove(tableName);
		}
		
		List> factoryList = Service.locator().list(IModelFactory.class).getServiceReferences();
		if (factoryList != null)
		{
			for(IServiceReferenceHolder factory : factoryList)
			{
				IModelFactory service = factory.getService();
				if (service != null)
				{
					po = service.getPO(tableName, Record_ID, trxName);
					if (po != null)
					{
						if (po.get_ID() != Record_ID && Record_ID > 0)
							po = null;
						s_modelFactoryCache.put(tableName, factory);
						break;
					}
				}
			}
		}
		if (po == null && s_modelFactoryCache.get(tableName) == null)
		{
			po = new GenericPO(tableName, getCtx(), Record_ID, trxName);
			if (po.get_ID() != Record_ID && Record_ID > 0)
				po = null;
			// TODO: how to add GenericPO to the s_modelFactoryCache ??
		}
		return po;
	}	//	getPO
	private static final ThreadLocal