/*
   Copyright (C) 2004 - 2015 by Guillaume Melquiond <guillaume.melquiond@inria.fr>
   Part of the Gappa tool https://gappa.gitlabpages.inria.fr/

   This program is free software; you can redistribute it and/or modify
   it under the terms of the CeCILL Free Software License Agreement or
   under the terms of the GNU General Public License version.

   See the COPYING and COPYING.GPL files for more details.
*/

#include <iostream>
#include <stack>

#include "numbers/interval_utility.hpp"
#include "numbers/real.hpp"
#include "numbers/round.hpp"
#include "parser/ast.hpp"
#include "proofs/dichotomy.hpp"
#include "proofs/proof_graph.hpp"
#include "proofs/schemes.hpp"
#include "proofs/updater.hpp"

struct backend;

extern int parameter_dichotomy_depth;
extern bool warning_dichotomy_failure;
extern bool detailed_io;
extern backend *proof_generator;

/**
 * Abstract generator of sub-intervals when performing a dichotomy.
 */
struct splitter
{
  bool do_merge;   /**< Whether successful intervals should be merged. */
  bool splittable; /**< Whether the current interval can be split further. */
  double depth;    /**< Depth for splittable intervals. */
  splitter(bool m): do_merge(m), splittable(false), depth(1.) {}
  /**
   * Splits the current interval and returns the splitting point.
   */
  virtual number split() { assert(false); }
  /**
   * Returns the next splitting point.
   * @return +inf if all the intervals have been handled.
   */
  virtual number next() = 0;
  virtual ~splitter() = default;
};

/**
 * Generator of equally-wide sub-intervals.
 */
struct fixed_splitter: splitter
{
  interval bnd;  /**< Whole interval. */
  int pos;       /**< Number of sub-intervals already handled. */
  int nb;        /**< Total number of sub-intervals. */
  fixed_splitter(interval const &i, int n)
    : splitter(false), bnd(i), pos(0), nb(n)
  { assert(is_bounded(bnd)); }
  virtual number next();
};

/**
 * Splits the whole interval into two parts at ratio #pos / #nb.
 * Returns the sub-interval that has yet to be handled in the #left part.
 */
number fixed_splitter::next()
{
  if (++pos == nb) return number::pos_inf;
  std::pair< interval, interval > ii = ::split(bnd, (double)pos / nb);
  return upper(ii.first);
}

typedef std::vector<split_point> number_vect;

/**
 * Generator of user-specified sub-intervals.
 */
struct point_splitter: splitter
{
  interval bnd;  /**< Whole interval. */
  number_vect const &bnds; /**< Sets of user-specified points. */
  int pos;       /**< Number of user points already handled. */
  point_splitter(interval const &i, number_vect const *b)
    : splitter(false), bnd(i), bnds(*b), pos(-1) {}
  virtual number next();
};

/**
 * Iteratively increments #pos until an intersection between #bnd and the user-specified intervals is found.
 * Returns this intersection.
 */
number point_splitter::next()
{
  for(int sz = bnds.size(); ++pos < sz;)
  {
    number res = bnds[pos].pt;
    if (res <= lower(bnd)) continue;
    if (pos > 0 && res == bnds[pos - 1].pt) continue;
    if (res < upper(bnd)) return res;
  }
  return number::pos_inf;
}

/**
 * Generator of sub-intervals based on dichotomy. A merge of the sub-intervals is performed at the end.
 */
struct best_splitter: splitter
{
  /**
   * Stack of sub-intervals not handled yet and their depth in the dichotomy tree.
   * Top of the stack is the currently handled interval.
   */
  std::stack< std::pair< interval, int > > stack;
  best_splitter(interval const &i);
  virtual number split();
  virtual number next();
};

/**
 * Stores the whole interval @a i at the top of the stack (it will be skipped by the first call to #next)
 * and its two sub-intervals.
 */
best_splitter::best_splitter(interval const &i): splitter(true)
{
  std::pair< interval, interval > ii = ::split(i);
  stack.push(std::make_pair(ii.second, 1));
  stack.push(std::make_pair(ii.first, 1));
  stack.push(std::make_pair(i, 0));
}

/**
 * Splits the interval at the top of the stack. Replaces it by the right part.
 * Pushes the left part and returns it. Sets an increased depth to both parts.
 */
number best_splitter::split()
{
  std::pair<interval, int> &p = stack.top();
  assert(p.second < parameter_dichotomy_depth);
  std::pair< interval, interval > ii = ::split(p.first);
  p.first = ii.second;
  ++p.second;
  stack.push(std::make_pair(ii.first, p.second));
  splittable = p.second < parameter_dichotomy_depth;
  depth = (double)p.second / parameter_dichotomy_depth;
  return upper(ii.first);
}

/**
 * Pops the interval at the top of the stack and returns the next one.
 */
number best_splitter::next()
{
  stack.pop();
  assert(!stack.empty());
  if (stack.size() == 1) return number::pos_inf;
  std::pair<interval, int> &p = stack.top();
  splittable = p.second < parameter_dichotomy_depth;
  depth = (double)p.second / parameter_dichotomy_depth;
  return upper(p.first);
}

typedef std::vector<graph_t *> graph_vect;

class dichotomy_node;

struct dichotomy_failure {
  property hyp;
  property expected;
  property found;
};

struct dichotomy_graphs
{
  graph_vect graphs;
  unsigned nb_ref;
  dichotomy_graphs(): nb_ref(0) {}
  ~dichotomy_graphs();
};

struct dichotomy_helper
{
  dichotomy_graphs *graphs;
  property tmp_hyp;
  int depth;
  proof_context previous_context;
  property_tree const &targets;
  dichotomy_sequence const &hints;
  graph_t *last_graph;
  splitter *gen;
  graph_t *try_hypothesis(dichotomy_failure *, proof_context const &) const;
  void try_graph(graph_t *, proof_context const &);
  void dichotomize();
  dichotomy_node *generate_node(property const &);
  dichotomy_helper(ast_real const *v, property_tree const &t,
    dichotomy_sequence const &h, splitter *s, proof_context const &p)
  : graphs(new dichotomy_graphs()), tmp_hyp(v), depth(0), previous_context(p),
    targets(t), hints(h), last_graph(NULL), gen(s) {}
  ~dichotomy_helper();
};

class dichotomy_node: public dependent_node {
  property res;
  dichotomy_graphs *graphs;
 public:
  dichotomy_node(property const &p, dichotomy_graphs *g)
    : dependent_node(UNION), res(p), graphs(g) { ++g->nb_ref; }
  ~dichotomy_node();
  virtual property const &get_result() const { return res; }
  using dependent_node::insert_pred;
  virtual void enlarge(property const &p) { res = boundify(p, res); }
  virtual property maximal_for(node const *) const { return res; }
};

dichotomy_graphs::~dichotomy_graphs()
{
  assert(nb_ref == 0);
  for(graph_vect::iterator i = graphs.begin(),
      i_end = graphs.end(); i != i_end; ++i)
  {
    delete *i;
  }
}

dichotomy_helper::~dichotomy_helper()
{
  delete last_graph;
  delete gen;
}

dichotomy_node::~dichotomy_node()
{
  clean_dependencies();
  if (--graphs->nb_ref == 0)
    delete graphs;
}

static bool find_negative(property &p, property_tree const &t)
{
  if (t.left)
    return find_negative(p, *t.left) || find_negative(p, *t.right);
  if (t.conjunction) return false;
  p = *t.atom;
  return true;
}

graph_t *dichotomy_helper::try_hypothesis(dichotomy_failure *exn, proof_context const &pctx) const
{
  graph_t *g = new graph_t(top_graph, property_tree(tmp_hyp));
  g->populate(targets, hints, pctx, NULL);
  if (g->get_contradiction() || targets.empty())
    return g;
  property_tree t;
  bool chg;
  if (targets.try_simplify(simpl_graph(g), chg, t) < 0)
    return g;
  if (exn)
  {
    if (find_negative(exn->expected, chg ? t : targets)) {
      if (node *n = g->find_already_known(exn->expected.real))
        exn->found = n->get_result();
    }
    exn->hyp = g->find_already_known(tmp_hyp.real)->get_result();
  }
  delete g;
  return NULL;
}

void dichotomy_helper::try_graph(graph_t *g2, proof_context const &pctx)
{
  graph_t *g1 = last_graph;
  if (!g1)
  {
    last_graph = g2;
    return;
  }
  if (proof_generator && (gen->do_merge ||
        (g1->get_contradiction() && g2->get_contradiction())))
  {
    property p(*g1->get_hypotheses().atom);
    p.bnd() = interval(lower(p.bnd()), upper(g2->get_hypotheses().atom->bnd()));
    tmp_hyp = p;
    if (graph_t *g = try_hypothesis(NULL, pctx)) {
      if (gen->do_merge || g->get_contradiction())
      {
        // joined graph was successful, keep it as the last graph instead of g1 and g2
        delete g1;
        delete g2;
        last_graph = g;
        return;
      }
      delete g;
    }
  }
  // validate g1 and keep g2 as the last graph
  graphs->graphs.push_back(g1);
  last_graph = g2;
}

dichotomy_node *dichotomy_helper::generate_node(property const &p)
{
  property q;
  bool found_one = false;
  for (graph_t *g: graphs->graphs)
  {
    if (g->get_contradiction()) continue;
    node *n = g->find_already_known(p.real);
    if (!n) return NULL;
    property const &r = n->get_result();
    if (!r.implies(p)) return NULL;
    if (!found_one) {
      q = r;
      found_one = true;
    } else
      q.hull(r);
  }
  assert(found_one && q.implies(p));
  if (!q.strict_implies(p)) return NULL;
  dichotomy_node *n = new dichotomy_node(q, graphs);
  for (graph_t *g: graphs->graphs) {
    node *m = g->get_contradiction();
    if (!m) m = g->find_already_known(p.real);
    n->insert_pred(m);
  }
  return n;
}

void dichotomy_helper::dichotomize()
{
  number prev = number::neg_inf;
  number next = gen->next();
  dichotomy_failure exn;
  if (next == number::pos_inf) throw exn;
  double min_depth = 0, max_depth = 0;
  for (;;)
  {
    bool splittable = gen->splittable;
    retry:
    tmp_hyp.bnd() = interval(prev, next);
    //std::cerr << dump_property(tmp_hyp) << '\n';
    exn.hyp = tmp_hyp;
    proof_context pctx = previous_context;
    double cur_depth = 0;
    if (splittable) {
      cur_depth = std::max(max_depth, gen->depth);
      pctx = proof_context { tmp_hyp.real.real(), pctx.depth * cur_depth };
    }
    graph_t *g = try_hypothesis(&exn, pctx);
    if (!g) {
      if (!splittable) throw exn;
      if (is_singleton(exn.hyp.bnd())) {
        // Try again, as we might have hit the iteration limit.
        splittable = false;
        goto retry;
      }
      if (min_depth <= gen->depth) {
        //std::cerr << "Failure at depth " << gen->depth << " using " << cur_depth << " (" << min_depth << ")\n";
        max_depth = cur_depth * 2;
        if (max_depth > 1) max_depth = 1.;
      }
      next = gen->split();
      continue;
    }
    if (splittable) {
      //std::cerr << "Success at depth " << gen->depth << " using " << cur_depth << '\n';
      max_depth = cur_depth * 0.95;
      min_depth = gen->depth;
    }
    if (next < number::pos_inf) {
      try_graph(g, pctx);
      prev = next;
      next = gen->next();
      continue;
    }
    if (graphs->graphs.empty()) {
      // if this is the second and last graph, do not try a merge
      if (last_graph) graphs->graphs.push_back(last_graph);
      last_graph = g;
    } else try_graph(g, pctx);
    graphs->graphs.push_back(last_graph);
    last_graph = NULL;
    return;
  }
}

/**
 * Applies a ::dichotomy_hint.
 */
void graph_t::dichotomize(dichotomy_hint const &hint, proof_context const &pctx)
{
  assert(top_graph == this);
  dichotomy_sequence hints;
  assert(hint.src.size() >= 1);
  dichotomy_var const &var = hint.src[0];
  if (hint.src.size() > 1) {
    dichotomy_hint h =
      { dvar_vect(hint.src.begin() + 1, hint.src.end()),
        hint.dst, hint.user_def };
    hints.push_back(h);
  }
  if (false) {
    no_range:
    if (warning_dichotomy_failure && hint.user_def)
      std::cerr << "Warning: case split on " << dump_real(var.real)
                << " has no range to split.\n";
    return;
  }
  interval bnd(number::neg_inf, number::pos_inf);
  if (node *varn = find_proof(predicated_real(var.real, PRED_BND)))
    bnd = varn->get_result().bnd();
  splitter *gen;
  property_tree targets;
  if (var.splitter & 1) {
    if (!is_bounded(bnd)) goto no_range;
    gen = new fixed_splitter(bnd, var.splitter / 2);
  } else if (var.splitter) {
    gen = new point_splitter(bnd, (number_vect const *)var.splitter);
  } else {
    targets = hint.dst;
    graph_t *g = this;
    while (g->father) g = g->father;
    targets.negate();
    targets.fill_undefined(g->hyps);
    if (targets.empty()) {
      if (warning_dichotomy_failure && hint.user_def)
        std::cerr << "Warning: case split on " << dump_real(var.real)
                  << " is not goal-driven anymore.\n";
      return;
    }
    if (targets.simplify(simpl_graph(this))) return;
    gen = new best_splitter(bnd);
  }
  dichotomy_helper h(var.real, targets, hints, gen, pctx);
  try {
    h.dichotomize();
  } catch (dichotomy_failure const &e) {
    if (warning_dichotomy_failure && hint.user_def) {
      property const &h = e.hyp;
      if (h.real.null()) goto no_range;
      change_io_format dummy(IO_FULL);
      std::cerr << "Warning: when " << dump_real_short(h.real) << " is in "
                << h.bnd() << ", ";
      predicated_real const &dst = e.expected.real;
      if (dst.null())
        std::cerr << "nothing is satisfied.\n";
      else
      {
        std::cerr << dump_property(e.expected) << " cannot be proved";
        if (!e.found.real.null())
        {
          assert(e.expected.real == e.found.real);
          if (e.found.real.pred_bnd())
            std::cerr << " (best: " << e.found.bnd() << ')';
          else if (e.found.real.pred_cst())
            std::cerr << " (best: " << e.found.cst() << ')';
        }
        std::cerr << ".\n";
      }
    }
    delete h.graphs;
    return;
  }
  bool only_contradictions = true;
  typedef std::set< predicated_real > preal_set;
  preal_set reals;
  for (graph_t *g: h.graphs->graphs)
  {
    if (g->contradiction) continue;
    only_contradictions = false;
    for (auto const &j: g->known_reals)
      reals.insert(j.first);
    break;
  }
  assert(!contradiction);
  if (only_contradictions) {
    dichotomy_node *n = new dichotomy_node(property(), h.graphs);
    for (graph_t *g: h.graphs->graphs) {
      n->insert_pred(g->get_contradiction());
    }
    set_contradiction(n);
    return;
  }
  ++h.graphs->nb_ref;
  for(predicated_real const &r: reals) {
    property p(r);
    if (node *n = find_already_known(r)) p = n->get_result();
    if (node *n = h.generate_node(p)) try_node(n);
  }
  if (--h.graphs->nb_ref == 0) {
    delete h.graphs;
    if (warning_dichotomy_failure && hint.user_def)
      std::cerr << "Warning: case split on " << dump_real(var.real)
                << " has not produced any interesting new result.\n";
    return;
  }
  for (graph_t *g: h.graphs->graphs) {
    g->purge();
  }
}

interval create_interval(ast_number const *, ast_number const * = NULL, bool = true);

uintptr_t fill_splitter(uintptr_t s, ast_number const *n) {
  number_vect *v = reinterpret_cast<number_vect *>(s);
  number m = lower(create_interval(n));
  split_point mb(m, true);
  if (!v) v = new number_vect(1, mb);
  else v->push_back(mb);
  return reinterpret_cast<uintptr_t>(v);
}

uintptr_t fill_splitter(uintptr_t s, split_point const &m)
{
  number_vect *v = reinterpret_cast<number_vect *>(s);
  if (!v) v = new number_vect(1, m);
  else v->push_back(m);
  return reinterpret_cast<uintptr_t>(v);
}

void clear_splitter(uintptr_t s)
{
  if ((s & 1) == 0) delete reinterpret_cast<number_vect *>(s);
}
