001package scpc;
002
003import java.io.Serializable;
004import java.math.BigDecimal;
005import java.math.RoundingMode;
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.Comparator;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013import java.util.Set;
014import javax.script.ScriptException;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017import scpc.model.BonusItem;
018import scpc.model.CurrentItem;
019import scpc.model.IChainRule;
020import scpc.model.IItem;
021import scpc.model.ILeafRule;
022import scpc.model.IRule;
023import scpc.model.SingleItem;
024
025/**
026 * Calculator of Shopping cart promotion.
027 *
028 * @author Kent Yeh
029 */
030public class Calculator {
031
032    private static final Logger logger = LoggerFactory.getLogger(Calculator.class);
033
034    /**
035     * Spread shopping cart into single item for iteration.
036     * <br>展開購物車以便逐一檢視單個品項
037     *
038     * @param <T> Type represented of shopping cart item.
039     * @param cartItems shopping cart items.
040     * @return
041     */
042    public static <T> List<SingleItem<T>> flat(Set<? extends IItem<T>> cartItems) {
043        List<IItem<T>> items = new ArrayList<>(cartItems);
044        Collections.sort(items, new Comparator<IItem<T>>() {
045            @Override
046            public int compare(IItem o1, IItem o2) {
047                return o1.getSalePrice() < o2.getSalePrice() ? 1
048                        : o1.getSalePrice() == o2.getSalePrice()
049                                ? o1.getRegularPrice() < o2.getRegularPrice() ? 1
050                                        : o1.getRegularPrice() == o2.getRegularPrice() ? 0 : -1 : -1;
051            }
052        });
053        int idx = 1;
054        List<SingleItem<T>> flats = new ArrayList<>();
055        for (IItem item : items) {
056            for (int i = 0; i < item.getQuantity(); i++) {
057                SingleItem si = new SingleItem(item, idx++);
058                si.setSerialLast(i == item.getQuantity() - 1);
059                flats.add(si);
060            }
061        }
062        return flats;
063    }
064
065    /**
066     * Extract real cart items from a collection.
067     * <br>取出集合中真正的購物品項
068     *
069     * @param <T> type of real cart item.
070     * @param items those wrapped shopping cart items.
071     * @return real cart items
072     */
073    public static <T> List<T> purification(Collection<? extends IItem<T>> items) {
074        List<T> res = new ArrayList<>(items.size());
075        for (IItem<T> item : items) {
076            res.add(item.as());
077        }
078        return res;
079    }
080
081    /**
082     * Caculate bonus from rules and shopping cart items.
083     * <br>從規則與購物品項計算出優惠
084     *
085     * @param <T> type of real cart item.
086     * @param rules Those rules will be applied to caculation.
087     * @param cartItems Shopping cart items
088     * @return Bonus from rules and items.
089     * @throws ScriptException Error prone when quantity measure from a
090     * {@link ILeafRule} meet an error.
091     */
092    public static <T> Collection<BonusItem<T>> calcBonus(List<? extends IRule<T>> rules, Set<? extends IItem<T>> cartItems) throws ScriptException {
093        Map<Serializable, BonusItem<T>> bonuses = new HashMap<>();
094        Collections.sort(rules, new Comparator<IRule<T>>() {
095
096            @Override
097            public int compare(IRule<T> o1, IRule<T> o2) {
098                return o1.getPriority() - o2.getPriority();
099            }
100        });
101        List<SingleItem<T>> items = flat(cartItems);
102        for (int i = 0; i < rules.size(); i++) {
103            IRule<T> rule = rules.get(i);
104            logger.debug("[{}] is going to apply.", rule);
105            if (iterate(rule, items, bonuses) && i != rules.size() - 1) {
106            }
107        }
108        return bonuses.values();
109    }
110
111    private static <T> void unlockItems(Collection<SingleItem<T>> items) {
112        for (SingleItem<T> item : items) {
113            item.setExclusiveLock(false);
114        }
115        items.clear();
116    }
117
118    private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses) throws ScriptException {
119        return iterate(rule, cartItems, bonuses, false);
120    }
121
122    private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses, boolean backOnTriggered) throws ScriptException {
123        List<SingleItem<T>> lockedItems = new ArrayList<>();
124        boolean hasPromote = false;
125        if (rule.isLeaf()) {
126            ILeafRule<T> lrule = ((ILeafRule<T>) rule);
127            int idx = 0;
128            IItem preitem = null;
129            for (SingleItem<T> item : cartItems) {
130                if (!item.isExclusiveLock() && rule.contains(item)) {
131                    BigDecimal op = BigDecimal.valueOf(item.getRegularPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
132                    BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
133                    logger.debug("\t inspect [{}-{}].", ++idx, item);
134                    lockedItems.add(item.setExclusiveLock(true));
135                    rule.containsCountInc().serialNumInc(item.getItem().equals(preitem)).sumOfContainsRegularPriceInc(op).sumOfContainsSalePriceInc(sp)
136                            .sumOfSerialRegularPriceInc(op).sumOfSerialSalePriceInc(sp);
137                    if (rule.isTriggered(item)) {
138                        double quantity = lrule.evalQuantity();
139                        if (quantity > 0) {
140                            IItem<T> ruleBonus = lrule.getBonus();
141                            Serializable primaryKey = String.format("%d-%s", rule.hashCode(), CurrentItem.isCurrent(ruleBonus) ? item.getIdentity() : ruleBonus.getIdentity());
142                            BonusItem<T> bonus = bonuses.get(primaryKey);
143                            if (bonus == null) {
144                                IRule<T> topMostRule = rule.getPrevious() == null ? rule : rule.getPrevious();
145                                while (topMostRule.getPrevious() != null) {
146                                    topMostRule = topMostRule.getPrevious();
147                                }
148                                bonus = new BonusItem<>(topMostRule, CurrentItem.isCurrent(ruleBonus) ? lrule.getCurrentAsBonus(item) : lrule.getBonus());
149                                bonuses.put(primaryKey, bonus);
150                                logger.debug("init bonus[{}:{}]", primaryKey, bonus);
151                            }
152                            if (lrule.isLastQuantityOnly()) {
153                                bonus.setFracQuantity(quantity);
154                            } else {
155                                bonus.incFracQuantity(quantity);
156                            }
157                            lockedItems.clear();
158                            hasPromote = true;
159                            if (hasPromote && backOnTriggered) {
160                                break;
161                            }
162                        }
163                    }
164                    if (item.isSerialLast()) {
165                        rule.resetSumOfSerialRegularPrice();
166                        rule.resetSumOfSerialSalePrice();
167                        preitem = null;
168                    } else {
169                        preitem = item.getItem();
170                    }
171                }
172            }
173        } else {
174            int idx = 0;
175            for (SingleItem<T> item : cartItems) {
176                if (!item.isExclusiveLock() && rule.contains(item)) {
177                    BigDecimal op = BigDecimal.valueOf(item.getRegularPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
178                    BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
179                    logger.debug("\t inspect [{}-{}].", ++idx, item);
180                    lockedItems.add(item.setExclusiveLock(true));
181                    rule.containsCountInc().sumOfContainsRegularPriceInc(op).sumOfContainsSalePriceInc(sp)
182                            .sumOfSerialRegularPriceInc(op).sumOfSerialSalePriceInc(sp);
183                    if (rule.isTriggered(item)) {
184                        if (iterate(((IChainRule<T>) rule).getNext(), cartItems, bonuses, true)) {
185                            lockedItems.clear();
186                            hasPromote = true;
187                            if (hasPromote && backOnTriggered) {
188                                break;
189                            }
190                        }
191                    }
192                    if (item.isSerialLast()) {
193                        rule.resetSumOfSerialRegularPrice();
194                        rule.resetSumOfSerialSalePrice();
195                    }
196                }
197                if (hasPromote) {
198                    rule.resetSumOfPrice();
199                }
200            }
201        }
202        unlockItems(lockedItems);
203        if (hasPromote && backOnTriggered) {
204            rule.resetSumOfPrice();
205        }
206        return hasPromote;
207    }
208}