//**************************************************************************************************
//*  FRIDA: fast reliable interactive data analysis
//*  (C) Joachim Wuttke 1990-, v2(C++) 2001-
//*  http://apps.jcns.fz-juelich.de/frida
//**************************************************************************************************

//! \file  axis.cpp
//! \brief CAxis: One axis of a coordinate frame.

#include <cmath>
#include <iomanip>
#include <iostream>
#include <vector>

#include "../readplus/macro.hpp"
#include "../trivia/math.hpp"
#include "../trivia/string_convs.hpp"

#include "axis.hpp"

using std::string;
using std::vector;
using std::cout;

#define S(a) triv::strg((a))


//! Sets infinite limits so that next plot call will recompute them.

void CAxis::set_auto()
{
    inf = -INFINITY;
    sup = +INFINITY;
}


//! Sets limits to arguments.

void CAxis::set_limits(double _inf, double _sup)
{
    if (_inf >= _sup)
        throw "Invalid limits " + S(_inf) + " .. " + S(_sup) + " for axis " + name;
    if (logflag && _inf <= 0)
        throw "Invalid nonpositive limit " + S(_inf) + " for axis " + name;
    inf = _inf;
    sup = _sup;
}


//! Sets log flag to argument, resets limits to infinite defaults.

void CAxis::set_log(bool _log)
{
    logflag = _log;
    set_auto();
}


//! Asks for new limits.

void CAxis::ask_and_set(const string& quest)
{
    for (;;) {
        double inf_in, sup_in;
        string resp1 = NMacro::readwd(quest + " [" + str() + "] ? ");
        if (resp1 == "\\q") {
            throw S("user escape to main menu");
        } else if (resp1 == "") {
            return; // don't change current value
        } else if (resp1 == "*") {
            set_auto();
            return;
        } else if (resp1 == "\\EOL") {
            inf_in = inf;
        } else if (triv::any2dbl(resp1, &inf_in)) {
            // ok
        } else {
            cout << "required input: plot axis range\n"
                    "examples:\n"
                    "    0.1 1000    two floating-point numbers\n"
                    "    \\   1000    let lower bond unchanged\n"
                    "    0.1  \\      let upper bound unchanged\n"
                    "    *           redetermine bounds from data\n"
                    "just press ENTER to let present range unchanged ["
                 << str() << "]\n";
            continue;
        }
        if (logflag && inf_in <= 0) {
            cout << "log axis requires lower bound>0\n";
            continue;
        }
        string resp2 = NMacro::readwd(".. and upper limit [" + S(sup) + "] ? ");
        if (resp2 == "\\q") {
            throw S("user escape to main menu");
        } else if (resp2 == "" || resp2 == "\\EOL") {
            sup_in = sup;
        } else if (triv::any2dbl(resp2, &sup_in)) {
            // ok
        } else {
            cout << "invalid upper bound; use '?' for help\n";
            continue;
        }
        if (sup_in <= inf_in) {
            cout << "invalid input, does not fulfill lower<upper\n";
            continue;
        }
        if (logflag && (sup_in > 1e199 || inf_in < 1e-199)) {
            cout << "log range currently restricted to 1E-199..1E199\n";
            continue;
        }
        if (!logflag && (sup_in > 3e38 || inf_in < -3e38)) {
            cout << "lin range currently restricted to -3e38..3e38\n";
            continue;
        }
        inf = inf_in;
        sup = sup_in;
        return;
    }
}


//! Sets limits to rounded values of arguments.

void CAxis::set_rounded_limits(double _inf, double _sup)
{
    static double relmargin = 0.05;
    static double digits = 2;

    if (_inf == -INFINITY || _sup == +INFINITY)
        throw "BUG: rounding " + name + " limits: unexpected inf limits";
    if (_inf > _sup)
        throw "BUG: rounding " + name + " limits: invalid plot range " + S(_inf) + " > " + S(_sup);
    if (logflag && _inf <= 0)
        throw "BUG: rounding " + name + " limits: nonpositive log inf " + S(_inf);

    inf = _inf;
    sup = _sup;

    if (inf == sup) {
        if (logflag) {
            inf /= 10;
            sup *= 10;
        } else if (inf > 0) {
            inf *= 0.9;
            sup *= 1.1;
        } else if (sup < 0) {
            inf *= 1.1;
            sup *= 0.9;
        } else {
            inf = -1;
            sup = +1;
        }
        return;
    }

    double inf2, sup2;
    double mydigits = digits;
    if (!logflag) { // lin limits
        double margin = relmargin * (sup - inf);
        do {
            sup2 = triv::round_decimal(sup + margin, mydigits);
            inf2 = triv::round_decimal(inf - margin, mydigits);
            if (sup < 0 && sup2 > 0)
                sup2 = 0;
            if (inf2 < 0 && inf > 0)
                inf2 = 0;
            mydigits += 0.1;
            // printf("DEB [%g,%g] -> [%g,%g] (%g)\n", inf, sup, inf2, sup2, mydigits);
            if (mydigits > 17) {
                std::cerr << std::setprecision(17)
                          << "PLEASE REPORT THIS BUG: escaping from endless loop in Axis::"
                             "set_rounded_limits("
                          << inf << "," << sup << ")\n";
                inf2 = inf - margin;
                sup2 = sup - margin;
                break;
            }
        } while (inf2 > inf || sup2 < sup
                 || (sup2 - inf2) > (1.33 * (sup - inf) + 1e-14 * (sup + inf)));
    } else { // log limits
        double ratio = sup / inf;
        double margin = exp(relmargin * log(ratio));
        do {
            sup2 = triv::round_decimal(sup * margin, mydigits);
            inf2 = triv::round_decimal(inf / margin, mydigits);
            mydigits += 0.5;
        } while (!((inf2 < inf) && (sup < sup2)) && mydigits < 18);
    }
    set_limits(inf2, sup2);
}


//! Are both limits finite ?

bool CAxis::finite() const { return inf != -INFINITY && sup != +INFINITY; }


//! Is argument val contained in present plot range?

bool CAxis::contains(double val) const { return inf <= val && val <= sup; }


//! Do arguments v1 and v2 agree within tolerance tol, relative to plot range?

bool CAxis::close_enough(double v1, double v2, double tol) const
{
    if (!finite())
        throw S("undefined plot range");
    if (logflag) {
        if (v1 <= 0 || v2 <= 0)
            return false;
        return fabs(log(v1 / v2)) <= tol * log(sup / inf);
    } else {
        return fabs(v1 - v2) <= tol * (sup - inf);
    }
}


//! Returns plot coordinate (between 0 and 1) of argument.

double CAxis::value2plotcoord(double v) const
{
    if (!finite())
        throw S("undefined plot range");
    if (logflag) {
        if (inf < 0 || v < 0)
            throw S("negative value in log range");
        return log(v / inf) / log(sup / inf);
    } else {
        return (v - inf) / (sup - inf);
    }
}


//! Returns half length of error bar (in plot coordinates 0..1).

double CAxis::value2ploterror(double v, double dv) const
{
    if (!finite())
        throw S("undefined plot range");
    if (dv < 0)
        throw S("negative error");
    if (dv == 0)
        return 0;
    if (logflag) {
        if (inf < 0 || v < 0)
            throw S("negative value in log range");
        return dv / v / log(sup / inf);
    } else {
        return dv / (sup - inf);
    }
}


//! Sets vector x such that its plot coordinates form an equidistent grid.

void CAxis::set_xgrid(vector<double>& x, int n) const
{
    if (!finite())
        throw S("undefined plot range");
    if (n < 2)
        throw S("call to set_xgrid with less than two points");
    if (inf >= sup)
        throw S("call to set_xgrid with invalid limits");
    x.resize(n);
    x[0] = inf;
    x[n - 1] = sup;
    for (int i = 1; i < n - 1; ++i) {
        if (logflag) {
            double c = ((double)(i)) / ((double)(n - 1));
            x[i] = pow(inf, 1 - c) * pow(sup, c);
        } else {
            x[i] = ((n - 1 - i) * inf + i * sup) / (n - 1);
        }
    }
}


//! Returns string representation of plot range.

string CAxis::str() const
{
    if (inf == -INFINITY || sup == +INFINITY)
        return "*";
    else
        return S(inf) + " " + S(sup);
}


//! Returns summary (log flag, plot limits) for use in table of plot frames.

string CAxis::info() const
{
    string ret;
    ret = logflag ? "log" : "lin";
    ret += " " + S(inf);
    ret += " " + S(sup);
    return ret;
}


//! Returns argument converted to my PostScript units (0..10).

double CAxis::pc(double v) const { return 10 * value2plotcoord(v); }


//! Returns half length of error bar (in my Postscript coordinates 0..10).

double CAxis::pcerr(double v, double dv) const { return 10 * value2ploterror(v, dv); }


//! Sets Tacks, ntpt, nt4t, *ticklim to a nice division of the plot range.

void CAxis::calc_ticks(vector<double>& Tacks, int& ntpt, int& nt4t, double* ticklim) const
{
    if (logflag)
        calc_logticks(Tacks, ntpt, nt4t, ticklim);
    else
        calc_linticks(Tacks, ntpt, nt4t, ticklim);
}


//! Sets Tacks, ntpt, nt4t, *ticklim to a nice division of the plot range.

void CAxis::calc_linticks(vector<double>& Tacks, int& ntpt, int& nt4t, double* ticklim) const
{
    double dtack;
    calc_lintacks(Tacks, ntpt, dtack);
    ticklim[0] = Tacks.front() - dtack;
    ticklim[1] = Tacks.back() + dtack;
    nt4t = Tacks.size() + 2;
}

//! Sets Tacks, *ntpt, and dtack

void CAxis::calc_lintacks(vector<double>& Tacks, int& ntpt, double& dtack) const
{
    double range = sup-inf;
    if (range <= 0)
        throw "BUG detected by calc_linticks: inf=" + S(inf) + " >= sup=" + S(sup);
    double r = log10(range);
    int ir = (int)((r < 0) ? r - 1 : r);
    double rd = r - ir; // fractional part of r
    double rdr = pow(10., rd);
    if (rdr > 9.99999) {
        dtack = 2.5 * pow(10., ir); // spacing between tacks
        ntpt = 5; // ticks per tack
    } else if (rdr > 5.00001) {
        dtack = 2 * pow(10., ir);
        ntpt = 4;
    } else if (rdr > 2.500005) {
        dtack = 1 * pow(10., ir);
        ntpt = 5;
    } else if (rdr > 1.600003) {
        dtack = .5 * pow(10., ir);
        ntpt = 5;
    } else if (rdr > 1.250002) {
        dtack = .4 * pow(10., ir);
        ntpt = 4;
    } else {
        dtack = .25 * pow(10., ir);
        ntpt = 5;
    }
    Tacks.clear();
    printf( "DEBUG (%g,%g) => (%g,%g) step %g\n",
            inf, sup, dtack * (floor(inf/dtack)-1), dtack * (ceil(sup/dtack)+1), dtack);
    for (double d = dtack * (floor(inf/dtack)-1); d <= dtack * (ceil(sup/dtack)+1); d += dtack)
        if (inf-1e-4*range <= d && d <= sup+1e-4*range)
            Tacks.push_back(d);
}


//! Sets Tacks, ntpt, nt4t, *ticklim to a nice division of the plot range.

void CAxis::calc_logticks(vector<double>& Tacks, int& ntpt, int& nt4t, double* ticklim) const
{
    if (inf >= sup)
        throw "BUG detected by calc_logticks: inf=" + S(inf) + " >= sup=" + S(sup);
    if (inf <= 0)
        throw S("BUG detected by calc_logticks: negative log argument");

    static double eins = 1 + 1e-6 - 1e-12;

    double rlgmin = log10(inf);
    double rlgmax = log10(sup);
    double rlgrel = rlgmax - rlgmin;
    if (!std::isfinite(rlgrel) || rlgrel > 100)
        throw "Excessive log range " + S(inf) + " .. " + S(sup);

    int incr; // -> decades per tack
    if (rlgrel <= 10) {
        incr = 1;
    } else {
        incr = (int)(rlgrel / 7 + 1);
        if (incr > 3)
            incr = 3 * (incr / 3); // preferred values 3, 6, ...
    }

    // first and last tack
    int minexp = (int)(rlgmin > 0 ? rlgmin + 1e-6 : rlgmin - 1e-6 - 1);
    int maxexp = (int)(rlgmax > 0 ? rlgmax + 1e-6 : rlgmax - 1e-6 - 1);
    if (incr > 1) {
        minexp -= minexp % incr;
        maxexp -= maxexp % incr;
    }

    // set Tacks
    double d1 = inf / eins;
    double d2 = sup * eins;
    Tacks.clear();
    for (int i = minexp; i <= maxexp; i += incr) {
        double r0 = pow(10., i);
        if (d1 <= r0 && r0 <= d2)
            Tacks.push_back(r0);
    }
    nt4t = Tacks.size() + 2;

    // set tick limits (may exceed actual tick range)
    int ntack = Tacks.size();
    if (ntack >= 1) {
        ticklim[0] = Tacks.front() / pow(10., incr);
        ticklim[1] = Tacks.back()  * pow(10., incr);
    } else {
        // untested. just to allow plots when no tacks in window
        ticklim[0] = pow(10., minexp);
        ticklim[1] = pow(10., maxexp + 1);
    }

    // determine #ticks / tack
    if (incr == 1 && rlgrel <= 3) {
        ntpt = 9;
    } else if (incr == 1 && rlgrel <= 7) {
        ntpt = 3; // 1-2-5-10 - Schritte
    } else if (incr == 1) {
        ntpt = 1;
    } else {
        ntpt = -incr; // this one has special semantics: it means 'incr' ticks per tack
    }

    // for narrow ranges, overwrite tacks
    if (ntack<2) {
        double dtack;
        int ntpt_dummy;
        calc_lintacks(Tacks, ntpt_dummy, dtack);
    }
}