//
// (c) 2006 DS Data Systems UK Ltd, All rights reserved.
//
// DS Data Systems and KonaKart and their respective logos, are 
// trademarks of DS Data Systems UK Ltd. All rights reserved.
//
// The information in this document is free software; you can redistribute 
// it and/or modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// 
// This software 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
// Lesser General Public License for more details.
//

package com.konakart.bl.modules.shipping.zones;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;

import org.apache.torque.TorqueException;

import com.konakart.app.KKConfiguration;
import com.konakart.app.Country;
import com.konakart.app.KKEng;
import com.konakart.app.KKException;
import com.konakart.app.Order;
import com.konakart.app.ShippingQuote;
import com.konakart.bl.modules.BaseModule;
import com.konakart.bl.modules.shipping.BaseShippingModule;
import com.konakart.bl.modules.shipping.ShippingInfo;
import com.konakart.bl.modules.shipping.ShippingInterface;
import com.konakart.bl.modules.shipping.WeightCost;
import com.workingdogs.village.DataSetException;

/**
 * This shipping module implements a rate per item weight per zone . The items passed to this module
 * have already been split up into individual packages based on the maximum weight allowed per
 * single package. If MODULE_SHIPPING_ZONES_TAX_CLASS is greater than zero, then tax is added if the
 * shipping address is in a taxable zone. The handling charge defined by
 * MODULE_SHIPPING_ZONES_HANDLING_ is also added.
 * 
 */
public class Zones extends BaseShippingModule implements ShippingInterface
{
    private static HashMap countryToZoneMap = null;

    private static HashMap zoneToWeightCostMap = null;

    private static HashMap zoneToHandlingMap = null;

    private static int sortOrder = -1;

    private static int taxClass;

    private static String code = "zones";

    private static String icon = "";

    private static String bundleName = BaseModule.basePackage + ".shipping.zones.Zones";

    private static HashMap resourceBundleMap = new HashMap();

    private static String mutex = "zonesMutex";

    private KKEng eng;

    // Configuration Keys

    private final static String MODULE_SHIPPING_ZONES_COUNTRIES_ = "MODULE_SHIPPING_ZONES_COUNTRIES_";

    private final static String MODULE_SHIPPING_ZONES_COST_ = "MODULE_SHIPPING_ZONES_COST_";

    private final static String MODULE_SHIPPING_ZONES_HANDLING_ = "MODULE_SHIPPING_ZONES_HANDLING_";

    private final static String MODULE_SHIPPING_ZONES_SORT_ORDER = "MODULE_SHIPPING_ZONES_SORT_ORDER";

    private final static String MODULE_SHIPPING_ZONES_TAX_CLASS = "MODULE_SHIPPING_ZONES_TAX_CLASS";

    private final static String MODULE_SHIPPING_ZONES_STATUS = "MODULE_SHIPPING_ZONES_STATUS";

    // Message Catalogue Keys

    private final static String MODULE_SHIPPING_ZONES_TEXT_TITLE = "module.shipping.zones.text.title";

    private final static String MODULE_SHIPPING_ZONES_TEXT_DESCRIPTION = "module.shipping.zones.text.description";

    private final static String MODULE_SHIPPING_ZONES_TEXT_WAY = "module.shipping.zones.text.way";

    private final static String MODULE_SHIPPING_ZONES_TEXT_UNITS = "module.shipping.zones.text.units";

    private final static String MODULE_SHIPPING_ZONES_INVALID_ZONE = "module.shipping.zones.invalid.zone";

    // private final static String MODULE_SHIPPING_ZONES_UNDEFINED_RATE =
    // "module.shipping.zones.undefined.rate";

    /**
     * Constructor
     * 
     * @throws DataSetException
     * @throws KKException
     * @throws TorqueException
     * 
     */
    public Zones() throws TorqueException, KKException, DataSetException
    {
        // Instantiate an object to interface to the main engine
        eng = new KKEng();

        // Create the static maps from the configuration info
        if (sortOrder == -1)
        {
            synchronized (mutex)
            {
                if (sortOrder == -1)
                {
                    setStaticVariables();
                }
            }
        }

    }

    /**
     * From the ShippingCountry we find the iso code of the country and determine its zone.
     * 
     * @param order
     * @return Returns a ShippingQuote object
     * @throws KKException
     */
    public ShippingQuote getQuote(Order order, ShippingInfo info) throws Exception
    {
        // Get the resource bundle
        ResourceBundle rb = getResourceBundle(mutex, bundleName, resourceBundleMap, info
                .getLocale());
        if (rb == null)
        {
            throw new KKException("A resource file cannot be found for the country "
                    + info.getLocale().getCountry());
        }

        // Get a partially filled ShippingQuote object
        ShippingQuote quote = this.getShippingQuote(rb);

        // Get the country object for the shipping country name
        Country shippingCountry = eng.getCountryPerName(order.getDeliveryCountry());
        if (shippingCountry == null)
        {
            throw new KKException("A country matching the name " + order.getDeliveryCountry()
                    + " cannot be found in the database");
        }

        // Get the zone
        Integer Zone = (Integer) countryToZoneMap.get(shippingCountry.getIsoCode2());
        if (Zone == null)
        {
            quote.setResponseText(rb.getString(MODULE_SHIPPING_ZONES_INVALID_ZONE));
            throw new KKException("The delivery address is not within a valid shipping zone");
        }

        // Get the weight cost list for this zone
        List weightCostList = (List) zoneToWeightCostMap.get(Zone);

        // Go through the weight cost list and figure out the cost
        BigDecimal cost = new BigDecimal(0);

        // There is a list of weights since the total order weight may exceed the maximum weight for
        // a single package and so it has already been split up by the manager calling this module.
        for (Iterator iter = info.getOrderWeightList().iterator(); iter.hasNext();)
        {
            BigDecimal totalWeight = ((BigDecimal) iter.next()).add(info.getBoxWeight());
            for (Iterator iter1 = weightCostList.iterator(); iter1.hasNext();)
            {
                WeightCost wc = (WeightCost) iter1.next();
                if (totalWeight.compareTo(wc.getWeight()) == -1)
                {
                    cost = cost.add(wc.getCost());
                    break;
                }
            }
        }

        // Set all of the cost attributes
        BigDecimal handlingCost = (BigDecimal) zoneToHandlingMap.get(Zone);
        quote.setCost(cost);
        quote.setHandlingCost(handlingCost);
        BigDecimal costPlusHandling = cost.add(handlingCost);
        if (taxClass > 0 && info.getDeliveryZone() != null)
        {
            quote.setTax(eng.getTax(costPlusHandling, info.getDeliveryCountry().getId(), info
                    .getDeliveryZone().getZoneId(), taxClass));
            quote.setTotalExTax(costPlusHandling);
            quote.setTotalIncTax(quote.getTax().add(costPlusHandling));
        } else
        {
            quote.setTax(new BigDecimal(0));
            quote.setTotalExTax(costPlusHandling);
            quote.setTotalIncTax(costPlusHandling);
        }

        // Create the return string Shipping to IT : 17 lb(s)
        StringBuffer retTextBuf = new StringBuffer();
        retTextBuf.append(rb.getString(MODULE_SHIPPING_ZONES_TEXT_WAY));
        retTextBuf.append(" ");
        retTextBuf.append(shippingCountry.getIsoCode2());
        retTextBuf.append(" : ");
        for (Iterator iter = info.getOrderWeightList().iterator(); iter.hasNext();)
        {
            retTextBuf.append(((BigDecimal) iter.next()).add(info.getBoxWeight()));
            retTextBuf.append(" ");
            retTextBuf.append(rb.getString(MODULE_SHIPPING_ZONES_TEXT_UNITS));
            retTextBuf.append(" ");
        }
        retTextBuf.deleteCharAt(retTextBuf.length() - 1);
        quote.setResponseText(retTextBuf.toString());

        return quote;
    }

    /**
     * From the configuration data, we fill a static hash table with the mapping info
     * 
     * @throws KKException
     * 
     */
    @SuppressWarnings("unchecked")
    private void createCountryToZoneMap() throws KKException
    {

        Integer zone = new Integer(1);
        boolean keepLooping = true;
        do
        {
            KKConfiguration conf = eng.getConfiguration(MODULE_SHIPPING_ZONES_COUNTRIES_
                    + zone.intValue());
            if (conf != null)
            {
                String countries = conf.getValue();
                String[] countriesArray = countries.split(",");
                for (int i = 0; i < countriesArray.length; i++)
                {
                    countryToZoneMap.put(countriesArray[i], zone);
                }
                zone = new Integer(zone.intValue() + 1);
            } else
            {
                keepLooping = false;
            }
        } while (keepLooping);
    }

    /**
     * Sets some static variables during setup
     * 
     * @throws KKException
     * 
     */
    public void setStaticVariables() throws KKException
    {
        KKConfiguration conf;

        conf = eng.getConfiguration(MODULE_SHIPPING_ZONES_SORT_ORDER);
        if (conf == null)
        {
            sortOrder = 0;
        } else
        {
            sortOrder = new Integer(conf.getValue()).intValue();
        }

        conf = eng.getConfiguration(MODULE_SHIPPING_ZONES_TAX_CLASS);
        if (conf == null)
        {
            taxClass = 0;
        } else
        {
            taxClass = new Integer(conf.getValue()).intValue();
        }

        // Create the static maps from the configuration info
        if (countryToZoneMap == null)
        {
            countryToZoneMap = new HashMap();
        } else
        {
            countryToZoneMap.clear();
        }
        createCountryToZoneMap();

        if (zoneToWeightCostMap == null)
        {
            zoneToWeightCostMap = new HashMap();
        } else
        {
            zoneToWeightCostMap.clear();
        }
        createZoneToWeightCostMap();

        if (zoneToHandlingMap == null)
        {
            zoneToHandlingMap = new HashMap();
        } else
        {
            zoneToHandlingMap.clear();
        }
        createZoneToHandlingMap();
    }

    /**
     * From the configuration data, we fill a static hash table with the mapping info of zone to
     * handling charge
     * 
     * @throws KKException
     * 
     */
    @SuppressWarnings("unchecked")
    private void createZoneToHandlingMap() throws KKException
    {

        Integer zone = new Integer(1);
        boolean keepLooping = true;
        do
        {
            KKConfiguration conf = eng.getConfiguration(MODULE_SHIPPING_ZONES_HANDLING_
                    + zone.intValue());
            if (conf != null)
            {
                String handling = conf.getValue();
                zoneToHandlingMap.put(zone, new BigDecimal(handling));
                zone = new Integer(zone.intValue() + 1);
            } else
            {
                keepLooping = false;
            }
        } while (keepLooping);
    }

    /**
     * From the configuration data, we fill a static hash table with the mapping info
     * 
     * @throws KKException
     * 
     */
    @SuppressWarnings("unchecked")
    private void createZoneToWeightCostMap() throws KKException
    {

        Integer zone = new Integer(1);
        boolean keepLooping = true;
        do
        {
            KKConfiguration conf = eng.getConfiguration(MODULE_SHIPPING_ZONES_COST_
                    + zone.intValue());
            if (conf != null)
            {
                List weightCostList = new ArrayList();
                String weightCosts = conf.getValue();
                String[] weightCostsArray = weightCosts.split(",");
                for (int i = 0; i < weightCostsArray.length; i++)
                {
                    String weightCost = weightCostsArray[i];
                    String[] weightCostArray = weightCost.split(":");
                    if (weightCostArray.length == 2)
                    {
                        WeightCost wc = new WeightCost(new BigDecimal(weightCostArray[0]),
                                new BigDecimal(weightCostArray[1]));
                        weightCostList.add(wc);
                    }
                }
                zoneToWeightCostMap.put(zone, weightCostList);
                zone = new Integer(zone.intValue() + 1);
            } else
            {
                keepLooping = false;
            }
        } while (keepLooping);
    }

    /**
     * 
     * @param rb
     * @return A ShippingQuote object
     * @throws KKException
     */
    private ShippingQuote getShippingQuote(ResourceBundle rb) throws KKException
    {

        ShippingQuote quote = new ShippingQuote();

        // Populate some attributes from static data
        quote.setCode(code);
        quote.setSortOrder(sortOrder);
        quote.setIcon(icon);
        quote.setTaxClass(taxClass);

        // Populate locale specific attributes from the resource bundle
        quote.setDescription(rb.getString(MODULE_SHIPPING_ZONES_TEXT_DESCRIPTION));
        quote.setTitle(rb.getString(MODULE_SHIPPING_ZONES_TEXT_TITLE));

        return quote;
    }

    /**
     * Returns true or false
     * 
     * @throws KKException
     */
    public boolean isAvailable() throws KKException
    {
        return isAvailable(eng, MODULE_SHIPPING_ZONES_STATUS);
    }

}
