/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *******************************************************************************/
package org.apache.ofbiz.webpos.transaction;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.GeneralException;
import org.apache.ofbiz.base.util.UtilDateTime;
import org.apache.ofbiz.base.util.UtilGenerics;
import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.util.EntityUtil;
import org.apache.ofbiz.order.shoppingcart.CheckOutHelper;
import org.apache.ofbiz.order.shoppingcart.ShoppingCart;
import org.apache.ofbiz.order.shoppingcart.ShoppingCartItem;
import org.apache.ofbiz.order.shoppingcart.ShoppingCart.CartPaymentInfo;
import org.apache.ofbiz.product.store.ProductStoreWorker;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.service.ServiceUtil;
import org.apache.ofbiz.webpos.session.WebPosSession;

public class WebPosTransaction {

    public static final String resource = "WebPosUiLabels";
    public static final String module = WebPosTransaction.class.getName();
    public static final int NO_PAYMENT = 0;
    public static final int INTERNAL_PAYMENT = 1;
    public static final int EXTERNAL_PAYMENT = 2;

    private CheckOutHelper ch = null;
    private GenericValue txLog = null;
    private String transactionId = null;
    private String orderId = null;
    private String partyId = null;
    private boolean isOpen = false;
    private int drawerIdx = 0;
    private GenericValue shipAddress = null;
    private WebPosSession webPosSession = null;

    public WebPosTransaction(WebPosSession session) {
        this.webPosSession = session;
        this.partyId = "_NA_";
        Delegator delegator = session.getDelegator();
        ShoppingCart cart = session.getCart();
        this.ch = new CheckOutHelper(session.getDispatcher(), delegator, cart);
        cart.setChannelType("POS_SALES_CHANNEL");
        cart.setFacilityId(session.getFacilityId());
        cart.setTerminalId(session.getId());

        if (session.getUserLogin() != null) {
            cart.addAdditionalPartyRole(session.getUserLogin().getString("partyId"), "SALES_REP");
        }

        // setup the TX log
        this.transactionId = delegator.getNextSeqId("PosTerminalLog");
        txLog = delegator.makeValue("PosTerminalLog");
        txLog.set("posTerminalLogId", this.transactionId);
        txLog.set("posTerminalId", session.getId());
        txLog.set("transactionId", transactionId);
        txLog.set("userLoginId", session.getUserLoginId());
        txLog.set("statusId", "POSTX_ACTIVE");
        txLog.set("logStartDateTime", UtilDateTime.nowTimestamp());
        try {
            txLog.create();
            cart.setTransactionId(transactionId);
        } catch (GenericEntityException e) {
            Debug.logError(e, "Unable to create TX log - not fatal", module);
        }

        Debug.logInfo("Created WebPosTransaction [" + this.transactionId + "]", module);
    }

    public String getUserLoginId() {
        return webPosSession.getUserLoginId();
    }

    public int getDrawerNumber() {
        return drawerIdx + 1;
    }

    public String getTransactionId() {
        return this.transactionId;
    }

    public String getTerminalId() {
        return webPosSession.getId();
    }

    public String getFacilityId() {
        return webPosSession.getFacilityId();
    }

    public String getTerminalLogId() {
        return txLog.getString("posTerminalLogId");
    }

    public boolean isOpen() {
        this.isOpen = false;
        GenericValue terminalState = this.getTerminalState();
        if (terminalState != null) {
            if ((terminalState.getDate("closedDate")) == null) {
                this.isOpen = true;
            }
        }
        return this.isOpen;
    }

    public GenericValue getTerminalState() {
        Delegator delegator = webPosSession.getDelegator();
        List<GenericValue> states = null;
        try {
            states = delegator.findList("PosTerminalState", EntityCondition.makeCondition(UtilMisc.toMap("posTerminalId", webPosSession.getId(), "startingTxId", getTransactionId())), null, null, null, false);
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        states = EntityUtil.filterByDate(states, UtilDateTime.nowTimestamp(), "openedDate", "closedDate", true);
        return EntityUtil.getFirst(states);
    }

    public void closeTx() {
        if (UtilValidate.isNotEmpty(txLog)) {
            txLog.set("statusId", "POSTX_CLOSED");
            txLog.set("itemCount", (long) getCart().size());
            txLog.set("logEndDateTime", UtilDateTime.nowTimestamp());
            try {
                txLog.store();
            } catch (GenericEntityException e) {
                Debug.logError(e, "Unable to store TX log - not fatal", module);
            }
            getCart().clear();
            Debug.logInfo("Transaction closed", module);
        }
    }

    public void paidInOut(String type) {
        if (UtilValidate.isNotEmpty(txLog)) {
            txLog.set("statusId", "POSTX_PAID_" + type);
            txLog.set("logEndDateTime", UtilDateTime.nowTimestamp());
            try {
                txLog.store();
            } catch (GenericEntityException e) {
                Debug.logError(e, "Unable to store TX log - not fatal", module);
            }
            webPosSession.setCurrentTransaction(null);
            Debug.logInfo("Paid "+ type, module);
        }
    }

    public void modifyPrice(int cartLineIdx, BigDecimal price) {
        ShoppingCartItem item = getCart().findCartItem(cartLineIdx);
        if (UtilValidate.isNotEmpty(item)) {
            Debug.logInfo("Modify item price " + item.getProductId() + "/" + price, module);
            item.setBasePrice(price);
        } else {
            Debug.logInfo("Item " + cartLineIdx + " not found", module);
        }
    }

    public void calcTax() {
        try {
            ch.calcAndAddTax(this.getStoreOrgAddress());
        } catch (GeneralException e) {
            Debug.logError(e, module);
        }
    }

    public BigDecimal processSale() throws GeneralException {
        Debug.logInfo("Process sale", module);
        BigDecimal grandTotal = this.getGrandTotal();
        BigDecimal paymentAmt = this.getPaymentTotal();
        if (grandTotal.compareTo(paymentAmt) > 0) {
            throw new GeneralException(UtilProperties.getMessage(resource, "WebPosNotEnoughFunds", webPosSession.getLocale()));
        }

        // attach the party ID to the cart
        getCart().setOrderPartyId(partyId);

        // validate payment methods
        Debug.logInfo("Validating payment methods", module);
        Map<String, ? extends Object> valRes = UtilGenerics.cast(ch.validatePaymentMethods());
        if (valRes != null && ServiceUtil.isError(valRes)) {
            throw new GeneralException(ServiceUtil.getErrorMessage(valRes));
        }

        // store the order
        Debug.logInfo("Store order", module);
        Map<String, ? extends Object> orderRes = UtilGenerics.cast(ch.createOrder(webPosSession.getUserLogin()));

        if (orderRes != null && ServiceUtil.isError(orderRes)) {
            throw new GeneralException(ServiceUtil.getErrorMessage(orderRes));
        } else if (orderRes != null) {
            this.orderId = (String) orderRes.get("orderId");
        }

        // process the payment(s)
        Debug.logInfo("Processing the payment(s)", module);
        Map<String, ? extends Object> payRes = null;
        try {
            payRes = UtilGenerics.cast(ch.processPayment(ProductStoreWorker.getProductStore(webPosSession.getProductStoreId(), webPosSession.getDelegator()), webPosSession.getUserLogin(), true));
        } catch (GeneralException e) {
            Debug.logError(e, module);
            throw e;
        }

        if (payRes != null && ServiceUtil.isError(payRes)) {
            throw new GeneralException(ServiceUtil.getErrorMessage(payRes));
        }

        // get the change due
        BigDecimal change = grandTotal.subtract(paymentAmt);

        // TODO notify the change due

        // threaded drawer/receipt printing
        // TODO open the drawer
        // TODO print the receipt

        // save the TX Log
        txLog.set("statusId", "POSTX_SOLD");
        txLog.set("orderId", orderId);
        txLog.set("itemCount", (long) getCart().size());
        txLog.set("logEndDateTime", UtilDateTime.nowTimestamp());
        try {
            txLog.store();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Unable to store TX log - not fatal", module);
        }

        return change;
    }

    private synchronized GenericValue getStoreOrgAddress() {
        if (UtilValidate.isEmpty(this.shipAddress)) {
            // locate the store's physical address - use this for tax
            GenericValue facility = null;
            try {
                facility = webPosSession.getDelegator().findOne("Facility", UtilMisc.toMap("facilityId", webPosSession.getFacilityId()), false);
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
            if (UtilValidate.isEmpty(facility)) {
                return null;
            }

            List<GenericValue> fcp = null;
            try {
                fcp = facility.getRelated("FacilityContactMechPurpose", UtilMisc.toMap("contactMechPurposeTypeId", "SHIP_ORIG_LOCATION"), null, false);
            } catch (GenericEntityException e) {
                Debug.logError(e, module);
            }
            fcp = EntityUtil.filterByDate(fcp);
            GenericValue purp = EntityUtil.getFirst(fcp);
            if (UtilValidate.isNotEmpty(purp)) {
                try {
                    this.shipAddress = webPosSession.getDelegator().findOne("PostalAddress",
                            UtilMisc.toMap("contactMechId", purp.getString("contactMechId")), false);
                } catch (GenericEntityException e) {
                    Debug.logError(e, module);
                }
            }
        }
        return this.shipAddress;
    }

    public void clearPayments() {
        Debug.logInfo("all payments cleared from sale", module);
        getCart().clearPayments();
    }

    public void clearPayment(int index) {
        Debug.logInfo("removing payment " + index, module);
        getCart().clearPayment(index);
    }

    public void clearPayment(String id) {
        Debug.logInfo("removing payment " + id, module);
        getCart().clearPayment(id);
    }

    public CartPaymentInfo getPaymentInfo(int index) {
        return getCart().getPaymentInfo(index);
    }

    public String getPaymentMethodTypeId(int index) {
        return getCart().getPaymentInfo(index).paymentMethodTypeId;
    }

    public int checkPaymentMethodType(String paymentMethodTypeId) {
        Map<String, ? extends Object> fields = UtilMisc.toMap("paymentMethodTypeId", paymentMethodTypeId, "productStoreId", webPosSession.getProductStoreId());
        List<GenericValue> values = null;
        try {
            values = webPosSession.getDelegator().findList("ProductStorePaymentSetting", EntityCondition.makeCondition(fields), null, null, null, true);
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }

        final String externalCode = "PRDS_PAY_EXTERNAL";
        if (UtilValidate.isEmpty(values)) {
            return NO_PAYMENT;
        } else {
            boolean isExternal = true;
            Iterator<GenericValue> i = values.iterator();
            while (i.hasNext() && isExternal) {
                GenericValue v = i.next();
                //Debug.logInfo("Testing [" + paymentMethodTypeId + "] - " + v, module);
                if (!externalCode.equals(v.getString("paymentServiceTypeEnumId"))) {
                    isExternal = false;
                }
            }

            if (isExternal) {
                return EXTERNAL_PAYMENT;
            } else {
                return INTERNAL_PAYMENT;
            }
        }
    }

    public static int getNoPaymentCode() {
        return NO_PAYMENT;
    }

    public static int getExternalPaymentCode() {
        return EXTERNAL_PAYMENT;
    }

    public static int getInternalPaymentCode() {
        return INTERNAL_PAYMENT;
    }

    public BigDecimal addPayment(String id, BigDecimal amount) {
        return this.addPayment(id, amount, null, null);
    }

    public BigDecimal addPayment(String id, BigDecimal amount, String refNum, String authCode) {
        Debug.logInfo("Added payment " + id + "/" + amount, module);
        if ("CASH".equals(id)) {
            // clear cash payments first; so there is only one
            getCart().clearPayment(id);
        }
        getCart().addPaymentAmount(id, amount, refNum, authCode, true, true, false);
        return this.getTotalDue();
    }

    public BigDecimal processAmount(BigDecimal amount) throws GeneralException {
        if (UtilValidate.isEmpty(amount)) {
            Debug.logInfo("Amount is empty; assumption is full amount : " + this.getTotalDue(), module);
            amount = this.getTotalDue();
            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
                throw new GeneralException();
            }
        }
        return amount;
    }

    public synchronized void processNoPayment(String paymentMethodTypeId) {
        try {
            BigDecimal amount = processAmount(null);
            Debug.logInfo("Processing [" + paymentMethodTypeId + "] Amount : " + amount, module);

            // add the payment
            addPayment(paymentMethodTypeId, amount, null, null);
        } catch (GeneralException e) {
            // errors handled
        }
    }

    public synchronized void processExternalPayment(String paymentMethodTypeId, BigDecimal amount, String refNum) {
        if (refNum == null) {
            //TODO handle error message
            return;
        }

        try {
            amount = processAmount(amount);
            Debug.logInfo("Processing [" + paymentMethodTypeId + "] Amount : " + amount, module);

            // add the payment
            addPayment(paymentMethodTypeId, amount, refNum, null);
        } catch (GeneralException e) {
            // errors handled
        }
    }

    public String makeCreditCardVo(String cardNumber, String expDate, String firstName, String lastName) {
        LocalDispatcher dispatcher = webPosSession.getDispatcher();
        String expMonth = expDate.substring(0, 2);
        String expYear = expDate.substring(2);
        // two digit year check -- may want to re-think this
        if (expYear.length() == 2) {
            expYear = "20" + expYear;
        }

        Map<String, Object> svcCtx = new HashMap<String, Object>();
        svcCtx.put("userLogin", webPosSession.getUserLogin());
        svcCtx.put("partyId", partyId);
        svcCtx.put("cardNumber", cardNumber);
        svcCtx.put("firstNameOnCard", firstName == null ? "" : firstName);
        svcCtx.put("lastNameOnCard", lastName == null ? "" : lastName);
        svcCtx.put("expMonth", expMonth);
        svcCtx.put("expYear", expYear);
        svcCtx.put("cardType", UtilValidate.getCardType(cardNumber));

        Map<String, Object> svcRes = null;
        try {
            svcRes = dispatcher.runSync("createCreditCard", svcCtx);
        } catch (GenericServiceException e) {
            Debug.logError(e, module);
            return null;
        }
        if (ServiceUtil.isError(svcRes)) {
            Debug.logError(ServiceUtil.getErrorMessage(svcRes) + " - " + svcRes, module);
            return null;
        } else {
            return (String) svcRes.get("paymentMethodId");
        }
    }

    public void setPaymentRefNum(int paymentIndex, String refNum, String authCode) {
        Debug.logInfo("setting payment index reference number " + paymentIndex + " / " + refNum + " / " + authCode, module);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.refNum[0] = refNum;
        inf.refNum[1] = authCode;
    }

    /* CVV2 code should be entered when a card can't be swiped */
    public void setPaymentSecurityCode(String paymentId, String refNum, String securityCode) {
        Debug.logInfo("setting payment security code " + paymentId, module);
        int paymentIndex = getCart().getPaymentInfoIndex(paymentId, refNum);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.securityCode = securityCode;
        inf.isSwiped = false;
    }

    /* Track2 data should be sent to processor when a card is swiped. */
    public void setPaymentTrack2(String paymentId, String refNum, String securityCode) {
        Debug.logInfo("setting payment security code " + paymentId, module);
        int paymentIndex = getCart().getPaymentInfoIndex(paymentId, refNum);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.securityCode = securityCode;
        inf.isSwiped = true;
    }

    /* Postal code should be entered when a card can't be swiped */
    public void setPaymentPostalCode(String paymentId, String refNum, String postalCode) {
        Debug.logInfo("setting payment security code " + paymentId, module);
        int paymentIndex = getCart().getPaymentInfoIndex(paymentId, refNum);
        ShoppingCart.CartPaymentInfo inf = getCart().getPaymentInfo(paymentIndex);
        inf.postalCode = postalCode;
    }

    public BigDecimal getTaxTotal() {
        return getCart().getTotalSalesTax();
    }

    public BigDecimal getGrandTotal() {
        return getCart().getGrandTotal();
    }

    public int getNumberOfPayments() {
        return getCart().selectedPayments();
    }

    public BigDecimal getPaymentTotal() {
        return getCart().getPaymentTotal();
    }
    
    public BigDecimal getTotalQuantity() {
        return getCart().getTotalQuantity();
    }

    public BigDecimal getTotalDue() {
        BigDecimal grandTotal = this.getGrandTotal();
        BigDecimal paymentAmt = this.getPaymentTotal();
        return grandTotal.subtract(paymentAmt);
    }

    public String addProductPromoCode(String code) {
        String result = getCart().addProductPromoCode(code, webPosSession.getDispatcher());
        calcTax();
        return result;
    }

    public ShoppingCart getCart() {
        return webPosSession.getCart();
    }
}
