package dr.math.distributions;

import dr.math.UnivariateFunction;
import org.apache.commons.math.MathException;
import org.apache.commons.math.distribution.AbstractContinuousDistribution;

/**
 * @author Andrew Rambaut
 */
public class TruncatedDistribution extends AbstractContinuousDistribution implements Distribution {

    public double getLower() {
        return lower;
    }

    public double getUpper() {
        return upper;
    }

    public TruncatedDistribution(Distribution source, double lower, double upper) {
        this.source = source;

        if (lower == upper) {
            throw new IllegalArgumentException("upper equals lower");
        }

        if (source.getProbabilityDensityFunction().getLowerBound() > lower) {
            lower = source.getProbabilityDensityFunction().getLowerBound();
        }

        if (source.getProbabilityDensityFunction().getUpperBound() < upper) {
            upper = source.getProbabilityDensityFunction().getUpperBound();
        }

        this.lower = lower;
        this.upper = upper;

        if (!Double.isInfinite(this.lower)) {
            this.lowerCDF = source.cdf(lower);
        } else {
            this.lowerCDF = 0;
        }

        if (!Double.isInfinite(this.upper)) {
            this.normalization = source.cdf(upper) - lowerCDF;
        } else {
            this.normalization = 1.0 - lowerCDF;
        }
    }


    public double pdf(double x) {
        if (x >= upper && x < lower)
            return 0.0;
        else
            return source.pdf(x) / normalization;
    }

    public double logPdf(double x) {
        return Math.log(pdf(x));
    }

    public double cdf(double x) {
        double cdf;
        if (x < lower)
            cdf = 0.0;
        else if (x >= lower && x < upper)
            cdf = (source.cdf(x) - lowerCDF) / normalization;
        else
            cdf = 1.0;

        return cdf;
    }

    public double quantile(double y) {

        if (y == 0)
            return lower;

        if (y == 1.0)
            return upper;

        if (Double.isInfinite(lower) && Double.isInfinite(upper)) {
            return source.quantile(y);
        }

        try {
            return super.inverseCumulativeProbability(y);
        } catch (MathException e) {
//                throw MathRuntimeException.createIllegalArgumentException(                // AR - throwing exceptions deep in numerical code causes trouble. Catching runtime
            // exceptions is bad. Better to return NaN and let the calling code deal with it.
            return Double.NaN;

//                    "Couldn't calculate beta quantile for alpha = " + alpha + ", beta = " + beta + ": " +e.getMessage());
        }
    }

    /**
     * mean of the distribution
     *
     * @return mean
     */
    public double mean() {
        if (source != null) {
            return source.mean();
        } else {
            throw new IllegalArgumentException("Distribution is null");
        }
    }

    /**
     * variance of the distribution
     *
     * @return variance
     */
    public double variance() {
        if (source != null) {
            return source.variance();
        } else {
            throw new IllegalArgumentException("Distribution is null");
        }

    }

    public UnivariateFunction getProbabilityDensityFunction() {
        return pdfFunction;
    }

    private UnivariateFunction pdfFunction = new UnivariateFunction() {
        public final double evaluate(double x) {
            return pdf(x);
        }

        public final double getLowerBound() {
            return lower;
        }

        public final double getUpperBound() {
            return upper;
        }
    };

    private final Distribution source;
    private final double lower;
    private final double upper;
    private final double normalization;
    private final double lowerCDF;

    @Override
    protected double getInitialDomain(double v) {
        if (!Double.isInfinite(lower) && !Double.isInfinite(upper)) {
            return (upper + lower) / 2;
        } else if (!Double.isInfinite(upper)) {
            return upper / 2;
        } else if (!Double.isInfinite(lower)) {
            return lower * 2;
        }
        return v;
    }

    @Override
    protected double getDomainLowerBound(double v) {
        return lower;
    }

    @Override
    protected double getDomainUpperBound(double v) {
        return upper;
    }

    public double cumulativeProbability(double v) throws MathException {
        return cdf(v);
    }
}
