/******************************************************************************
 * 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.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.ResultSet;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;

import org.compiere.util.CCache;
import org.compiere.util.Env;
import org.compiere.util.Msg;
import org.compiere.util.TimeUtil;
import org.compiere.util.Util;
import org.idempiere.cache.ImmutableIntPOCache;
import org.idempiere.cache.ImmutablePOSupport;

/**
 *  Tax Model
 *
 *	@author Jorg Janke
 *	@version $Id: MTax.java,v 1.3 2006/07/30 00:51:02 jjanke Exp $
 * 	red1 - FR: [ 2214883 ] Remove SQL code and Replace for Query
 *  trifonnt - BF [2913276] - Allow only one Default Tax Rate per Tax Category
 *  mjmckay - BF [2948632] - Allow edits to the Default Tax Rate 
 */
public class MTax extends X_C_Tax implements ImmutablePOSupport
{
	/**
	 * generated serial id
	 */
	private static final long serialVersionUID = -7971399495606742382L;
	/**	Cache						*/
	private static ImmutableIntPOCache<Integer,MTax>		s_cache	= new ImmutableIntPOCache<Integer,MTax>(Table_Name, 5);
	/**	Cache of Client						*/
	private static CCache<Integer,MTax[]>	s_cacheAll = new CCache<Integer,MTax[]>(Table_Name, Table_Name+"_Of_Client", 5);
	
	/**	Child Taxes			*/
	private MTax[]			m_childTaxes = null;
	/** Postal Codes		*/
	private MTaxPostal[]	m_postals = null;
	
	/**
	 * 	Get All Tax codes for client of this session/context
	 *	@param ctx context
	 *	@return array of MTax
	 */
	public static MTax[] getAll (Properties ctx)
	{
		int AD_Client_ID = Env.getAD_Client_ID(ctx);
		MTax[] retValue = (MTax[])s_cacheAll.get(AD_Client_ID);
		if (retValue != null)
		{
			if (ctx == Env.getCtx())
				return retValue;
			else
				return Arrays.stream(retValue).map(e -> {return new MTax(ctx, e).markImmutable();}).toArray(MTax[]::new);
		}

		//	Create it
		List<MTax> list = new Query(ctx, I_C_Tax.Table_Name, null, null)
								.setClient_ID()
								.setOrderBy("C_CountryGroupFrom_ID, C_Country_ID, C_Region_ID, C_CountryGroupTo_ID, To_Country_ID, To_Region_ID, ValidFrom DESC")
								.setOnlyActiveRecords(true)
								.list();
		for (MTax tax : list)
		{
			s_cache.put(tax.get_ID(), tax, e -> new MTax(Env.getCtx(), e));
		}
		retValue = list.toArray(new MTax[list.size()]);
		if (ctx == Env.getCtx())
			s_cacheAll.put(AD_Client_ID, retValue);
		else
			s_cacheAll.put(AD_Client_ID, Arrays.stream(retValue).map(e -> {return new MTax(Env.getCtx(), e);}).toArray(MTax[]::new));
		return retValue;
	}	//	getAll

	/**
	 * 	Get Tax from Cache (immutable)
	 *	@param C_Tax_ID id
	 *	@return MTax
	 */
	public static MTax get (int C_Tax_ID)
	{
		return get(Env.getCtx(), C_Tax_ID);
	}
	
	/**
	 * 	Get Tax from Cache (immutable)
	 *	@param ctx context
	 *	@param C_Tax_ID id
	 *	@return MTax
	 */
	public static MTax get (Properties ctx, int C_Tax_ID)
	{
		Integer key = Integer.valueOf(C_Tax_ID);
		MTax retValue = s_cache.get (ctx, key, e -> new MTax(ctx, e));
		if (retValue != null)
			return retValue;
		retValue = new MTax (ctx, C_Tax_ID, (String)null);
		if (retValue.get_ID () == C_Tax_ID)
		{
			s_cache.put (key, retValue, e -> new MTax(Env.getCtx(), e));
			return retValue;
		}
		return null;
	}	//	get

	/**
	 * Get updateable copy of MTax from cache
	 * @param ctx
	 * @param C_Tax_ID
	 * @param trxName
	 * @return MTax
	 */
	public static MTax getCopy(Properties ctx, int C_Tax_ID, String trxName)
	{
		MTax tax = get(C_Tax_ID);
		if (tax != null)
			tax = new MTax(ctx, tax, trxName);
		return tax;
	}
	
    /**
     * UUID based Constructor
     * @param ctx  Context
     * @param C_Tax_UU  UUID key
     * @param trxName Transaction
     */
    public MTax(Properties ctx, String C_Tax_UU, String trxName) {
        super(ctx, C_Tax_UU, trxName);
		if (Util.isEmpty(C_Tax_UU))
			setInitialDefaults();
    }

	/**
	 * 	Standard Constructor
	 *	@param ctx context
	 *	@param C_Tax_ID id
	 *	@param trxName transaction
	 */
	public MTax (Properties ctx, int C_Tax_ID, String trxName)
	{
		super (ctx, C_Tax_ID, trxName);
		if (C_Tax_ID == 0)
			setInitialDefaults();
	}	//	MTax

	/**
	 * Set the initial defaults for a new record
	 */
	private void setInitialDefaults() {
		setIsDefault (false);
		setIsDocumentLevel (true);
		setIsSummary (false);
		setIsTaxExempt (false);
		setRate (Env.ZERO);
		setRequiresTaxCertificate (false);
		setSOPOType (SOPOTYPE_Both);
		setValidFrom (TimeUtil.getDay(1990,1,1));
		setIsSalesTax(false);
	}

	/**
	 * 	Load Constructor
	 *	@param ctx context
	 *	@param rs result set
	 *	@param trxName transaction
	 */
	public MTax (Properties ctx, ResultSet rs, String trxName)
	{
		super(ctx, rs, trxName);
	}	//	MTax

	/**
	 * 	New record Constructor
	 *	@param ctx
	 *	@param Name
	 *	@param Rate
	 *	@param C_TaxCategory_ID
	 *	@param trxName transaction
	 */
	public MTax (Properties ctx, String Name, BigDecimal Rate, int C_TaxCategory_ID, String trxName)
	{
		this (ctx, 0, trxName);
		setName (Name);
		setRate (Rate == null ? Env.ZERO : Rate);
		setC_TaxCategory_ID (C_TaxCategory_ID);	//	FK
	}	//	MTax

	/**
	 * Copy constructor
	 * @param copy
	 */
	public MTax(MTax copy) 
	{
		this(Env.getCtx(), copy);
	}

	/**
	 * Copy constructor
	 * @param ctx
	 * @param copy
	 */
	public MTax(Properties ctx, MTax copy) 
	{
		this(ctx, copy, (String) null);
	}

	/**
	 * Copy constructor
	 * @param ctx
	 * @param copy
	 * @param trxName
	 */
	public MTax(Properties ctx, MTax copy, String trxName) 
	{
		this(ctx, 0, trxName);
		copyPO(copy);
		this.m_childTaxes = copy.m_childTaxes != null ? Arrays.stream(copy.m_childTaxes).map(e -> {return new MTax(ctx, e, trxName);}).toArray(MTax[]::new) : null;
		this.m_postals = copy.m_postals != null ? Arrays.stream(copy.m_postals).map(e -> {return new MTaxPostal(ctx, e, trxName);}).toArray(MTaxPostal[]::new) : null;
	}


	/**
	 * 	Get Child Taxes
	 * 	@param requery true to reload from DB
	 *	@return array of taxes or null
	 */
	public MTax[] getChildTaxes (boolean requery)
	{
		if (!isSummary())
			return null;
		if (m_childTaxes != null && !requery)
			return m_childTaxes;
		//
		final String whereClause = COLUMNNAME_Parent_Tax_ID+"=?";
		List<MTax> list = new Query(getCtx(), I_C_Tax.Table_Name, whereClause,  get_TrxName())
			.setParameters(getC_Tax_ID())
			.setOnlyActiveRecords(true)
			.setClient_ID()
			.list();	
		if (list.size() > 0 && is_Immutable())
			list.stream().forEach(e -> e.markImmutable());
	 
		m_childTaxes = new MTax[list.size ()];
		list.toArray (m_childTaxes);
		return m_childTaxes;
	}	//	getChildTaxes
	
	/**
	 * Get Postal Qualifiers
	 * @param requery true to reload from DB
	 * @return array of MTaxPostal
	 */
	public MTaxPostal[] getPostals (boolean requery)
	{
		if (m_postals != null && !requery)
			return m_postals;

		final String whereClause = MTaxPostal.COLUMNNAME_C_Tax_ID+"=?";
		List<MTaxPostal> list = new Query(getCtx(), I_C_TaxPostal.Table_Name, whereClause,  get_TrxName())
			.setParameters(getC_Tax_ID())
			.setOnlyActiveRecords(true)
			.setOrderBy(I_C_TaxPostal.COLUMNNAME_Postal+", "+I_C_TaxPostal.COLUMNNAME_Postal_To)
			.list();	
		if (list.size() > 0 && is_Immutable())
			list.stream().forEach(e -> e.markImmutable());

		if (list.size() > 0) { 
			m_postals = new MTaxPostal[list.size ()];
			list.toArray (m_postals);
		}
		return m_postals;
	}	//	getPostals
	
	/**
	 * Is this tax uses postal code criteria
	 * @return true if uses postal code criteria
	 */
	public boolean isPostal()
	{
		if (getPostals(false) == null)
			return false;
		
		return getPostals(false).length > 0;
	}	//	isPostal
	
	/**
	 * Is Zero Tax
	 * @return true if tax rate is 0
	 */
	public boolean isZeroTax()
	{
		return getRate().signum() == 0;
	}	//	isZeroTax
	
	@Override
	public String toString()
	{
		StringBuilder sb = new StringBuilder("MTax[")
			.append(get_ID())
			.append(", Name = ").append(getName())
			.append(", SO/PO=").append(getSOPOType())
			.append(", Rate=").append(getRate())
			.append(", C_TaxCategory_ID=").append(getC_TaxCategory_ID())
			.append(", Summary=").append(isSummary())
			.append(", Parent=").append(getParent_Tax_ID())
			.append(", Country=").append(getC_Country_ID()).append("|").append(getTo_Country_ID())
			.append(", Region=").append(getC_Region_ID()).append("|").append(getTo_Region_ID())
			.append("]");
		return sb.toString();
	}	//	toString

	
	/**
	 * 	Calculate Tax Amount
	 *	@param amount base amount to calculate tax amount
	 *	@param taxIncluded if true tax amount is already included in the amount parameter and we need to extract tax amount from it.  
	 *	@param scale rounding scale for tax amount 
	 *	@return tax amount
	 */
	public BigDecimal calculateTax (BigDecimal amount, boolean taxIncluded, int scale)
	{
		//	Null Tax
		if (isZeroTax())
			return Env.ZERO;

		MTax[] taxarray;
		if (isSummary())
			taxarray = getChildTaxes(false);
		else
			taxarray = new MTax[] {this};

		BigDecimal tax = Env.ZERO;		
		for (MTax taxc : taxarray) {
			BigDecimal multiplier = taxc.getRate().divide(Env.ONEHUNDRED, 12, RoundingMode.HALF_UP);		
			if (!taxIncluded)	//	$100 * 6 / 100 == $6 == $100 * 0.06
			{
				BigDecimal itax = amount.multiply(multiplier).setScale(scale, RoundingMode.HALF_UP);
				tax = tax.add(itax);
			}
			else			//	$106 - ($106 / (100+6)/100) == $6 == $106 - ($106/1.06)
			{
				multiplier = multiplier.add(Env.ONE);
				BigDecimal base = amount.divide(multiplier, 12, RoundingMode.HALF_UP); 
				BigDecimal itax = amount.subtract(base).setScale(scale, RoundingMode.HALF_UP);
				tax = tax.add(itax);
			}
		}
		if (log.isLoggable(Level.FINE)) log.fine("calculateTax " + amount 
			+ " (incl=" + taxIncluded + ",scale=" + scale 
			+ ") = " + tax + " [" + tax + "]");
		return tax;
	}	//	calculateTax

	@Override
	protected boolean beforeSave(boolean newRecord) {
		if (isDefault()) {
			// Ensure that only one tax rate is set as Default for a tax category
			String whereClause = MTax.COLUMNNAME_C_TaxCategory_ID+"=? AND " + 
								 MTax.COLUMNNAME_C_Tax_ID+"<>? AND "+
								 "IsDefault='Y'";
			List<MTax> list = new Query(getCtx(), I_C_Tax.Table_Name, whereClause,  get_TrxName())
				.setParameters(getC_TaxCategory_ID(), getC_Tax_ID())
				.setOnlyActiveRecords(true)
				.list();
			if (list.size() >= 1) {
				log.saveError("Error", Msg.parseTranslation(getCtx(), Msg.getMsg(Env.getCtx(),"OnlyOneTaxPerCategoryMarkedDefault")));
				return false;
			}
		}
		// Ensure parent tax id is zero for summary tax
		if (isSummary()) {
			if (getParent_Tax_ID() > 0) {
				setParent_Tax_ID(0);
			}
		}
		return super.beforeSave(newRecord);
	}
	
	@Override
	protected boolean afterSave (boolean newRecord, boolean success)
	{
		// Create accounting record
		if (newRecord && success)
			insert_Accounting("C_Tax_Acct", "C_AcctSchema_Default", null);

		return success;
	}	//	afterSave

	@Override
	public MTax markImmutable() {
		if (is_Immutable()) 
			return this;
		
		makeImmutable();
		if (m_childTaxes != null && m_childTaxes.length > 0)
			Arrays.stream(m_childTaxes).forEach(e -> e.markImmutable());
		if (m_postals != null && m_postals.length > 0)
			Arrays.stream(m_postals).forEach(e -> e.markImmutable());
		
		return this;
	}
	
	/**
	 * @return 
	 * - true if tax is posted to the product asset account and added to product cost (non deductible input tax).<br/> 
	 * - false if tax is posted to a separate GL account from product asset account (deductible input tax).
	 */
	public boolean isDistributeTaxWithLineItem()
	{
		return TAXPOSTINGINDICATOR_DistributeTaxWithRelevantExpense.equals(getTaxPostingIndicator());
	}
}	//	MTax
