/*
 * Copyright (C) 2011 4th Line GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.fourthline.konto.shared.entity;

import org.fourthline.konto.shared.Constants;
import org.seamless.gwt.validation.shared.EntityProperty;
import org.fourthline.konto.shared.MonetaryAmount;
import org.seamless.gwt.validation.shared.Validatable;
import org.seamless.gwt.validation.shared.ValidationError;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

/**
 * http://ec.europa.eu/economy_finance/euro/adoption/conversion/index_en.htm
 * <p/>
 * Conversion rates are used for converting euro to national currency units and vice versa.
 * Inverse rates derived from the conversion rates may not be used. Converting a national
 * currency unit into the euro unit consists in dividing the amount expressed in the national
 * currency unit by the conversion rate of the euro against this national currency unit.
 * <p/>
 * Examples:
 * 12 euro = 12 * 6.55957 = 78.7148 francs = 78.71 francs
 * 52 francs = 52 / 6.55957 = 7.9273 euro = 7.93 euro
 * <p/>
 * Monetary amounts to be converted from one national currency unit into another shall first
 * be converted into a monetary amount expressed in the euro unit, whose amount may be rounded
 * to not less than three decimals and shall then be converted into the other national currency
 * unit. No alternative method (in particular, that which consists in calculating directly
 * the bilateral conversion rates between national currency units using the euro conversion
 * rates of the latter) may be used unless it produces the same results.
 * <p/>
 * Example:
 * 1 euro = 1.95583 mark
 * 54 francs = 54 / 6.55957 = 8.23224 euro
 * 8.232 euro = 8.232 * 1.95583 = 16.1003 marks = 16.10 marks
 * <p/>
 * http://www.banque-france.fr/gb/eurosys/telechar/docs/arrondis.pdf
 * <br/>
 * http://www.banana.ch/accounting/files/multicurrency_eng.pdf
 *
 * @author Christian Bauer
 */
@Entity
@Table(name = "CURRENCY_PAIR",
       uniqueConstraints = @UniqueConstraint(columnNames = {"FROM_CODE", "TO_CODE", "CREATED_ON"})
)
public class CurrencyPair implements Serializable, Validatable {

    public static final int PRECISION = 10;
    public static final int SCALE = 6;

    public static final BigDecimal DEFAULT_EXCHANGE_RATE =
            new BigDecimal(1).setScale(CurrencyPair.SCALE, RoundingMode.HALF_UP);

    public static enum Property implements EntityProperty {
        id,
        fromCode,
        toCode,
        exchangeRate,
        createdOn
    }

    @Id
    @GeneratedValue(generator = Constants.SEQUENCE_NAME)
    @Column(name = "CURRENCY_PAIR_ID")
    private Long id;

    @Column(name = "FROM_CODE", nullable = false)
    private String fromCode;

    @Column(name = "TO_CODE", nullable = false)
    private String toCode;

    @Column(name = "EXCHANGE_RATE",
            nullable = false,
            precision = CurrencyPair.PRECISION,
            scale = CurrencyPair.SCALE
    )
    private BigDecimal exchangeRate = DEFAULT_EXCHANGE_RATE;

    @Temporal(TemporalType.DATE)
    @Column(name = "CREATED_ON", nullable = false)
    private Date createdOn = new Date();

    @Transient
    private MonetaryUnit fromUnit;

    @Transient
    private MonetaryUnit toUnit;

    public CurrencyPair() {
    }

    public CurrencyPair(String fromCode, String toCode) {
        this(fromCode, toCode, DEFAULT_EXCHANGE_RATE);
    }

    public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit, MonetaryAmount originalAmount, MonetaryAmount exchangedAmount) {
        this(fromUnit.getCurrencyCode(),
             toUnit.getCurrencyCode(),
             originalAmount != null && exchangedAmount != null
                     ? getExchangeRate(originalAmount, exchangedAmount)
                     : DEFAULT_EXCHANGE_RATE
        );
        this.fromUnit = fromUnit;
        this.toUnit = toUnit;
    }

    public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit, String exchangeRate) {
        this(fromUnit.getCurrencyCode(), toUnit.getCurrencyCode(), new BigDecimal(exchangeRate));
        this.fromUnit = fromUnit;
        this.toUnit = toUnit;
    }

    public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit) {
        this(fromUnit, toUnit, DEFAULT_EXCHANGE_RATE);
    }

    public CurrencyPair(MonetaryUnit fromUnit, MonetaryUnit toUnit, BigDecimal exchangeRate) {
        this(fromUnit.getCurrencyCode(), toUnit.getCurrencyCode(), exchangeRate);
        this.fromUnit = fromUnit;
        this.toUnit = toUnit;
    }

    public CurrencyPair(String fromCode, String toCode, BigDecimal exchangeRate) {
        this.fromCode = fromCode;
        this.toCode = toCode;
        setExchangeRate(exchangeRate);
    }

    public CurrencyPair(String fromCode, String toCode, BigDecimal exchangeRate, Date createdOn) {
        this.fromCode = fromCode;
        this.toCode = toCode;
        setExchangeRate(exchangeRate);
        this.createdOn = createdOn;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFromCode() {
        return fromCode;
    }

    public void setFromCode(String fromCode) {
        this.fromCode = fromCode;
    }

    public String getToCode() {
        return toCode;
    }

    public void setToCode(String toCode) {
        this.toCode = toCode;
    }

    public BigDecimal getExchangeRate() {
        return exchangeRate;
    }

    public String getExchangeRateString() {
        // Cut off the remaining zeroes of fraction
        StringBuilder sb = new StringBuilder();
        sb.append(getExchangeRate().toString());
        int dot = sb.lastIndexOf(".");
        for (int i = sb.length() -1; i >= dot && (sb.charAt(i) == '0' || sb.charAt(i) == '.'); i--) {
            sb.deleteCharAt(i);
        }
        return sb.toString();
    }

    public boolean isValidExchangeRate(BigDecimal er) {
        // Make sure it fits within our constraints for the database type etc.
        if (er.precision() > PRECISION)
            return false;
        if (er.signum() <= 0)
            return false;

        return true;
    }

    /**
     * @param er The desired rate.
     * @throws IllegalArgumentException if the rate is not valid (negative, zero, invalid precision, etc.)
     */
    public void setExchangeRate(BigDecimal er) throws IllegalArgumentException {
        if (!isValidExchangeRate(er))
            throw new IllegalArgumentException("Invalid exchange rate '" + getFromCode() + "/" + getToCode() + "': " + er);
        this.exchangeRate = er.setScale(CurrencyPair.SCALE, RoundingMode.HALF_UP);
    }

    public Date getCreatedOn() {
        return createdOn;
    }

    public void setCreatedOn(Date createdOn) {
        this.createdOn = createdOn;
    }

    public MonetaryUnit getFromUnit() {
        return fromUnit;
    }

    public void setFromUnit(MonetaryUnit fromUnit) {
        this.fromUnit = fromUnit;
    }

    public MonetaryUnit getToUnit() {
        return toUnit;
    }

    public void setToUnit(MonetaryUnit toUnit) {
        this.toUnit = toUnit;
    }

    static public BigDecimal getExchangeRate(MonetaryAmount fromAmount, MonetaryAmount toAmount) {
        if (toAmount.abs().signum() == 0) return DEFAULT_EXCHANGE_RATE; // Avoid divide by zero
        return toAmount.getValue().abs().divide(
                fromAmount.getValue().abs(), CurrencyPair.SCALE, RoundingMode.HALF_UP
        );
    }

    public MonetaryAmount getExchangedAmount(MonetaryAmount originalAmount) {
        return new MonetaryAmount(
                toUnit,
                originalAmount.getValue().multiply(getExchangeRate())
        );
    }

    public MonetaryAmount getOriginalAmount(MonetaryAmount exchangedAmount) {
        // A scale of 32 should be enough, it is cut to unit's scale next anyway
        BigDecimal original =
                exchangedAmount.getValue()
                        .divide(getExchangeRate(), 32, RoundingMode.HALF_UP);
        return new MonetaryAmount(getFromUnit(), original);
    }

    public boolean equalsFromToCodes(CurrencyPair that) {
        return (getFromCode().equals(that.getFromCode())
                && getToCode().equals(that.getToCode()));
    }

    public static List<CurrencyPair> getPairs(List<MonetaryUnit> monetaryUnits) {

        // Always sort the collection so result is guaranteed to be the same
        List<MonetaryUnit> sortedUnits = new ArrayList(monetaryUnits);
        Collections.sort(
                sortedUnits,
                new Comparator<MonetaryUnit>() {
                    @Override
                    public int compare(MonetaryUnit a, MonetaryUnit b) {
                        return a.getCurrencyCode().compareToIgnoreCase(b.getCurrencyCode());
                    }
                });

        List<CurrencyPair> pairs = new ArrayList();
        for (MonetaryUnit unitA : sortedUnits) {
            for (MonetaryUnit unitB : sortedUnits) {
                if (unitA.equals(unitB)) continue;
                pairs.add(new CurrencyPair(unitA.getCurrencyCode(), unitB.getCurrencyCode()));
            }
        }
        return pairs;
    }

    @Override
    public List<ValidationError> validate(String group) {
        return new ArrayList();
    }

    @Override
    public String toString() {
        return getFromCode() + "/" + getToCode() + ", " + getExchangeRate() + ", " + getCreatedOn();
    }
}
