/*
 * Copyright (c) 2012, 2018, Werner Keil, Anatole Tresch and others.
 *
 * Licensed 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.
 *
 * Contributors: @atsticks
 */
package org.javamoney.calc;

import javax.money.*;

import java.util.function.Predicate;

/**
 * Platform RI: This class decorates an arbitrary {@link MonetaryAmount}
 * instance and ensure the given {@link Predicate} is always
 * {@code true}.
 * <p>
 * As required by the {@link MonetaryAmount} interface, this class is
 * <ul>
 * <li>immutable</li>
 * <li>final</li>
 * <li>thread-safe/li>
 * <li>serializable</li>
 * </ul>
 * <p>
 * As a consequence all this attributes must also be true for the
 * {@link Predicate} used.
 *
 * @author Anatole Tresch
 * @author Werner Keil
 */
final class ValidatedMoney implements
        MonetaryAmount {
    /**
     * The amount's predicate.
     */
    private Predicate<MonetaryAmount> predicate;
    /**
     * The underlying amount.
     */
    private final MonetaryAmount amount;

    /**
     * Creates a new wrapper instance.
     *
     * @param amount    the underlying amount, not null and not negative.
     * @param predicate the predicate
     * @throws IllegalArgumentException if the amount passed is negative.
     */
    ValidatedMoney(MonetaryAmount amount,
                   Predicate<MonetaryAmount> predicate) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount required.");
        }
        if (predicate == null) {
            throw new IllegalArgumentException("predicate required.");
        }
        if (!predicate.test(amount)) {
            throw new IllegalArgumentException("Constraint failed: "
                    + predicate + " with " + amount);
        }
        this.amount = amount;
        this.predicate = predicate;
    }

    /**
     * Access an ValidatedMoney based on the given
     * {@link MonetaryAmount}.
     * @param predicate the validation function, not null.
     * @param amount the amount to decorate
     * @return the decorated (validated) amount instance.
     */
    private static ValidatedMoney of(MonetaryAmount amount,
                                     Predicate<MonetaryAmount> predicate) {
        return new ValidatedMoney(amount, predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#getCurrency()
     */
    @Override
    public CurrencyUnit getCurrency() {
        return this.amount.getCurrency();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#abs()
     */
    public ValidatedMoney abs() {
        return of(this.amount.abs(), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#add(javax.money.MonetaryAmount)
     */
    public ValidatedMoney add(MonetaryAmount augend) {
        return of(this.amount.add(augend), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#divide(java.lang.Number)
     */
    public ValidatedMoney divide(Number divisor) {
        return of(this.amount.divide(divisor), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#divideAndRemainder(java.lang.Number)
     */
    public ValidatedMoney[] divideAndRemainder(Number divisor) {
        MonetaryAmount[] res = this.amount.divideAndRemainder(divisor);
        return new ValidatedMoney[]{of(res[0], predicate),
                of(res[1], predicate)};
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#divideToIntegralValue(java.lang.Number)
     */
    public ValidatedMoney divideToIntegralValue(Number divisor) {
        return of(this.amount.divideToIntegralValue(divisor), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#multiply(java.lang.Number)
     */
    public ValidatedMoney multiply(Number multiplicand) {
        return of(this.amount.multiply(multiplicand), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#negate()
     */
    public ValidatedMoney negate() {
        return of(this.amount.negate(), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#plus()
     */
    public ValidatedMoney plus() {
        return of(this.amount.plus(), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#remainder(java.lang.Number)
     */
    public ValidatedMoney remainder(Number divisor) {
        return of(this.amount.remainder(divisor), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#scaleByPowerOfTen(int)
     */
    public ValidatedMoney scaleByPowerOfTen(int n) {
        return of(this.amount.scaleByPowerOfTen(n), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isZero()
     */
    public boolean isZero() {
        return this.amount.isZero();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isPositive()
     */
    public boolean isPositive() {
        return this.amount.isPositive();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isPositiveOrZero()
     */
    public boolean isPositiveOrZero() {
        return this.amount.isPositiveOrZero();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isNegative()
     */
    public boolean isNegative() {
        return this.amount.isNegative();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isNegativeOrZero()
     */
    public boolean isNegativeOrZero() {
        return this.amount.isNegativeOrZero();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#with(javax.money.MonetaryOperator)
     */
    @Override
    public ValidatedMoney with(MonetaryOperator adjuster) {
        return of(this.amount.with(adjuster), predicate);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#signum()
     */
    public int signum() {
        return this.amount.signum();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isLessThan(javax.money.MonetaryAmount)
     */
    public boolean isLessThan(MonetaryAmount amount) {
        return this.amount.isLessThan(amount);
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * javax.money.MonetaryAmount#isLessThanOrEqualTo(javax.money.MonetaryAmount
     * )
     */
    public boolean isLessThanOrEqualTo(MonetaryAmount amount) {
        return this.amount.isLessThanOrEqualTo(amount);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isGreaterThan(javax.money.MonetaryAmount)
     */
    public boolean isGreaterThan(MonetaryAmount amount) {
        return this.amount.isGreaterThan(amount);
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * javax.money.MonetaryAmount#isGreaterThanOrEqualTo(javax.money.MonetaryAmount
     * )
     */
    public boolean isGreaterThanOrEqualTo(MonetaryAmount amount) {
        return this.amount.isGreaterThanOrEqualTo(amount);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.money.MonetaryAmount#isEqualTo(javax.money.MonetaryAmount)
     */
    public boolean isEqualTo(MonetaryAmount amount) {
        return this.amount.isEqualTo(amount);
    }

    @Override
    public MonetaryContext getContext() {
        return this.amount.getContext();
    }

    @Override
    public NumberValue getNumber() {
        return this.amount.getNumber();
    }

    @Override
    public <R> R query(MonetaryQuery<R> query) {
        return query.queryFrom(this);
    }

    @Override
    public ValidatedMoney subtract(MonetaryAmount amount) {
        return new ValidatedMoney(this.amount.subtract(amount),
                predicate);
    }

    @Override
    public ValidatedMoney multiply(long multiplicand) {
        return new ValidatedMoney(amount.multiply(multiplicand),
                predicate);
    }

    @Override
    public ValidatedMoney multiply(double multiplicand) {
        return new ValidatedMoney(amount.multiply(multiplicand),
                predicate);
    }

    @Override
    public ValidatedMoney divide(long divisor) {
        return new ValidatedMoney(amount.divide(divisor), predicate);
    }

    @Override
    public ValidatedMoney divide(double divisor) {
        return new ValidatedMoney(amount.divide(divisor), predicate);
    }

    @Override
    public ValidatedMoney remainder(long divisor) {
        return new ValidatedMoney(amount.remainder(divisor), predicate);
    }

    @Override
    public ValidatedMoney remainder(double divisor) {
        return new ValidatedMoney(amount.remainder(divisor), predicate);
    }

    @Override
    public ValidatedMoney[] divideAndRemainder(long divisor) {
        MonetaryAmount[] result = this.amount.divideAndRemainder(divisor);
        return new ValidatedMoney[]{
                new ValidatedMoney(result[0], predicate),
                new ValidatedMoney(result[1], predicate)};
    }

    @Override
    public ValidatedMoney[] divideAndRemainder(double divisor) {
        MonetaryAmount[] result = this.amount.divideAndRemainder(divisor);
        return new ValidatedMoney[]{
                new ValidatedMoney(result[0], predicate),
                new ValidatedMoney(result[1], predicate)};
    }

    @Override
    public ValidatedMoney divideToIntegralValue(long divisor) {
        return new ValidatedMoney(
                amount.divideToIntegralValue(divisor),
                predicate);
    }

    @Override
    public ValidatedMoney divideToIntegralValue(double divisor) {
        return new ValidatedMoney(
                amount.divideToIntegralValue(divisor),
                predicate);
    }

    @Override
    public ValidatedMoney stripTrailingZeros() {
        return new ValidatedMoney(amount.stripTrailingZeros(),
                predicate);
    }

    @Override
    public MonetaryAmountFactory<ValidatedMoney> getFactory() {
        return new ValidatedAmountFactory(this);
    }

    @Override
    public int compareTo(MonetaryAmount o) {
        return this.amount.compareTo(o);
    }

    /**
     * Amount factory, which validates the amount created, based on the default amount factory.
     */
    private final class ValidatedAmountFactory implements MonetaryAmountFactory<ValidatedMoney>{
        private Predicate<MonetaryAmount> predicate;
        private MonetaryAmountFactory<?> factory = Monetary.getDefaultAmountFactory();

        /**
         * Instantiates a new Validated amount factory.
         *
         * @param amount the amount
         */
        public ValidatedAmountFactory(ValidatedMoney amount){
            this.predicate = amount.predicate;
        }

        @Override
        public Class<? extends MonetaryAmount> getAmountType() {
            return ValidatedMoney.class;
        }

        @Override
        public MonetaryAmountFactory setCurrency(CurrencyUnit currency) {
            factory.setCurrency(currency);
            return this;
        }

        @Override
        public MonetaryAmountFactory setNumber(double number) {
            factory.setNumber(number);
            return this;
        }

        @Override
        public MonetaryAmountFactory setNumber(long number) {
            factory.setNumber(number);
            return this;
        }

        @Override
        public MonetaryAmountFactory setNumber(Number number) {
            factory.setNumber(number);
            return this;
        }

        @Override
        public NumberValue getMaxNumber() {
            return factory.getMaxNumber();
        }

        @Override
        public NumberValue getMinNumber() {
            return factory.getMinNumber();
        }

        @Override
        public MonetaryAmountFactory setContext(MonetaryContext monetaryContext) {
            factory.setContext(monetaryContext);
            return this;
        }

        @Override
        public ValidatedMoney create() {
            return new ValidatedMoney(factory.create(), predicate);
        }

        @Override
        public MonetaryContext getDefaultMonetaryContext() {
            return factory.getDefaultMonetaryContext();
        }
    }

}
