/***********************************************************************
 * This file is part of iDempiere ERP Open Source                      *
 * http://www.idempiere.org                                            *
 *                                                                     *
 * Copyright (C) Contributors                                          *
 *                                                                     *
 * This program is free software; you can redistribute it and/or       *
 * modify it under the terms of the GNU General Public License         *
 * as published by the Free Software Foundation; either version 2      *
 * of the License, or (at your option) any later version.              *
 *                                                                     *
 * 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., 51 Franklin Street, Fifth Floor, Boston,          *
 * MA 02110-1301, USA.                                                 *
 *                                                                     *
 * Contributors:                                                       *
 * - hengsin                         								   *
 **********************************************************************/
package org.compiere.util;

import java.beans.Expression;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;

import org.compiere.model.GridTab;
import org.compiere.model.MClient;
import org.compiere.model.MColumn;
import org.compiere.model.MRefList;
import org.compiere.model.MTable;
import org.compiere.model.PO;

/**
 * Default {@link Evaluatee} implementation
 * @author hengsin
 */
public class DefaultEvaluatee implements Evaluatee {

	private DataProvider m_dataProvider;
	private int m_windowNo;
	private int m_tabNo;
	private boolean m_onlyWindow;
	private Boolean m_onlyTab;
	private boolean m_useMsgForBoolean = false;
	private boolean m_useColumnDateFormat = false;
	private String m_trxName = null;

	/** Cache for {@link #getColumnValue(MTable, String, String, int, String)} */
	private static final ReferenceCache s_ReferenceCache = new ReferenceCache("DefaultEvaluatee_ReferenceCache", 100, 1, 2000);
	
	/**
	 * @param gridTab
	 * @param windowNo
	 * @param tabNo
	 */
	public DefaultEvaluatee(GridTab gridTab, int windowNo, int tabNo) {
		this(gridTab, windowNo, tabNo, true);
	}
	
	/**
	 * @param gridTab
	 * @param windowNo
	 * @param tabNo
	 * @param onlyWindow
	 */
	public DefaultEvaluatee(GridTab gridTab, int windowNo, int tabNo, boolean onlyWindow) {
		this.m_dataProvider = gridTab != null ? new GridTabDataProvider(gridTab) : null;
		this.m_windowNo = windowNo;
		this.m_tabNo = tabNo;
		this.m_onlyWindow = onlyWindow;
	}
		
	/**
	 * @param gridTab
	 * @param windowNo
	 * @param tabNo
	 * @param onlyTab
	 * @param onlyWindow
	 */
	public DefaultEvaluatee(GridTab gridTab, int windowNo, int tabNo, boolean onlyTab, boolean onlyWindow) {
		this.m_dataProvider = gridTab != null ? new GridTabDataProvider(gridTab) : null;
		this.m_windowNo = windowNo;
		this.m_tabNo = tabNo;
		this.m_onlyWindow = onlyWindow;
		this.m_onlyTab = Boolean.valueOf(onlyTab);
	}
	
	/**
	 * @param po
	 */
	public DefaultEvaluatee(PO po) {
		this(new PODataProvider(po));
	}
	
	/**
	 * Help to use variable from window context 
	 * @param dataProvider
	 * @param windowNo
	 * @param tabNo
	 */
	public DefaultEvaluatee(DataProvider dataProvider, int windowNo, int tabNo) {
		this(dataProvider);
		this.m_windowNo = windowNo;
		this.m_tabNo = tabNo;
	}
	
	/**
	 * @param dataProvider
	 */
	public DefaultEvaluatee(DataProvider dataProvider) {
		this.m_dataProvider = dataProvider;
		this.m_windowNo = 0;
		this.m_tabNo = -1;
		this.m_onlyWindow = false;
		this.m_onlyTab = null;
	}
	
	/**
	 * Default constructor
	 */
	public DefaultEvaluatee() {
		this.m_dataProvider = null;
		this.m_windowNo = 0;
		this.m_tabNo = -1;
		this.m_onlyWindow = false;
		this.m_onlyTab = null;
	}
		
	public void setTrxName(String trxName) {
		m_trxName = trxName;
	}
	
	@Override
	public String get_ValueAsString(String variableName) {
		return get_ValueAsString(Env.getCtx(), variableName);
	}

	/**
	 * 	Get Variable Value (Evaluatee)
	 *  @param ctx
	 *	@param variableName name
	 *	@return value
	 */
	public String get_ValueAsString (Properties ctx, String variableName)
	{
		//format string
		String format = "";
		int f = variableName.indexOf(Evaluator.VARIABLE_FORMATTING_OPERATOR_START);
		if (f > 0 && variableName.endsWith(Evaluator.VARIABLE_FORMATTING_OPERATOR_END)) {
			format = variableName.substring(f+1, variableName.length()-1);
			variableName = variableName.substring(0, f);
		}
		
		//reference column
		String foreignColumn = "";
		f = variableName.indexOf(Evaluator.VARIABLE_REFERENCE_OPERATOR);
		if (f > 0) {
			String t = variableName.substring(0, f);
			if (t.matches(".*[_]ID([:].+)?")) {
				foreignColumn = variableName.substring(f+1, variableName.length());
				variableName = t;
			}
		}
		
		// IDEMPIERE-194 Handling null context variable (default value)
		String defaultValue = null;
		int idx = variableName.indexOf(Evaluator.VARIABLE_DEFAULT_VALUE_OPERATOR);	//	or clause
		if (idx  >=  0) 
		{
			defaultValue = variableName.substring(idx+1, variableName.length());
			variableName = variableName.substring(0, idx);
		}
					
		String value = null;
		boolean globalVariable = Env.isGlobalVariable(variableName);
		boolean tabOnly = m_onlyTab != null ? m_onlyTab.booleanValue() : false;
		// get value from window context or global
		if (m_windowNo != 0)
		{
			if (variableName.equalsIgnoreCase(GridTab.CTX_Record_ID))			
			{
				String keycolumnName = Env.getContext(Env.getCtx(), m_windowNo, m_tabNo, GridTab.CTX_KeyColumnName,
						m_onlyTab != null ? m_onlyTab.booleanValue() : false);
				value = Env.getContext(Env.getCtx(), m_windowNo, m_tabNo, keycolumnName, m_onlyTab != null ? m_onlyTab.booleanValue() : false);
			}
			else if (m_tabNo <= 0)
			{
				if (!tabOnly)
					value = Env.getContext (ctx, m_windowNo, variableName, m_onlyWindow);
				else
					value = Env.getContext (ctx, m_windowNo, m_tabNo, variableName, m_onlyTab.booleanValue(), m_onlyWindow);
			}
		    else
		    {		    	
		    	if (variableName.startsWith(Evaluator.VARIABLE_SELF_TAB_OPERATOR)) 
		    		variableName = variableName.substring(1);
		    	value = Env.getContext (ctx, m_windowNo, m_tabNo, variableName, tabOnly, true);
		    }
		}
		if (Util.isEmpty(value) && (globalVariable || Env.isPreference(variableName)))
		{
			value = Env.getContext(ctx, variableName);	// get from global context
		}

		// po property operator
		Object dataValue = null;
		if (Util.isEmpty(value) && m_dataProvider != null && !globalVariable) {
			if (variableName.startsWith(Evaluator.VARIABLE_PO_PROPERTY_OPERATOR)) {
				variableName = variableName.substring(1);
				dataValue = m_dataProvider.getProperty(variableName);
				value = dataValue != null ? dataValue.toString() : "";
			}
		}
		
		//remove prefix from variable name
		boolean withTabNo = false;
		if (globalVariable) {
			variableName = variableName.substring(1);				
		} else if (variableName.indexOf(Evaluator.VARIABLE_TAB_NO_SEPARATOR) > 0) {
			variableName = variableName.substring(variableName.lastIndexOf(Evaluator.VARIABLE_TAB_NO_SEPARATOR)+1);
			withTabNo = true;
		} else if (variableName.startsWith(Evaluator.VARIABLE_SELF_TAB_OPERATOR)) {
			variableName = variableName.substring(1);
			withTabNo = true;
		}
		
		//try window context again after removal of tab no
		if (!globalVariable && Util.isEmpty(value) && m_windowNo != 0 && withTabNo && !tabOnly) {
			value = Env.getContext(ctx, m_windowNo, variableName);
		}
		
		// get value from data provider(usually PO or GridTab)
		if (Util.isEmpty(value) && m_dataProvider != null && !globalVariable) {
			dataValue = m_dataProvider.getValue(variableName);
			value = dataValue != null ? dataValue.toString() : "";
		}
		
		//try context if no data provider and not only window and not only tab
		if (!globalVariable && m_dataProvider == null && Util.isEmpty(value) && !m_onlyWindow && (m_onlyTab == null || !m_onlyTab.booleanValue())) {
			value = Env.getContext(ctx, variableName);
		}
		
		//fall back to default (if define)
		if (Util.isEmpty(value) && defaultValue != null)
			value = defaultValue;
		
		// get column from data provider (usually GridTab or PO)
		MColumn column = null;		
		if (!globalVariable) {
			if (m_dataProvider != null) {
				column = m_dataProvider.getColumn(variableName);
			}
		}
		
		// handle reference(.) operator
		if (!Util.isEmpty(value) && !Util.isEmpty(foreignColumn) && variableName.endsWith(Evaluator.ID_COLUMN_SUFFIX)) {
			int id = 0;
			try {
				id = Integer.parseInt(value);
			} catch (Exception e){}
			if (id > 0) {
				String defaultReferenceValue = "";
				String foreignTable = getForeignTableName(variableName, column);
				//verify fully qualified foreign column
				if (!Util.isEmpty(foreignTable)) {
					int tblIndex = foreignColumn.indexOf(".");
					if (tblIndex > 0) {
						String qualifiedTable = foreignColumn.substring(0, tblIndex);
						if (qualifiedTable.equalsIgnoreCase(foreignTable)) 
							foreignColumn = foreignColumn.substring(tblIndex + 1);
						else
							foreignTable = null;
					}
				}
				if (!Util.isEmpty(foreignTable)) {
					MTable table = MTable.get(Env.getCtx(), foreignTable);
					if (table != null) {
						dataValue = getColumnValue(table, foreignTable, foreignColumn, id, defaultReferenceValue);
						if (dataValue == null)
							value = "";
						else if (dataValue instanceof Boolean booleanValue)
							value = booleanValue ? "Y" : "N";
						else
							value = dataValue.toString();
					}
				}				
			}
		}
		
		// handle format in <> operator
		if (format != null && format.length() > 0) {
			String foreignTable = getForeignTableName(variableName, column);
			if (foreignTable == null && column != null && getPO() != null
					&& getPO().get_KeyColumns() != null
					&& getPO().get_KeyColumns().length == 1
					&& getPO().get_KeyColumns()[0].equalsIgnoreCase(column.getColumnName())) {
				foreignTable = getPO().get_TableName();
			}
			//no dot operator and variable name is *_ID
			if (Util.isEmpty(foreignColumn) && variableName.endsWith(Evaluator.ID_COLUMN_SUFFIX)) {
				int id = 0;
				try {
					id = Integer.parseInt(value);
				} catch (Exception e){}
				if (id > 0) {					
					//verify fully qualified foreign column
					if (!Util.isEmpty(foreignTable)) {
						int tblIndex = format.indexOf(".");
						if (tblIndex > 0) {
							String qualifiedTable = format.substring(0, tblIndex);
							if (qualifiedTable.equalsIgnoreCase(foreignTable)) 
								format = format.substring(tblIndex + 1);
							else
								foreignTable = null;
						}
					}
					if (!Util.isEmpty(foreignTable)) {
						MTable table = MTable.get(Env.getCtx(), foreignTable);
						if (table != null) {
							column = table.getColumn(format);
							if (column != null) {
								if (column.isSecure()) {
									value = "********";
								} else {
									String trxName = m_trxName != null ? m_trxName : (m_dataProvider != null ? m_dataProvider.getTrxName() : null);
									String keyCol = foreignTable + Evaluator.ID_COLUMN_SUFFIX;
									value = DB.getSQLValueString(trxName,"SELECT " + format + " FROM " + foreignTable + " WHERE " + keyCol + "=?", id);
								}
							}
						}
					}
				}				
			} else if (!Util.isEmpty(value) && !Util.isEmpty(foreignTable) && foreignTable.equals(MRefList.Table_Name) && column != null) {
				int refID = column.getAD_Reference_Value_ID();
				if (format.equals("Name"))
					value = MRefList.getListName(Env.getCtx(), refID, (String) value);
				else if (format.equals("Description"))
					value = MRefList.getListDescription(Env.getCtx(), DB.getSQLValueStringEx(null, "SELECT Name FROM AD_Reference WHERE AD_Reference_ID = ?", refID), value);
			} else if (dataValue != null && dataValue instanceof Date dateValue) {
				SimpleDateFormat df = new SimpleDateFormat(format);
				value = df.format(dateValue);
			} else if (dataValue != null && dataValue instanceof Number numberValue) {
				DecimalFormat df = new DecimalFormat(format);
				value = df.format(numberValue.doubleValue());
			} else if (dataValue != null) {
				MessageFormat mf = new MessageFormat(format);
				value = mf.format(dataValue);
			}			
		} else if (column != null) {
			if (column.isSecure()) {
				value = "********";
			} else if (column != null && column.getAD_Reference_ID() == DisplayType.YesNo && dataValue != null && dataValue instanceof Boolean booleanValue) {
				if (m_useMsgForBoolean ) {
					if (booleanValue.booleanValue())
						value = Msg.getMsg(Env.getCtx(), "Yes");
					else
						value = Msg.getMsg(Env.getCtx(), "No");
				} else {
					value = booleanValue.booleanValue() ? "Y" : "N";
				}
			} else if (column != null && DisplayType.isDate(column.getAD_Reference_ID()) && dataValue != null && dataValue instanceof Date dateValue && m_useColumnDateFormat) {
				SimpleDateFormat sdf = DisplayType.getDateFormat(column.getAD_Reference_ID());
				value = sdf.format (dateValue);
			}				
		} else if (dataValue != null && dataValue instanceof BigDecimal decimalValue) {
			int precision = MClient.get(Env.getCtx()).getAcctSchema().getStdPrecision();
			value = decimalValue.setScale(precision, RoundingMode.HALF_UP).toPlainString();
		}
		
		return value != null ? value : "";
	}

	/**
	 * Get reference table name
	 * @param variableName
	 * @param column
	 * @return null or reference table name
	 */
	private static String getForeignTableName(String variableName, MColumn column) {
		String foreignTable = null;
		if (column != null) {
			foreignTable = column.getReferenceTableName();
		} else if (variableName.endsWith(Evaluator.ID_COLUMN_SUFFIX)){
			foreignTable = variableName.substring(0, variableName.length()-3);
			if (MTable.get(Env.getCtx(), foreignTable) == null)
				foreignTable = null;
		}
		return foreignTable;
	}
	
	/**
	 * Get value of foreignColumn from foreignTable
	 * @param table
	 * @param foreignTable
	 * @param foreignColumn
	 * @param id record id for foreign table
	 * @param defaultValue default value
	 * @return value from foreign table or default value
	 */
	private Object getColumnValue(MTable table, String foreignTable, String foreignColumn, int id, String defaultValue) {
		Object returnValue = defaultValue; 
		String key = foreignTable+"|"+id;
		PO po = null;
		if (s_ReferenceCache.containsKey(key))
		{
			po = s_ReferenceCache.get(key);
			if (po != null && po.get_ID() == id)
			{
				returnValue = po.get_Value(foreignColumn);				
			}
			else
				po = null;
		}
		if (po == null)
		{
			po = table.getPO(id, null);
			if (po != null && po.get_ID() == id)
			{
				s_ReferenceCache.put(key, po);
				returnValue = po.get_Value(foreignColumn);
			}						 						
		}
		return returnValue;
	}
	
	/**
	 * CacheChangeListener for {@link DefaultEvaluatee#s_ReferenceCache}
	 */
	private static final class ReferenceCache extends CCache<String, PO> implements CacheChangeListener {

		private static final long serialVersionUID = 6884795644185015913L;

		private ReferenceCache(String name, int initialCapacity, int expireMinutes, int maxSize) {
			super(null, name, initialCapacity, expireMinutes, false, maxSize);
		}

		@Override
		public void reset(String tableName) {
			String filter = tableName + "|";
			List<String> keys = keySet().stream().filter(e -> e.startsWith(filter)).collect(Collectors.toList());
			if (!keys.isEmpty()) {
				for(String key : keys)
					remove(key);
			}
		}

		@Override
		public void reset(String tableName, int recordId) {
			String key = tableName + "|" + recordId;
			remove(key);
		}
		
		@Override
		public void reset(String tableName, String key) {
			remove(key);
		}
	}
	
	/**
	 * Is using message translation for boolean data value
	 * @return true if using message translation for boolean data value
	 */
	public boolean isUseMsgForBoolean() {
		return m_useMsgForBoolean;
	}

	/**
	 * Set the use of message translation for boolean data value
	 * @param useMsgForBoolean
	 */
	public void setUseMsgForBoolean(boolean useMsgForBoolean) {
		this.m_useMsgForBoolean = useMsgForBoolean;
	}

	/**
	 * Is using column display format for date data value
	 * @return true if using column display format for date data value
	 */
	public boolean isUseColumnDateFormat() {
		return m_useColumnDateFormat;
	}

	/**
	 * Set the use of column display format for date data value
	 * @param useColumnDateFormat
	 */
	public void setUseColumnDateFormat(boolean useColumnDateFormat) {
		this.m_useColumnDateFormat = useColumnDateFormat;
	}
	
	/**
	 * Data provider interface
	 */
	public static interface DataProvider {
		/**
		 * Get column value via column name
		 * @param columnName
		 * @return null or value for columnName
		 */
		Object getValue(String columnName);
		
		/**
		 * Get value via bean property name
		 * @param propertyName
		 * @return null or value for propertyName
		 */
		Object getProperty(String propertyName);
		
		/**
		 * Get column via column name
		 * @param columnName
		 * @return null or column for columnName
		 */
		MColumn getColumn(String columnName);
		
		/**
		 * Get transaction name
		 * @return null or transaction name
		 */
		String getTrxName();
	}
	
	/**
	 * Get wrapped PO 
	 * @return wrapped PO or null
	 */
	public PO getPO() {
		if (m_dataProvider != null && m_dataProvider instanceof PODataProvider pdp) {
			return pdp.po;
		}
		return null;
	}
	
	/**
	 * Data provider implementation for PO
	 */
	public static class PODataProvider implements DataProvider {

		private PO po;

		public PODataProvider(PO po) {
			this.po = po;
		}
		
		@Override
		public Object getValue(String columnName) {
			if (po != null && po.get_ColumnIndex(columnName) >= 0)
				return po.get_Value(columnName);
				
			return null;
		}

		@Override
		public Object getProperty(String propertyName) {
			char startChar = propertyName.charAt(0);
			if (startChar != Character.toUpperCase(startChar)) {
				propertyName = Character.toUpperCase(startChar) + propertyName.substring(1);
			}
			String methodName = "get" + propertyName;
			Expression methodExpression = new Expression(po, methodName, null);
			Object poValue = null;
			try {
				poValue = methodExpression.getValue();
			} catch (Exception e) {				
			}
			return poValue;
		}

		@Override
		public MColumn getColumn(String columnName) {
			MColumn column = null;
			if (po != null) {
				if (po.get_ColumnIndex(columnName) >= 0) {
					column = MColumn.get(Env.getCtx(), po.get_TableName(), columnName);
				}
			}
			return column;
		}

		@Override
		public String getTrxName() {
			return po != null ? po.get_TrxName() : null;
		}
		
	}
	
	/**
	 * Data provider implementation for GridTab
	 */
	public static class GridTabDataProvider implements DataProvider {

		private GridTab gridTab;

		public GridTabDataProvider(GridTab gridTab) {
			this.gridTab = gridTab;
		}
		
		@Override
		public Object getValue(String columnName) {
			if (gridTab != null && gridTab.getField(columnName) != null) 
				return gridTab.getValue(columnName);
				
			return null;
		}

		@Override
		public Object getProperty(String propertyName) {
			return null;
		}

		@Override
		public MColumn getColumn(String columnName) {
			MColumn column = null;
			if (gridTab != null) {
				column = MColumn.get(Env.getCtx(), gridTab.getTableName(), columnName);					
				if (column == null) {
					//try parent
					GridTab parent = gridTab.getParentTab();
					while (column == null && parent != null) {
						column = MColumn.get(Env.getCtx(), parent.getTableName(), columnName);
						parent = parent.getParentTab();
					}
				}
			}
			return column;
		}

		@Override
		public String getTrxName() {
			return null;
		}		
	}
}
