/**
 * @file DotCircuit.cpp
 * @brief DotCircuit method implementations and GraphViz rendering.
 *
 * Implements:
 * - @c DotCircuit::addGate(): allocate a gate and extend the @c desc vector.
 * - @c DotCircuit::setGate(const uuid&, DotGate): create a gate by UUID.
 * - @c DotCircuit::setGate(const uuid&, DotGate, std::string): create a gate
 *   with a label string stored in @c desc.
 * - @c DotCircuit::toString(): return the description string for a gate.
 * - @c DotCircuit::render(): produce a complete GraphViz @c digraph string
 *   with semiring symbols (⊗, ⊕, ⊖, δ) as node labels and the
 *   provenance token UUIDs as edge labels for input gates.
 */
#include "DotCircuit.h"
#include "external_tool.h"
#include "ToolRegistry.h"
#include "scoped_tempdir.h"
#include <type_traits>

extern "C"
{
#include "provsql_utils.h"
#include <unistd.h>
}

#include <cassert>
#include <string>
#include <fstream>
#include <sstream>
#include <cstdlib>

// Has to be redefined because of name hiding
gate_t DotCircuit::setGate(const uuid &u, DotGate type)
{
  auto id = Circuit::setGate(u, type);
  if(type == DotGate::IN)
    inputs.insert(id);
  return id;
}

gate_t DotCircuit::setGate(const uuid &u, DotGate type, std::string d)
{
  auto id = setGate(u, type);
  desc[static_cast<std::underlying_type<gate_t>::type>(id)] = d;
  return id;
}

gate_t DotCircuit::addGate()
{
  auto id=Circuit::addGate();
  desc.push_back("");
  return id;
}

//Outputs the gate in Graphviz dot format
std::string DotCircuit::toString(gate_t) const
{
  std::string op;
  std::string result="digraph circuit{\n graph [rankdir=UD] ;\n";

  //looping through the gates
  //eliminating the minusr and minusl gates
  unsigned i = 0;
  for (auto g : gates)
  {
    if (g != DotGate::OMINUSR && g != DotGate::OMINUSL)
    {
      result += std::to_string(i) + " [label=";
      switch (g)
      {
      case DotGate::IN:
        result += "\"" + desc[i] + "\"";
        break;
      case DotGate::OMINUS:
        result += "\"⊖\"";
        break;
      case DotGate::UNDETERMINED:
        result += "\"?\"";
        break;
      case DotGate::OTIMES:
        result += "\"⊗\"";
        break;
      case DotGate::OPLUS:
        result += "\"⊕\"";
        break;
      case DotGate::DELTA:
        result += "\"δ\"";
        break;
      case DotGate::EQ:
        result += "\"" + desc[i] + "\"";
        break;
      case DotGate::PROJECT:
        result += "\"Π" + desc[i] + "\"";
        break;
      case DotGate::OMINUSR:
      case DotGate::OMINUSL:
        break;
      }
      if(i==0)
        result+=",shape=\"double\"";
      result+="];\n";
    }
    i++;
  }

  //looping through the gates and their wires
  for(i=0; i<wires.size(); ++i) {
    if(gates[i] != DotGate::OMINUSR && gates[i] != DotGate::OMINUSL) {
      std::unordered_map<gate_t, unsigned> number_gates;
      for(auto s: wires[i]) {
        if(number_gates.find(s)!=number_gates.end()) {
          number_gates[s] = number_gates[s]+1;
        }
        else
        {
          number_gates[s] = 1;
        }
      }
      for(auto [s,n]: number_gates)
      {
        if(getGateType(s) == DotGate::OMINUSR || getGateType(s) == DotGate::OMINUSL) {
          for(auto t: getWires(s)) {
            result += std::to_string(i)+" -> "+to_string(t);
            if(getGateType(s) == DotGate::OMINUSR)
              result += " [label=\"R\"];\n";
            else
              result += " [label=\"L\"];\n";
          }
        }
        else {
          result += std::to_string(i)+" -> "+to_string(s);
          if(n==1) {
            result += ";\n";
          }
          else
          {
            result += " [label=\"" + std::to_string(n) + "\"];\n";
          }
        }
      }
    }
  }
  return result + "}";
}

std::string DotCircuit::render() const {
  //Executing the Graphviz dot renderer through graph-easy for ASCII
  //output. The executable and its enabled flag come from the tool
  //registry (logical name "graph-easy"); a record absent from the
  //registry resolves to itself.
  std::string render_bin = "graph-easy";
  std::string render_argtpl = "--as=boxart --output={out} {in}";
  {
    const provsql::ToolRecord *rec = provsql::tool_registry().find("graph-easy");
    if(rec != nullptr) {
      if(!rec->enabled)
        throw CircuitException(
                "Tool 'graph-easy' is disabled in the tool registry");
      render_bin = rec->binary;
      render_argtpl = rec->argtpl;
    }
  }
  if(find_external_tool(render_bin).empty()) {
    throw CircuitException(
            render_bin + " not found on PATH; install it or add its "
            "directory to provsql.tool_search_path");
  }

  // Private 0700 directory rather than a bare mkstemp file so the
  // deterministically-derived sibling output path (.out) cannot be
  // raced by a local user pre-creating a symlink before graph-easy
  // opens it. The RAII guard unlinks both files and rmdirs at scope
  // exit even when the body throws.
  provsql::ScopedTempDir tmp;
  std::string filename    = tmp.file("dot");
  std::string outfilename = tmp.file("dot.out");

  {
    std::ofstream ofs(filename);
    ofs << toString(gate_t{0});
  }

  std::string cmdline=provsql::expandCommandTemplate(
      render_argtpl, render_bin, filename, outfilename);

  int retvalue = run_external_tool(cmdline);
  if(retvalue)
    throw CircuitException(format_external_tool_status(retvalue, "graph-easy"));

  std::ifstream ifs(outfilename.c_str());
  std::string str((std::istreambuf_iterator<char>(ifs)),
                  std::istreambuf_iterator<char>());

  if(provsql_verbose>=20)
    tmp.keep();

  return str;
}
