/*
   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 <cmath>
#include <cstdint>
#include <iostream>
#include <list>
#include <map>
#include <ostream>
#include <set>
#include <string>
#include <typeinfo>
#include <vector>

#include "utils.hpp"
#include "backends/backend.hpp"
#include "numbers/interval_utility.hpp"
#include "numbers/real.hpp"
#include "parser/ast.hpp"
#include "parser/pattern.hpp"
#include "proofs/schemes.hpp"

bool is_unknown_theorem(char const *th)
{
  extern backend *proof_generator;
  return proof_generator && !proof_generator->is_known(th);
}

typedef std::set< predicated_real > preal_set;
extern preal_set input_reals, output_reals;
extern real_set user_reals;

struct scheme_factories: std::vector< scheme_factory const * > {
  ~scheme_factories() {
    for (iterator i = begin(), i_end = end(); i != i_end; ++i)
      delete *i;
  }
};

static static_ptr<scheme_factories> factories;

scheme_factory::scheme_factory(predicated_real const &r, ast_real const *u)
  : target(r), user_source(u)
{
  factories->push_back(this);
}

struct factorx_wrapper: scheme_factory {
  typedef factory_creator::factorx_fun factory_fun;
  factory_fun fun;
  factorx_wrapper(factory_fun f, predicated_real const &r, ast_real const *u)
  : scheme_factory(r, u), fun(f) {}
  virtual proof_scheme *operator()(predicated_real const &r, ast_real_vect const &h) const
  { return (*fun)(r, h); }
};

struct factory_wrapper: scheme_factory {
  typedef factory_creator::factory_fun factory_fun;
  factory_fun fun;
  factory_wrapper(factory_fun f)
  : scheme_factory(predicated_real()), fun(f) {}
  virtual proof_scheme *operator()(predicated_real const &r, ast_real_vect const &) const
  { return r.pred() == PRED_BND ? (*fun)(r.real()) : NULL; }
};

struct factorz_wrapper: scheme_factory {
  typedef factory_creator::factorz_fun factory_fun;
  factory_fun fun;
  factorz_wrapper(factory_fun f)
  : scheme_factory(predicated_real()), fun(f) {}
  virtual proof_scheme *operator()(predicated_real const &r, ast_real_vect const &) const
  { return (*fun)(r); }
};

factory_creator::factory_creator(factorx_fun f, predicated_real const &r, ast_real const *u)
{ new factorx_wrapper(f, r, u); }

factory_creator::factory_creator(factorx_fun f, predicated_real const &r)
{ new factorx_wrapper(f, r, NULL); }

factory_creator::factory_creator(factory_fun f)
{ new factory_wrapper(f); }

factory_creator::factory_creator(factorz_fun f)
{ new factorz_wrapper(f); }

typedef std::vector< proof_scheme const * > scheme_vect;
static scheme_vect source_schemes;

/** Timestamp for the currently running graph algorithm. Increased each time an algorithm starts. */
static unsigned visit_counter = 0;

typedef std::set< proof_scheme const * > scheme_set;
struct real_dependency
{
  scheme_set dependent;
  scheme_set schemes;
  mutable unsigned visited;
  real_dependency(): visited(0) {}
  bool can_visit() const;
};
typedef std::map< predicated_real, real_dependency > real_dependencies;
static real_dependencies reals;

typedef std::list< proof_scheme const * > scheme_list;

/**
 * Tells if the scheme has yet to be visited by the current graph algorithm.
 * In that case, the function updates the #visited timestamp so that the next call returns false.
 */
bool proof_scheme::can_visit() const
{
  if (visited == visit_counter) return false;
  visited = visit_counter;
  return true;
}

/**
 * Tells if the real has yet to be visited by the current graph algorithm.
 * In that case, the function updates the #visited timestamp so that the next call returns false.
 */
bool real_dependency::can_visit() const
{
  if (visited == visit_counter) return false;
  visited = visit_counter;
  return true;
}

/** Priority queue for storing schemes. */
struct scheme_queue
{
  void push(proof_scheme const *s, bool slow);
  proof_scheme const *pop();
  scheme_list good_queue, bad_queue;
};

/**
 * Stores @a s in the corresponding queue.
 */
void scheme_queue::push(proof_scheme const *s, bool slow)
{
  if (slow || s->score == 0) bad_queue.push_back(s);
  else good_queue.push_back(s);
}

/**
 * Removes a scheme from #good_queue and returns it.
 * If the queue was empty, promotes #bad_queue to #good_queue first.
 */
proof_scheme const *scheme_queue::pop()
{
  if (good_queue.empty()) {
    if (bad_queue.empty()) return NULL;
    good_queue.swap(bad_queue);
  }
  proof_scheme const *s = good_queue.front();
  good_queue.pop_front();
  return s;
}

int stat_tested_real = 0, stat_discarded_real = 0,
    stat_tested_theo = 0, stat_discarded_theo = 0;

/**
 * Marks the source schemes as visited and puts them into @a v.
 */
static void initialize_scheme_list(scheme_queue &v)
{
  ++visit_counter;
  for (proof_scheme const *s: source_schemes) {
    s->visited = visit_counter;
    v.push(s, false);
  }
}

/**
 * Marks as visited and inserts into @a v all the schemes (except @a ss) that depend on @a real.
 */
static void insert_dependent(scheme_queue &v, predicated_real const &real, bool slow, proof_scheme const *ss)
{
  for (proof_scheme const *s: reals[real].dependent)
  {
    if (s == ss || !s->can_visit()) continue;
    unsigned c = s->hyp_cache;
    if (c == 0) {
      v.push(s, slow);
      continue;
    }
    unsigned l = s->needed_reals.size();
    if (l > 32) l = 32;
    for (unsigned j = 0; j != l; ++j) {
      unsigned mask = 1u << j;
      if (!(c & mask)) continue;
      if (s->needed_reals[j] == real) c &= ~mask;
    }
    s->hyp_cache = c;
    if (c) s->visited = 0;
    else v.push(s, false);
  }
}

static void delete_scheme(proof_scheme const *s, predicated_real const *restricted_real)
{
  // if there is a restriction, we are removing a scheme that depends on a real,
  // otherwise we are removing a scheme from a real; in both cases, we should
  // not modify the dependencies of this real
  for (predicated_real const &real: s->needed_reals) {
    if (restricted_real && real == *restricted_real) continue;
    reals[real].dependent.erase(s);
  }
  if (restricted_real) reals[s->real].schemes.erase(s);
  ++stat_discarded_theo;
  delete s;
}

static real_dependency &initialize_dependencies(predicated_real const &real)
{
  real_dependencies::iterator it = reals.find(real);
  if (it != reals.end()) return it->second;
  // no dependencies yet, let's generate them
  ++stat_tested_real;
  it = reals.insert(std::make_pair(real, real_dependency())).first;
  real_dependency &dep = it->second;
  scheme_set &l = dep.schemes;
  for (scheme_factories::iterator i = factories->begin(),
       i_end = factories->end(); i != i_end; ++i)
  {
    scheme_factory const &f = **i;
    ast_real_vect holders;
    if (f.target.null())
    {
      // no embedded pattern (or no hole left in the pattern, see below)
      no_hole:
      if (proof_scheme *s = f(real, holders)) l.insert(s);
      continue;
    }
    // there is an embedded pattern, match it against the current real
    if (f.target.pred() != real.pred()) continue;
    ast_real const *r1 = f.target.real(), *r2 = f.target.real2();
    if (!match(real.real(), r1, holders) ||
        (r2 && !match(real.real2(), r2, holders)))
      continue;
    // the embedded pattern is a match, try generating schemes
    if (f.user_source)
    {
      // look for user reals to fill the pattern holes
      for (ast_real const *j: user_reals) {
        ast_real_vect h = holders;
        if (!match(j, f.user_source, h)) continue;
        if (proof_scheme *s = f(real, h)) l.insert(s);
      }
      continue;
    }
    if (holders.size() < 2 || (holders[0] && holders[1]))
      goto no_hole;
    assert(holders[0] || holders[1]);
    int p = !holders[1];
    // pattern position p is not filled, use approx/accurate pairs
    link_map const *lm = p ? &approximates : &accurates;
    link_map::const_iterator k = lm->find(holders[1 - p]);
    if (k == lm->end()) continue;
    for (ast_real const *j: k->second) {
      holders[p] = j;
      if (proof_scheme *s = f(real, holders)) l.insert(s);
    }
  }
  // create the dependencies
  for (proof_scheme const *s: l) {
    ++stat_tested_theo;
    for (predicated_real const &j: s->needed_reals)
      initialize_dependencies(j).dependent.insert(s);
  }
  return it->second;
}

typedef std::list< predicated_real > preal_list;

/**
 * Marks all the schemes and reals reachable from @a real.
 */
static void mark_dependent_schemes(predicated_real const &real)
{
  preal_list pending_reals;
  if (!reals[real].can_visit()) return;
  pending_reals.push_back(real);
  while (!pending_reals.empty())
  {
    predicated_real r = pending_reals.back();
    pending_reals.pop_back();
    for (proof_scheme const *s: reals[r].dependent) {
      if (!s || !s->can_visit()) continue;
      if (reals[s->real].can_visit()) pending_reals.push_back(s->real);
    }
  }
}

/**
 * Marks all the reals potentially useful for reaching @a real.
 */
static void mark_useful_reals(predicated_real const &real)
{
  preal_list pending_reals;
  if (!reals[real].can_visit()) return;
  pending_reals.push_back(real);
  while (!pending_reals.empty())
  {
    predicated_real r = pending_reals.back();
    pending_reals.pop_back();
    for (proof_scheme const *s: reals[r].schemes)
    {
      if (!s) continue;
      preal_vect w = s->needed_reals;
      for (predicated_real const &j: w)
        if (reals[j].can_visit()) pending_reals.push_back(j);
    }
  }
}

/**
 * Generates all the schemes needed for linking inputs and outputs.
 */
preal_vect generate_proof_paths()
{
  // recursively search schemes needed for output reals
  for (predicated_real const &i: output_reals) {
    real_dependency &r = initialize_dependencies(i);
    r.dependent.insert(NULL);
  }
  // initialize hypothesis reals to handle contradictions
  for (predicated_real const &i: input_reals) {
    real_dependency &r = initialize_dependencies(i);
    r.schemes.insert(NULL);
    r.dependent.insert(NULL);
  }
  // find reachable schemes starting from source schemes and inputs, then remove unreachable schemes
  ++visit_counter;
  for (real_dependencies::iterator i = reals.begin(), i_end = reals.end(); i != i_end; ++i)
  {
    for (proof_scheme const *s: i->second.schemes) {
      if (!s || !s->needed_reals.empty()) continue;
      s->visited = visit_counter;
      mark_dependent_schemes(s->real);
    }
  }
  for (predicated_real const &i: input_reals)
    mark_dependent_schemes(i);
  for (real_dependencies::iterator i = reals.begin(), i_end = reals.end(); i != i_end; ++i)
  {
    real_dependency &r = i->second;
    scheme_set v;
    v.swap(r.schemes);
    for (proof_scheme const *s: v) {
      if (!s || s->visited == visit_counter) r.schemes.insert(s);
      else delete_scheme(s, NULL);
    }
  }
  // find reals reaching to outputs, then remove unneeded reals
  ++visit_counter;
  for (predicated_real const &i: output_reals)
    mark_useful_reals(i);
  for (predicated_real const &i: input_reals)
    mark_useful_reals(i);
  for (real_dependencies::iterator i = reals.begin(), i_end = reals.end(); i != i_end; ++i)
  {
    real_dependency &r = i->second;
    if (r.visited == visit_counter) continue;
    for (proof_scheme const *j: r.schemes) {
      if (j) delete_scheme(j, NULL);
    }
    r.schemes.clear();
  }
  // remove reals without any scheme
  ++visit_counter;
  preal_list pending_reals;
  for (real_dependencies::iterator i = reals.begin(), i_end = reals.end(); i != i_end; ++i)
  {
    if (!i->second.schemes.empty()) continue;
    pending_reals.push_back(i->first);
    i->second.visited = visit_counter;
  }
  while (!pending_reals.empty())
  {
    predicated_real real = pending_reals.back();
    pending_reals.pop_back();
    for (proof_scheme const *s: reals[real].dependent)
    {
      if (!s) continue;
      predicated_real u = s->real;
      real_dependency const &t = reals[u];
      delete_scheme(s, &real);
      if (t.schemes.empty() && t.can_visit()) pending_reals.push_back(u);
    }
    reals.erase(real);
    ++stat_discarded_real;
  }
  // setup the source schemes now, and clean the dependencies
  for (real_dependencies::iterator i = reals.begin(), i_end = reals.end(); i != i_end; ++i)
  {
    real_dependency &r = i->second;
    r.schemes.erase(NULL);
    for (proof_scheme const *s: r.schemes) {
      if (s->needed_reals.empty())
        source_schemes.push_back(s);
    }
    r.dependent.erase(NULL);
  }
  // find unreachable outputs
  preal_vect missing_targets;
  for (predicated_real const &i: output_reals) {
    real_dependencies::iterator j = reals.find(i);
    if (j == reals.end())
      missing_targets.push_back(i);
  }
  return missing_targets;
}

/**
 * Outputs the scheme graph.
 */
void output_schemes(std::ostream &out) {
  out << "digraph {\n  node [shape=record];\n";
  for (real_dependencies::iterator i = reals.begin(), i_end = reals.end(); i != i_end; ++i)
  {
    real_dependency &r = i->second;
    std::string var = dump_real(i->first);
    out << "  \"" << var << "\" [label=\"{";
    for (char j: var) {
      switch (j) {
      case '<': out << "&lt;"; break;
      case '>': out << "&gt;"; break;
      case '|': out << "&#124;"; break;
      default: out << j;
      }
    }
    int num_th = 0;
    for (proof_scheme const *j: r.schemes)
      out << "|<" << ++num_th << '>' << typeid(*j).name();
    out << "}\"];\n";
    num_th = 0;
    for (proof_scheme const *j: r.schemes) {
      ++num_th;
      for (predicated_real const &k: j->needed_reals)
        out << "  \"" << var << "\":" << num_th << " -> \"" << dump_real(k) << "\";\n";
    }
  }
  out << "}\n";
}

RUN_LAST(proof_paths_cleaner) {
  for(real_dependencies::iterator i = reals.begin(), i_end = reals.end(); i != i_end; ++i) {
    for (proof_scheme const *j: i->second.schemes)
      delete j;
  }
}

static bool is_usable(node const *n)
{
  property const &p = n->get_result();
  return !p.real.pred_bnd() || is_bounded(p.bnd());
}

static bool fill_hypotheses(property *hyp, preal_vect const &v)
{
  for(predicated_real const &i: v) {
    node *n = find_proof(i);
    if (!n || !is_usable(n)) return false;
    *(hyp++) = n->get_result();
  }
  return true;
}

int stat_tested_app = 0;

/**
 * Reduces disjunctions inside hypotheses given the knowledge of node @a n.
 */
static void reduce_hypotheses(node *n, std::list<logic_node *> &trees, logic_node *excluded, undefined_map *umap)
{
  std::list<logic_node *>::iterator i = trees.begin();
  while (i != trees.end())
  {
    if (*i == excluded) { ++i; continue; }
    property_tree t;
    bool changed;
    //std::cerr << dump_property_tree((*i)->tree) << '\n';
    int v = (*i)->tree.try_simplify(simpl_prop(n->get_result(), true, umap), changed, t);
    if (!v && !changed) { ++i; continue; }
    //std::cerr << "becomes " << dump_property_tree(t) << '\n';
    if ((v == 0 && changed) || v < 0) {
      logic_node *m = new logic_node(t, *i, n);
      if (v < 0) { top_graph->set_contradiction(m); return; }
      ++m->nb_good;
      trees.push_back(m);
    }
    (*i)->remove_known();
    i = trees.erase(i);
  }
}

static bool insert_atom(logic_node *n, property const &p, int i, std::list<logic_node *> &trees, property_tree &targets, scheme_queue *pending_schemes)
{
  int score = top_graph->try_property(p, n->depth + 1, false);
  if (score == 0) return false;
  //std::cerr << "Creating node for " << dump_property(p) << '\n';
  node *m = new logicp_node(p, n, i);
  top_graph->insert_node(m);
  if (top_graph->get_contradiction()) return true;
  if (!targets.empty() && targets.simplify(simpl_prop(m->get_result(), true, NULL)) < 0) return true;
  reduce_hypotheses(m, trees, n, NULL);
  if (top_graph->get_contradiction()) return true;
  if (pending_schemes && is_usable(m))
    insert_dependent(*pending_schemes, p.real, score == 1, NULL);
  return false;
}

/**
 * Repeatedly splits and reduces hypotheses.
 */
static bool split_hypotheses(std::list<logic_node *> &trees, property_tree &targets, scheme_queue *pending_schemes)
{
  restart:
  for (std::list<logic_node *>::iterator j = trees.begin(),
       j_end = trees.end(); j != j_end; ++j)
  {
    logic_node *n = *j;
    property_tree const &hyp = n->tree;
    assert(!hyp.empty());
    if (hyp.atom) {
      if (hyp.conjunction) {
        if (insert_atom(n, *hyp.atom, 0, trees, targets, pending_schemes))
          return true;
        n->remove_known();
        trees.erase(j);
        goto restart;
      }
      if (hyp.atom->real.pred() != PRED_BND) continue;
      interval const &b = hyp.atom->bnd();
      if (!is_defined(b) || is_bounded(b)) continue;
      property p(hyp.atom->real);
      if (lower(b) != number::neg_inf) p.bnd() = interval(number::neg_inf, lower(b));
      else p.bnd() = interval(upper(b), number::pos_inf);
      if (insert_atom(n, p, -1, trees, targets, pending_schemes))
        return true;
    } else if (hyp.conjunction) {
      for (int i = 0; i != 2; ++i) {
        property_tree const *t = i ? hyp.right : hyp.left;
        if (t->left || !t->conjunction) {
          logic_node *m = new logic_node(*t, n, i + 1);
          ++m->nb_good;
          trees.push_back(m);
        } else if (insert_atom(n, *t->atom, i + 1, trees, targets, pending_schemes))
          return true;
      }
      n->remove_known();
      trees.erase(j);
      goto restart;
    }
  }
  return false;
}

bool graph_t::populate_aux(property_tree &targets, proof_context const &pctx)
{
  if (contradiction) return true;
  int max_iter = exp(5 * (pctx.depth - 1)) * (10*1000*1000);
  unsigned max_depth = sqrt(max_iter);
  if (split_hypotheses(trees, targets, NULL))
    return true;

  scheme_queue missing_schemes;
  initialize_scheme_list(missing_schemes);
  for (auto const &i: known_reals) {
    if (!is_usable(i.second)) continue;
    insert_dependent(missing_schemes, i.first, false, NULL);
  }

  int iter = max_iter;
  proof_scheme const *s;
  while (iter > 0 && --iter && (s = missing_schemes.pop()))
  {
    s->visited = 0; // allows the scheme to be reused later, if needed
    ++stat_tested_app;
    std::vector<property> hyps_(s->needed_reals.size());
    property *hyps = hyps_.size() ? &hyps_[0] : NULL;
    if (!fill_hypotheses(hyps, s->needed_reals)) {
      // The scheme is missing some hypotheses.
      continue;
    }
    property res(s->real);
    std::string name = s->default_name;
    s->compute(hyps, res, name);
    if (res.null() || (res.real.pred_bnd() && !is_defined(res.bnd()))) {
      // The scheme failed.
      continue;
    }
    unsigned new_depth = 0;
    for (predicated_real const &r: s->needed_reals) {
      node const *m = find_proof(r);
      if (m && m->depth >= new_depth) new_depth = m->depth + 1;
    }
    int score = try_property(res, new_depth);
    if (score == 0 || new_depth >= max_depth) {
      // The scheme did not find anything new, so lower its score.
      s->score = 0;
      continue;
    } else if (score == 1 && res.real.pred() == PRED_BND &&
               res.real.real() == pctx.current_split) {
      // The scheme did find something of little interest about the expression currently being bisected,
      // so ignore it and lower its score, to favor bisection.
      s->score = 0;
      continue;
    } else if (score == 1 && s->score != 1) {
      // The scheme did find something of little interest, so retry it later.
      s->score = 1;
      s->visited = visit_counter;
      missing_schemes.push(s, true);
      continue;
    } else {
      // The scheme did find something, either of interest or discarded earlier.
      // Adjust the score accordingly.
      s->score = score == 2 ? 2 : 0;
    }
    //std::cerr << dump_property(res) << '\t' << name << '\n';
    node *n = create_theorem(s->needed_reals.size(), hyps, res, name, s);
    insert_node(n);
    if (contradiction) {
      // We have got a contradiction, everything is true.
      return true;
    }
    insert_dependent(missing_schemes, s->real, score == 1, s);
    reduce_hypotheses(n, trees, NULL, NULL);
    if (contradiction) return true;
    if (split_hypotheses(trees, targets, &missing_schemes))
      return true;
    if (!targets.empty() &&
        targets.simplify(simpl_prop(n->get_result(), true, NULL)) < 0)
      return true;
  }

  return false;
}

/**
 * Fills this graph by iteratively applying theorems until
 * \li @a targets are satisfied, or
 * \li a contradiction is found, or
 * \li a fixpoint is reached for the results, or
 * \li an upper bound on the number of iterations is reached.
 */
void graph_t::populate(property_tree const &targets,
  dichotomy_sequence const &dichotomy, proof_context const &pctx, undefined_map *umap)
{
  if (contradiction) return;
  property_tree current_targets = targets;
  graph_loader loader(this);

  if (split_hypotheses(trees, current_targets, NULL)) return;

  if (populate_aux(current_targets, pctx)) return;

  for (dichotomy_sequence::const_iterator dichotomy_it = dichotomy.begin(),
       dichotomy_end = dichotomy.end(); dichotomy_it != dichotomy_end; ++dichotomy_it)
  {
    dichotomize(*dichotomy_it, pctx);
    if (contradiction) return;
    for (auto const &i: known_reals) {
      if (!is_usable(i.second)) continue;
      reduce_hypotheses(i.second, trees, NULL, NULL);
      if (contradiction) return;
    }
    if (split_hypotheses(trees, current_targets, NULL)) return;
    if (populate_aux(current_targets, pctx)) return;
  }

  // Look for an expression on which to perform a point-based dichotomy.
  splitting s;
  for (logic_node const *i: trees)
    i->tree.get_splitting(s);
  unsigned max_pts = 0;
  splitting::value_type const *sv = NULL;
  for (auto const &i: s)
  {
    unsigned nb = i.second.size();
    if (nb <= max_pts) continue;
    node *n = find_proof(predicated_real(i.first, PRED_BND));
    if (n) {
      interval const &b = n->get_result().bnd();
      if (is_singleton(b)) continue;
      nb = 0;
      for (split_point const &j: i.second) {
        if (lower(b) < j.pt && j.pt < upper(b)) ++nb;
      }
      if (nb <= max_pts) continue;
    }
    max_pts = nb;
    sv = &i;
  }
  if (max_pts)
  {
    uintptr_t ds = 0;
    number prev = number::neg_inf;
    for (split_point const &i: sv->second) {
      if (i.pt == prev) continue;
      ds = fill_splitter(ds, i);
      prev = i.pt;
    }
    dichotomy_var dv = { sv->first, ds };
    dichotomy_hint dh = { dvar_vect(1, dv), property_tree(), false };
    dichotomize(dh, pctx);
    clear_splitter(ds);
  }

  // Fill all the remaining holes in the formula.
  if (!umap) return;
  for (auto const &i: known_reals) {
    if (!is_usable(i.second)) continue;
    reduce_hypotheses(i.second, trees, NULL, umap);
    if (contradiction) return;
  }
}
