/*
 * enforce.cpp: deferred range checks.
 *
 * This file implements spigot_enforce(), which takes two spigot
 * inputs and returns one of them effectively unchanged. The point of
 * it is that it also continually checks to see which side of the
 * second input the main one falls on, and if it ever finds out that
 * that's the _wrong_ side, it throws an exception, which the caller
 * has provided in advance (with an appropriate error message) in case
 * it's needed.
 */

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

#include "spigot.h"
#include "funcs.h"
#include "error.h"

#define STRING(a,b) b
static const char *const relation_strings[] = { ENFORCE_RELATIONS(STRING) };
#undef STRING

class Enforcer : public BinaryIntervalSource {
    Spigot x_orig;
    EnforceRelation rel;
    Spigot bound_orig;
    domain_error err;
    BracketingGenerator bg_x, bg_bound;

    virtual Spigot clone() override;
    virtual bool is_rational(bigint *n, bigint *d) override;
    virtual void gen_bin_interval(bigint *ret_lo, bigint *ret_hi,
                                  unsigned *ret_bits) override;

  public:
    Enforcer(Spigot x, EnforceRelation arel, Spigot bound, domain_error aerr);
};

MAKE_CLASS_GREETER(Enforcer);

Enforcer::Enforcer(Spigot x, EnforceRelation arel,
                   Spigot bound, domain_error aerr)
    : x_orig(x->clone()), rel(arel), bound_orig(bound->clone()),
      err(aerr), bg_x(move(x)), bg_bound(move(bound))
{
    dgreet(bg_x.dbg_id(), " ", relation_strings[rel]);
}

Spigot Enforcer::clone()
{
    return spigot_clone(this, x_orig->clone(), rel, bound_orig->clone(), err);
}

bool Enforcer::is_rational(bigint *n, bigint *d)
{
    return x_orig->is_rational(n, d);
}

void Enforcer::gen_bin_interval(bigint *ret_lo, bigint *ret_hi,
                                unsigned *ret_bits)
{
    bg_x.get_bracket_shift(ret_lo, ret_hi, ret_bits);
    dprint("got x bracket (", *ret_lo, ",", *ret_hi, ") / 2^", *ret_bits);

    bg_bound.set_denominator_lower_bound_shift(*ret_bits);
    bigint cmp_lo, cmp_hi;
    unsigned cmp_bits;
    bg_bound.get_bracket_shift(&cmp_lo, &cmp_hi, &cmp_bits);
    dprint("got bound bracket (", cmp_lo, ",", cmp_hi, ") / 2^", cmp_bits);

    assert(cmp_bits >= *ret_bits);
    unsigned ret_shift = cmp_bits - *ret_bits;

    bool ok;
    if (rel == ENFORCE_GT || rel == ENFORCE_GE) {
        ok = (*ret_hi << ret_shift) >= cmp_lo;
    } else /* if (rel == ENFORCE_LT || rel == ENFORCE_LE) */ {
        ok = (*ret_lo << ret_shift) <= cmp_hi;
    }
    if (!ok)
        throw err;
}

Spigot spigot_enforce(Spigot x, EnforceRelation rel, Spigot bound,
                      domain_error err)
{
    /*
     * Start by at least _trying_ to report the error up front, if
     * it's really obviously out of range: if we can detect the
     * problem exactly via rationals, or if it's so far out that even
     * a cursory check with get_approximate_approximant can tell.
     */
    bigint xn, xd, bn, bd;
    if (x->is_rational(&xn, &xd) && bound->is_rational(&bn, &bd)) {
        // Use here that is_rational always returns a positive denominator
        if ((rel == ENFORCE_GT && xn*bd <= bn*xd) ||
            (rel == ENFORCE_GE && xn*bd <  bn*xd) ||
            (rel == ENFORCE_LT && xn*bd >= bn*xd) ||
            (rel == ENFORCE_LE && xn*bd >  bn*xd))
            throw err;
    } else {
        StaticGenerator diffgen(spigot_sub(x->clone(), bound->clone()));
        bigint lo, hi;
        bool lo_open, hi_open;
        diffgen.iterate_to_bounds(&lo, &hi, &lo_open, &hi_open, 0, nullptr, true);

        if (((rel == ENFORCE_GT || rel == ENFORCE_GE) &&
             hi <= 0 && !(hi == 0 && !hi_open && rel == ENFORCE_GE)) ||
            ((rel == ENFORCE_LT || rel == ENFORCE_LE) &&
             lo >= 0 && !(lo == 0 && !lo_open && rel == ENFORCE_LE))) {
            throw err;
        }
    }

    /*
     * Failing that, use the above Enforcer class, which will watch
     * for the number turning out to be on the wrong side of the
     * boundary later on after further information comes to light.
     */
    return make_unique<Enforcer>(move(x), rel, move(bound), err);
}
