/*
   Copyright (C) 2013 - 2014 by Guillaume Melquiond <guillaume.melquiond@inria.fr>
   Copyright (C) 2023 by Tom Hubrecht <tom.hubrecht@ens.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 <cassert>
#include <ostream>
#include <map>
#include <sstream>

#include "backends/backend.hpp"
#include "numbers/interval_utility.hpp"
#include "numbers/real.hpp"
#include "parser/ast.hpp"
#include "parser/ast_real.hpp"
#include "proofs/proof_graph.hpp"

typedef std::map<graph_t *, int> graph_map;

extern std::string get_real_split(number const &f, int &exp, bool &zero, bool);

static std::string convert_name(std::string const &name) {
  size_t p = name.find_first_of("_<>");
  if (p == std::string::npos)
    return name;
  std::string buf;
  size_t o = 0;
  do {
    buf += name.substr(o, p - o);
    switch (name[p]) {
    case '_':
      buf += "\\_";
      break;
    case '<':
      buf += "\\langle ";
      break;
    case '>':
      buf += "\\rangle ";
      break;
    default:
      assert(false);
    }
    o = p + 1;
    p = name.find_first_of("_<>", o);
  } while (p != std::string::npos);
  buf += name.substr(o);
  return buf;
}

static std::string display(number const &f) {
  bool zero;
  int exp;
  std::string t = get_real_split(f, exp, zero, true);
  if (zero)
    return "0";
  if (!exp)
    return t;
  std::ostringstream s;
  s << t << " \\cdot 2^{" << exp << '}';
  return s.str();
}

static std::string display(interval const &i) {
  std::ostringstream s;
  s << '[' << display(lower(i)) << ',' << display(upper(i)) << ']';
  return s.str();
}

static std::string display(ast_real const *r, unsigned prio = 0) {
  if (hidden_real const *h = boost::get<hidden_real const>(r))
    r = h->real;
  if (r->name)
    return "\\mathit{" + convert_name(r->name->name) + "}";
  if (ast_number const *const *nn = boost::get<ast_number const *const>(r)) {
    ast_number const &n = **nn;
    std::string m = (n.mantissa.size() > 0 && n.mantissa[0] == '+')
                        ? n.mantissa.substr(1)
                        : n.mantissa;
    if (n.base == 0)
      return "0";
    if (n.exponent == 0)
      return m;
    std::ostringstream s;
    s << m << " \\cdot " << n.base << "^{" << n.exponent << "}";
    return s.str();
  }
  if (real_op const *o = boost::get<real_op const>(r)) {
    if (o->type == UOP_RND) {
      std::ostringstream s;
      s << "\\mathrm{" << convert_name(o->fun->pretty_name()) << "}(";
      bool first = true;
      for (ast_real const *i: o->ops) {
        if (first)
          first = false;
        else
          s << ", ";
        s << display(i, 0);
      }
      s << ')';
      return s.str();
    }
    static unsigned const pr[] = {0, 2, 0, 0, 0, 0, 1, 0, 0, 0};
    std::string t = display(o->ops[0], pr[o->type]);
    std::ostringstream s;
    if (o->ops.size() == 1) {
      switch (o->type) {
      case UOP_ABS:
        s << "\\left| " << t << " \\right|";
        prio = 0;
        break;
      case UOP_SQRT:
        s << "\\sqrt{" << t << '}';
        prio = 0;
        break;
      case UOP_NEG:
        s << '-' << t;
        break;
      default:
        assert(false);
      }
    } else {
      std::string u =
          display(o->ops[1], o->type == BOP_DIV ? 0 : pr[o->type] + 1);
      switch (o->type) {
      case BOP_ADD:
        s << t << " + " << u;
        break;
      case BOP_SUB:
        s << t << " - " << u;
        break;
      case BOP_MUL:
        s << t << " \\times " << u;
        break;
      case BOP_DIV:
        s << "\\frac{" << t << "}{" << u << '}';
        prio = 0;
        break;
      default:
        assert(false);
      }
    }
    if (prio <= pr[o->type])
      return s.str();
    return "\\left( " + s.str() + " \\right)";
  }
  assert(false);
  return "...";
}

static std::string display(property const &p) {
  // When the property is false, directly return \bot
  if (p.real.null())
    return "\\bot";

  std::ostringstream s;
  std::string r = display(p.real.real());

  switch (p.real.pred()) {
  case PRED_BND:
    s << r;
    break;
  case PRED_ABS:
    s << "\\left| " << r << " \\right|";
    break;
  case PRED_REL:
    s << r << " \\diamond " << display(p.real.real2());
    break;
  case PRED_LIN:
    s << r << " \\propto " << display(p.real.real2());
    break;
  case PRED_FIX:
    s << "\\mathrm{FIX}\\left( " << r << " \\right) = " << p.cst();
    break;
  case PRED_FLT:
    s << "\\mathrm{FLT}\\left(" << r << " \\right) = " << p.cst();
    break;
  case PRED_EQL:
    s << r << " = " << display(p.real.real2());
    break;
  case PRED_NZR:
    s << r << " \\neq 0";
    break;
  case PRED_NUL:
    assert(false);
  }

  // If the property is not a bound, it is fully displayed, else we need to
  // dispay the interval
  if (!p.real.pred_bnd())
    return s.str();

  interval const &bnd = p.bnd();

  if (p.real.pred() == PRED_ABS && lower(bnd) == 0) {
    // Instead of writing |x| \in [0, y] just write |x| \le y
    s << " \\le " << display(upper(bnd));

  } else if (p.real.pred() != PRED_ABS && upper(bnd) == -lower(bnd) &&
             upper(bnd) != 0) {
    // Instead of writing x \in [-y, y] just write |x| \le y
    r = s.str();
    s.str(std::string());
    s << "\\left| " << r << " \\right| \\le " << display(upper(bnd));

  } else if (lower(bnd) == number::neg_inf) {
    // Instead of writing x \in [-\infty, y] just write x \le y
    s << " \\le " << display(upper(bnd));

  } else if (upper(bnd) == number::pos_inf) {
    // Instead of writing x \in [y, \infty] just write x \ge y
    s << " \\ge " << display(lower(bnd));

  } else {
    // Default case, write x \in [y, z]
    s << " \\in " << display(bnd);
  }
  return s.str();
}

/**
 * Retreives the real property
 */
static property const &fetch(property const &p) {
  if (p.real.pred_bnd() && !is_defined(p.bnd())) {
    undefined_map::const_iterator i = instances->find(p.real);
    assert(i != instances->end());
    return i->second;
  }
  return p;
}

/**
 * Displays a property tree
 */
static std::string display(property_tree const &t) {
  std::ostringstream s;
  if (t.left) {
    s << "\\left( " << display(*t.left)
      << (t.conjunction ? " \\land " : " \\lor ") << display(*t.right)
      << " \\right)";
  } else if (t.atom) {
    if (!t.conjunction)
      s << "\\neg\\left( ";
    s << display(fetch(*t.atom));
    if (!t.conjunction)
      s << " \\right)";
  } else
    s << "\\bot";
  return s.str();
}

static id_cache<node const *> displayed_nodes;

static graph_map graph_cache;

/**
 * Displays a node
 */
static std::string display(node const *n) {
  assert(n);
  switch (n->type) {
  case LOGIC: {
    logic_node const *ln = static_cast<logic_node const *>(n);
    if (!ln->before)
      return "";
    break;
  }
  case LOGICP: {
    logicp_node const *ln = static_cast<logicp_node const *>(n);
    if (!ln->index)
      return display(ln->before);
    break;
  }
  default:
    break;
  }

  int n_id = displayed_nodes.find(n);
  int g_id;
  auto g_cached = graph_cache.find(n->graph);

  if (g_cached == graph_cache.end()) {
    // We are in a new graph, create a box with the current hypothesis
    g_id = graph_cache.size();
    graph_cache[n->graph] = g_id;
    *out << "g" << g_id
         << ": 'By hypothesis' {\n  style.font-size: 15\n  th: |||latex "
         << display(n->graph->get_hypotheses()) << " |||\n";
    *out << "}\n";
  } else
    g_id = g_cached->second;

  std::string name = composite('l', n_id);

  if (n_id < 0)
    return name;

  std::vector<std::string> hyps;

  node_vect const pred = n->get_subproofs(); // transient result, so copy it

  for (node const *i: pred) {
    hyps.push_back(display(i));
  }

  *out << name << ": ";

  switch (n->type) {
  case LOGIC:
    if (hyps.size() < 2)
      *out << "By selecting a component";
    else
      *out << "By discarding contradictory literals";
    break;
  case LOGICP:
    *out << "By selecting a component";
    break;
  case MODUS: {
    std::string name = static_cast<modus_node const *>(n)->target->name;
    if (!name.empty())
      *out << "Using theorem `" << name << "`";
    else
      *out << "Using a provided rewriting";
  } break;
  case INTERSECTION:
    *out << "By performing an intersection";
    break;
  case UNION:
    *out << "By performing an union";
    break;
  default:
    assert(false);
  }

  *out << " {\n  style.font-size: 15\n  th: |||latex ";
  if (n->type == LOGIC) {
    logic_node const *ln = static_cast<logic_node const *>(n);
    *out << display(ln->tree);
  } else {
    *out << display(n->get_result());
  }
  *out << " |||\n}\n";

  for (std::string const &i: hyps) {
    if (i.empty())
      // If there are no hypotheses, it means we use those of the graph, thus
      // link the graph to the node
      *out << "g" << g_id << " -> " << name << "\n";
    else
      // Link the hypothesis to the node
      *out << i << " -> " << name << "\n";
  }
  return name;
}

struct d2_backend : backend {
  d2_backend(): backend("d2", "produce a D2 graph script") {}
  void initialize(std::ostream &o) { out = &o; }
  void finalize() {}
  virtual std::string rewrite(ast_real const *, ast_real const *,
                              pattern_cond_vect const &) {
    return "";
  };
  virtual std::string theorem(node const *n) { return display(n); }
};

static struct d2_backend dummy;
