/* SPDX-FileCopyrightText: 2023 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup bli
 *
 * Language grammar: https://www.graphviz.org/doc/info/lang.html
 * Attributes: https://www.graphviz.org/doc/info/attrs.html
 * Node Shapes: https://www.graphviz.org/doc/info/shapes.html
 * Preview: https://dreampuf.github.io/GraphvizOnline
 */

#pragma once

#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_utility_mixins.hh"
#include "BLI_vector.hh"

#include "BLI_dot_export_attribute_enums.hh"

#include <iosfwd>
#include <optional>

namespace blender::dot_export {

class Graph;
class DirectedGraph;
class UndirectedGraph;
class Node;
class NodePort;
class DirectedEdge;
class UndirectedEdge;
class Cluster;

class Attributes {
 private:
  Map<std::string, std::string> attributes_;

 public:
  void export__as_bracket_list(std::stringstream &ss) const;

  void set(StringRef key, StringRef value)
  {
    attributes_.add_overwrite(key, value);
  }

  void set(StringRef key, float value)
  {
    attributes_.add_overwrite(key, std::to_string(value));
  }
};

class Graph {
 private:
  Vector<std::unique_ptr<Node>> nodes_;
  Vector<std::unique_ptr<Cluster>> clusters_;

  Set<Node *> top_level_nodes_;
  Set<Cluster *> top_level_clusters_;

  friend Cluster;
  friend Node;

 public:
  Attributes attributes;

  Node &new_node(StringRef label);
  Cluster &new_cluster(StringRef label = "");

  void export__declare_nodes_and_clusters(std::stringstream &ss) const;

  void set_rankdir(Attr_rankdir rankdir)
  {
    attributes.set("rankdir", rankdir_to_string(rankdir));
  }

  void set_random_cluster_bgcolors();
};

class Cluster {
 private:
  Graph &graph_;
  Cluster *parent_ = nullptr;
  Set<Cluster *> children_;
  Set<Node *> nodes_;

  friend Graph;
  friend Node;

 public:
  Attributes attributes;

  Cluster(Graph &graph) : graph_(graph) {}

  void export__declare_nodes_and_clusters(std::stringstream &ss) const;

  std::string name() const
  {
    return "cluster_" + std::to_string(uintptr_t(this));
  }

  void set_parent_cluster(Cluster *new_parent);
  void set_parent_cluster(Cluster &cluster)
  {
    this->set_parent_cluster(&cluster);
  }

  Cluster *parent_cluster()
  {
    return parent_;
  }

  void set_random_cluster_bgcolors();

  bool contains(Node &node) const;
};

class Node {
 private:
  Graph &graph_;
  Cluster *cluster_ = nullptr;

  friend Graph;

 public:
  Attributes attributes;

  Node(Graph &graph) : graph_(graph) {}

  void set_parent_cluster(Cluster *cluster);
  void set_parent_cluster(Cluster &cluster)
  {
    this->set_parent_cluster(&cluster);
  }

  Cluster *parent_cluster()
  {
    return cluster_;
  }

  void set_shape(Attr_shape shape)
  {
    attributes.set("shape", shape_to_string(shape));
  }

  /* See https://www.graphviz.org/doc/info/attrs.html#k:color. */
  void set_background_color(StringRef name)
  {
    attributes.set("fillcolor", name);
    attributes.set("style", "filled");
  }

  void export__as_id(std::stringstream &ss) const;

  void export__as_declaration(std::stringstream &ss) const;
};

class UndirectedGraph final : public Graph {
 private:
  Vector<std::unique_ptr<UndirectedEdge>> edges_;

 public:
  std::string to_dot_string() const;

  UndirectedEdge &new_edge(NodePort a, NodePort b);
};

class DirectedGraph final : public Graph {
 private:
  Vector<std::unique_ptr<DirectedEdge>> edges_;

 public:
  std::string to_dot_string() const;

  DirectedEdge &new_edge(NodePort from, NodePort to);
};

class NodePort {
 private:
  Node *node_;
  std::optional<std::string> port_name_;
  std::optional<std::string> port_position_;

 public:
  NodePort(Node &node,
           std::optional<std::string> port_name = {},
           std::optional<std::string> port_position = {})
      : node_(&node), port_name_(std::move(port_name)), port_position_(std::move(port_position))
  {
  }

  void to_dot_string(std::stringstream &ss) const;
};

class Edge : blender::NonCopyable, blender::NonMovable {
 protected:
  NodePort a_;
  NodePort b_;

 public:
  Attributes attributes;

  Edge(NodePort a, NodePort b) : a_(std::move(a)), b_(std::move(b)) {}

  void set_arrowhead(Attr_arrowType type)
  {
    attributes.set("arrowhead", arrowType_to_string(type));
  }

  void set_arrowtail(Attr_arrowType type)
  {
    attributes.set("arrowtail", arrowType_to_string(type));
  }

  void set_dir(Attr_dirType type)
  {
    attributes.set("dir", dirType_to_string(type));
  }

  void set_label(StringRef label)
  {
    attributes.set("label", label);
  }
};

class DirectedEdge : public Edge {
 public:
  DirectedEdge(NodePort from, NodePort to) : Edge(std::move(from), std::move(to)) {}

  void export__as_edge_statement(std::stringstream &ss) const;
};

class UndirectedEdge : public Edge {
 public:
  UndirectedEdge(NodePort a, NodePort b) : Edge(std::move(a), std::move(b)) {}

  void export__as_edge_statement(std::stringstream &ss) const;
};

std::string color_attr_from_hsv(float h, float s, float v);

struct NodeWithSockets {
  struct Socket {
    std::string name;
    std::optional<std::string> fontcolor;
  };
  struct Input : public Socket {};
  struct Output : public Socket {};

  std::string node_name;
  Vector<Input> inputs;
  Vector<Output> outputs;

  Input &add_input(std::string name)
  {
    this->inputs.append({});
    Input &input = this->inputs.last();
    input.name = std::move(name);
    return input;
  }

  Output &add_output(std::string name)
  {
    this->outputs.append({});
    Output &output = this->outputs.last();
    output.name = std::move(name);
    return output;
  }
};

class NodeWithSocketsRef {
 private:
  Node *node_;

 public:
  NodeWithSocketsRef(Node &node, const NodeWithSockets &data);

  Node &node()
  {
    return *node_;
  }

  NodePort input(int index) const
  {
    std::string port = "\"in" + std::to_string(index) + "\"";
    return NodePort(*node_, port, "w");
  }

  NodePort output(int index) const
  {
    std::string port = "\"out" + std::to_string(index) + "\"";
    return NodePort(*node_, port, "e");
  }
};

}  // namespace blender::dot_export
