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.getOriginalPrice() < o2.getOriginalPrice() ? 1
050                                        : o1.getOriginalPrice() == o2.getOriginalPrice() ? 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++, i + 1);
058                si.setSerialLast(i == item.getQuantity() - 1);
059                flats.add(si);
060            }
061        }
062        return flats;
063    }
064
065    private static <T> List<SingleItem<T>> rearrange(List<SingleItem<T>> items) {
066        SingleItem preSingle = null;
067        IItem preitem = null;
068        int serialNum = 1, sequenceNum = 1;
069        List<SingleItem<T>> flats = new ArrayList<>();
070        for (int i = 0; i < items.size(); i++) {
071            SingleItem curritem = items.get(i);
072            if (!curritem.isExclusiveLock()) {
073                if (preitem == null || !preitem.equals(curritem.getItem())) {
074                    preitem = curritem.getItem();
075                    serialNum = 1;
076                    if (preSingle != null) {
077                        preSingle.setSerialLast(true);
078                    }
079                }
080                curritem = new SingleItem(curritem.getItem(), sequenceNum++, serialNum++);
081                flats.add(curritem);
082                preSingle = curritem;
083            }
084        }
085        if (preSingle != null) {
086            preSingle.setSerialLast(true);
087        }
088        return flats;
089    }
090
091    /**
092     * Extract real cart items from a collection.
093     * <br>取出集合中真正的購物品項
094     *
095     * @param <T> type of real cart item.
096     * @param items those wrapped shopping cart items.
097     * @return real cart items
098     */
099    public static <T> List<T> purification(Collection<? extends IItem<T>> items) {
100        List<T> res = new ArrayList<>(items.size());
101        for (IItem<T> item : items) {
102            res.add(item.as());
103        }
104        return res;
105    }
106
107    /**
108     * Caculate bonus from rules and shopping cart items.
109     * <br>從規則與購物品項計算出優惠
110     *
111     * @param <T> type of real cart item.
112     * @param rules Those rules will be applied to caculation.
113     * @param cartItems Shopping cart items
114     * @return Bonus from rules and items.
115     * @throws ScriptException Error prone when quantity measure from a
116     * {@link ILeafRule} meet an error.
117     */
118    public static <T> Collection<BonusItem<T>> calcBonus(List<? extends IRule<T>> rules, Set<? extends IItem<T>> cartItems) throws ScriptException {
119        Map<Serializable, BonusItem<T>> bonuses = new HashMap<>();
120        Collections.sort(rules, new Comparator<IRule<T>>() {
121
122            @Override
123            public int compare(IRule<T> o1, IRule<T> o2) {
124                return o1.getPriority() - o2.getPriority();
125            }
126        });
127        List<SingleItem<T>> items = flat(cartItems);
128        for (int i = 0; i < rules.size(); i++) {
129            IRule<T> rule = rules.get(i);
130            logger.debug("[{}] is going to apply.", rule);
131            if (iterate(rule, items, bonuses) && i != rules.size() - 1) {
132                items = rearrange(items);
133            }
134        }
135        return bonuses.values();
136    }
137
138    private static <T> void unlockItems(Collection<SingleItem<T>> items) {
139        for (SingleItem<T> item : items) {
140            item.setExclusiveLock(false);
141        }
142        items.clear();
143    }
144
145    private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses) throws ScriptException {
146        return iterate(rule, cartItems, bonuses, false);
147    }
148
149    private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses, boolean backOnTriggered) throws ScriptException {
150        List<SingleItem<T>> lockedItems = new ArrayList<>();
151        boolean hasPromote = false;
152        if (rule.isLeaf()) {
153            ILeafRule<T> lrule = ((ILeafRule<T>) rule);
154            int idx = 0;
155            for (SingleItem<T> item : cartItems) {
156                if (!item.isExclusiveLock() && rule.contains(item)) {
157                    BigDecimal op = BigDecimal.valueOf(item.getOriginalPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
158                    BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
159                    logger.debug("\t inspect [{}-{}].", ++idx, item);
160                    lockedItems.add(item.setExclusiveLock(true));
161                    rule.containsCountInc().sumOfContainsOriginalPriceInc(op).sumOfContainsSalePriceInc(sp)
162                            .sumOfSerialOriginalPriceInc(op).sumOfSerialSalePriceInc(sp);
163                    if (rule.isTriggered(item)) {
164                        double quantity = lrule.evalQuantity();
165                        if (quantity > 0) {
166                            IItem<T> ruleBonus = lrule.getBonus();
167                            Serializable primaryKey = String.format("%d-%s", rule.hashCode(), CurrentItem.isCurrent(ruleBonus) ? item.getIdentity() : ruleBonus.getIdentity());
168                            BonusItem<T> bonus = bonuses.get(primaryKey);
169                            if (bonus == null) {
170                                IRule<T> topMostRule = rule.getPrevious() == null ? rule : rule.getPrevious();
171                                while (topMostRule.getPrevious() != null) {
172                                    topMostRule = topMostRule.getPrevious();
173                                }
174                                bonus = new BonusItem<>(topMostRule, CurrentItem.isCurrent(ruleBonus) ? lrule.getCurrentAsBonus(item) : lrule.getBonus());
175                                bonuses.put(primaryKey, bonus);
176                                logger.debug("init bonus[{}:{}]", primaryKey, bonus);
177                            }
178                            if (lrule.isLastQuantityOnly()) {
179                                bonus.setFracQuantity(quantity);
180                            } else {
181                                bonus.incFracQuantity(quantity);
182                            }
183                            lockedItems.clear();
184                            hasPromote = true;
185                            if (hasPromote && backOnTriggered) {
186                                break;
187                            }
188                        }
189                    }
190                    if (item.isSerialLast()) {
191                        rule.resetSumOfSerialOriginalPrice();
192                        rule.resetSumOfSerialSalePrice();
193                    }
194                }
195            }
196        } else {
197            int idx = 0;
198            for (SingleItem<T> item : cartItems) {
199                if (!item.isExclusiveLock() && rule.contains(item)) {
200                    BigDecimal op = BigDecimal.valueOf(item.getOriginalPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
201                    BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN);
202                    logger.debug("\t inspect [{}-{}].", ++idx, item);
203                    lockedItems.add(item.setExclusiveLock(true));
204                    rule.containsCountInc().sumOfContainsOriginalPriceInc(op).sumOfContainsSalePriceInc(sp)
205                            .sumOfSerialOriginalPriceInc(op).sumOfSerialSalePriceInc(sp);
206                    if (rule.isTriggered(item)) {
207                        if (iterate(((IChainRule<T>) rule).getNext(), cartItems, bonuses, true)) {
208                            lockedItems.clear();
209                            hasPromote = true;
210                            if (hasPromote && backOnTriggered) {
211                                break;
212                            }
213                        }
214                    }
215                    if (item.isSerialLast()) {
216                        rule.resetSumOfSerialOriginalPrice();
217                        rule.resetSumOfSerialSalePrice();
218                    }
219                }
220                if (hasPromote) {
221                    rule.resetSumOfPrice();
222                }
223            }
224        }
225        unlockItems(lockedItems);
226        if (hasPromote && backOnTriggered) {
227            rule.resetSumOfPrice();
228        }
229        return hasPromote;
230    }
231}