/******************************************************************************
 * Product: Adempiere ERP & CRM Smart Business Solution                       *
 * 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    *
 * Copyright (C) 2003-2010 e-Evolution,SC. All Rights Reserved.               *
 * Contributor(s): Victor Perez www.e-evolution.com                           *
 *                 Teo Sarca, www.arhipac.ro                                  *
 *****************************************************************************/
package org.eevolution.model;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.List;
import java.util.Properties;

import org.adempiere.exceptions.AdempiereException;
import org.compiere.model.I_M_Product;
import org.compiere.model.MProduct;
import org.compiere.model.MUOM;
import org.compiere.model.Query;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.idempiere.cache.ImmutablePOSupport;

/**
 *  PP Product BOM Line Model.
 * 	<code>
 * 			MPPProductBOMLine l = new MPPProductBOMLine(bom);
 * 			l.setM_Product_ID(wbl.getM_Product_ID());
 * 			l.setQty(wbl.getQuantity());
 * 			l.saveEx();
 *	</code>
 *
 * @author Victor Perez www.e-evolution.com     
 * @author Teo Sarca, www.arhipac.ro
 */
public class MPPProductBOMLine extends X_PP_Product_BOMLine implements ImmutablePOSupport
{
	/**
	 * generated serial id
	 */
	private static final long serialVersionUID = 5942313871247489972L;
	
	protected MPPProductBOM m_bom = null;
	
	/**
	 * Get all the Product BOM line for a Component Product
	 * @param product Component Product
	 * @return list of MPPProductBOMLine
	 */
	public static List<MPPProductBOMLine> getByProduct(MProduct product) 
	{		
		final String whereClause = MPPProductBOMLine.COLUMNNAME_M_Product_ID+"=?";
		return new Query(product.getCtx(), MPPProductBOMLine.Table_Name, whereClause, product.get_TrxName())
						.setParameters(product.getM_Product_ID())
						.setOnlyActiveRecords(true)
						.setClient_ID()
						.list();
	}
	
	/**
	 * Get all the BOM lines for a Product
	 * @param product Product
	 * @return list of MPPProductBOMLine
	 */
	public static MPPProductBOMLine[] getBOMLines(MProduct product) 
	{		
		final String whereClause = MPPProductBOMLine.COLUMNNAME_PP_Product_BOM_ID 
							+ " IN ( SELECT PP_PRODUCT_BOM_ID FROM PP_PRODUCT_BOM WHERE M_PRODUCT_ID = " + product.getM_Product_ID() + " AND IsActive='Y' AND BOMType='A' AND BOMUse='A' " 
							+ " AND AD_Client_ID=" + product.getAD_Client_ID() + ")";
		List <MPPProductBOMLine> list = new Query(product.getCtx(), MPPProductBOMLine.Table_Name, whereClause, product.get_TrxName())
								.setClient_ID()
								.setOnlyActiveRecords(true)
								.setOrderBy(MPPProductBOMLine.COLUMNNAME_Line)
								.list();
		MPPProductBOMLine[] retValue = new MPPProductBOMLine[list.size()];
		list.toArray(retValue);
		return retValue;
	}
	
    /**
     * UUID based Constructor
     * @param ctx  Context
     * @param PP_Product_BOMLine_UU  UUID key
     * @param trxName Transaction
     */
    public MPPProductBOMLine(Properties ctx, String PP_Product_BOMLine_UU, String trxName) {
        super(ctx, PP_Product_BOMLine_UU, trxName);
    }

	/**
	 *  Default Constructor
	 *  @param ctx context
	 *  @param PP_Product_BOMLine  BOM line to load
	 *  @param trxName
	 */
	public MPPProductBOMLine(Properties ctx, int PP_Product_BOMLine, String trxName)
	{
		super(ctx, PP_Product_BOMLine, trxName);
	} //	MPPProductBOMLine

	/**
	 *  Parent Constructor.
	 *  @param  bom parent BOM
	 */
	public MPPProductBOMLine(MPPProductBOM bom)
	{
		super(bom.getCtx(), 0, bom.get_TrxName());
		if (bom.get_ID() <= 0)
			throw new IllegalArgumentException("Header not saved");
		setPP_Product_BOM_ID(bom.getPP_Product_BOM_ID()); //	parent
	}

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

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

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

	/**
	 * Copy constructor
	 * @param ctx
	 * @param copy
	 * @param trxName
	 */
	public MPPProductBOMLine(Properties ctx, MPPProductBOMLine copy, String trxName) 
	{
		this(ctx, 0, trxName);
		copyPO(copy);
		this.m_bom = copy.m_bom != null ? new MPPProductBOM(ctx, copy.m_bom, trxName) : null;
	}

	/**
	 * @param ctx
	 * @param PP_Product_BOMLine_ID
	 * @param trxName
	 * @param virtualColumns
	 */
	public MPPProductBOMLine(Properties ctx, int PP_Product_BOMLine_ID, String trxName, String... virtualColumns) {
		super(ctx, PP_Product_BOMLine_ID, trxName, virtualColumns);
	}

	/**
	 * Get Low Level of parent Product
	 * @return low level
	 */
	public int getLowLevel()
	{
		I_M_Product parent = getPP_Product_BOM().getM_Product();
		if (parent.getLowLevel() > 0)
			return parent.getLowLevel()+1;
		else
			return 1;
	}

	/**
	 * get Parent BOM
	 * @return parent bom
	 */
	public MPPProductBOM getParent()
	{
		if(m_bom == null)
		{	
			m_bom = new MPPProductBOM (getCtx(), this.getPP_Product_BOM_ID(), get_TrxName());
		}
		return m_bom;
	}
	
	/**
	 * Get updateable copy of component product
	 * @return
	 */
	public MProduct getProduct()
	{
		return MProduct.getCopy(getCtx(), getM_Product_ID(), get_TrxName());
	}
	
	/**
	 * Calculate Low Level of a Product
	 * @param ctx
	 * @param M_Product_ID Product ID
	 * @param trxName
	 * @return int low level
	 */
	public static int getLowLevel(Properties ctx, int M_Product_ID, String trxName)
	{
		return MProduct.get(ctx, M_Product_ID, trxName).getLowLevel();
	}

	
	@Override
	protected boolean beforeSave(boolean newRecord)
	{
		// For Co/By Products, Qty should be always negative:
		if (isCoProduct() && getQty(false).signum() >= 0)
		{
			throw new AdempiereException("@Qty@ > 0");
		}

		// Update Line#
		if (getLine() <= 0)
		{
			final String sql = "SELECT COALESCE(MAX("+COLUMNNAME_Line+"),0) + 10 FROM "+Table_Name
								+" WHERE "+COLUMNNAME_PP_Product_BOM_ID+"=?";
			int line = DB.getSQLValueEx(get_TrxName(), sql, getPP_Product_BOM_ID());
			setLine(line);
		}
		
		return true;
	}

	@Override
	protected boolean afterSave(boolean newRecord, boolean success)
	{
		if (!success)
			return false;

		// Update LowLevel of product
		int lowlevel = getLowLevel();
		MProduct product = new MProduct(getCtx(), getM_Product_ID(), get_TrxName());
		if (lowlevel > product.getLowLevel())
		{
			product.setLowLevel(lowlevel);
			product.saveEx();
		}
		
		// Reset IsVerified flag of parent product
		MPPProductBOM bom = getParent();
		MProduct parentProduct = (MProduct) bom.getM_Product();
		if (parentProduct.isVerified())
		{
			MPPProductBOM defaultBOM = MPPProductBOM.getDefault(parentProduct, get_TrxName());
			if (defaultBOM != null && defaultBOM.getPP_Product_BOM_ID()==bom.getPP_Product_BOM_ID())
			{
				if (   newRecord 
					|| is_ValueChanged("M_Product_ID") //	Product Line was changed
					|| (is_ValueChanged("IsActive") && isActive())) // line was activated
				{
					//	Invalidate BOM
					parentProduct.setIsVerified(false);
					parentProduct.saveEx(get_TrxName());
				}
				if (parentProduct.isVerified() && is_ValueChanged("IsActive") && !isActive()) // line was inactivated 
				{  
					MPPProductBOMLine[] lines = bom.getLines(true);
					int count = 0;
					for (MPPProductBOMLine line : lines) 
					{
						if (line.getPP_Product_BOMLine_ID() == getPP_Product_BOMLine_ID())
							continue;
						if (line.isActive())
							count++;
					}
					if (count == 0) 
					{
						parentProduct.setIsVerified(false);
						parentProduct.saveEx(get_TrxName());
					}
				}
			}
		}
		
		return true;
	}
	
	/**
	 * Is BOM line valid for date
	 * @param date
	 * @return true if BOM line is valid for date
	 */
	public boolean isValidFromTo(Timestamp date)
	{
		Timestamp validFrom = getValidFrom();
		Timestamp validTo = getValidTo();
		
		if (validFrom != null && date.before(validFrom))
			return false;
		if (validTo != null && date.after(validTo))
			return false;
		return true;
	}
	
	/**
	 * Is COMPONENTTYPE_By_Product
	 * @return true if it is COMPONENTTYPE_By_Product
	 */
	public boolean isByProduct()
	{
		String componentType = getComponentType();
		return COMPONENTTYPE_By_Product.equals(componentType);
	}
	
	/**
	 * Is COMPONENTTYPE_Co_Product
	 * @return true if it is COMPONENTTYPE_Co_Product
	 */
	public boolean isCoProduct()
	{
		String componentType = getComponentType();
		return COMPONENTTYPE_Co_Product.equals(componentType);
	}
	
	/**
	 * Return absolute (unified) quantity value.<br/>
	 * If IsQtyPercentage then QtyBatch / 100 will be returned.
	 * Else QtyBOM will be returned.
	 * @param includeScrapQty if true, scrap qty will be used for calculating qty 
	 * @return qty
	 */
	public BigDecimal getQty(boolean includeScrapQty)
	{
		int precision = getPrecision();
		BigDecimal qty;
		if (isQtyPercentage())
		{
			precision += 2;
			qty = getQtyBatch().divide(Env.ONEHUNDRED, precision, RoundingMode.HALF_UP);
		}
		else
		{
			qty = getQtyBOM();
		}
		//
		if (includeScrapQty)
		{
			BigDecimal scrapDec = getScrap().divide(Env.ONEHUNDRED, 12, RoundingMode.UP);
			qty = qty.divide(Env.ONE.subtract(scrapDec), precision, RoundingMode.HALF_UP);
		}
		//
		if (qty.scale() > precision)
		{
			qty = qty.setScale(precision, RoundingMode.HALF_UP);
		}
		//
		return qty;
	}

	/**
	 * Like {@link #getQty(boolean)}, includeScrapQty = false
	 */
	public BigDecimal getQty()
	{
		return getQty(false);
	}
	
	/**
	 * Get UOM precision
	 * @return UOM precision
	 */
	public int getPrecision()
	{
		return MUOM.getPrecision(getCtx(), getC_UOM_ID());
	}
	
	/**
	 * Get cost allocation percentage
	 * @param fallback use QtyBOM/QtyPercentage if CostAllocationPerc is zero 
	 * @return co-product cost allocation percent (i.e. -1/qty)
	 */
	public BigDecimal getCostAllocationPerc(boolean fallback)
	{
		BigDecimal allocationPercent = super.getCostAllocationPerc();
		if (allocationPercent.signum() != 0)
			return allocationPercent;
		//
		// Fallback and try to calculate it from Qty
		if (fallback)
		{
			BigDecimal qty = getQty(false).negate();
			if (qty.signum() != 0)
			{
				allocationPercent = Env.ONE.divide(qty, 4, RoundingMode.HALF_UP);
			}
		}
		return allocationPercent;
	}
	
	@Override
	public MPPProductBOMLine markImmutable() {
		if (is_Immutable())
			return this;

		makeImmutable();
		return this;
	}

}
