#ifndef IM_NODE_FLOW
#define IM_NODE_FLOW
#pragma once

#include "context_wrapper.h"
#include "imgui.h"
#include "imgui_bezier_math.h"
#include "imgui_internal.h"
#include <algorithm>
#include <cmath>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

namespace ImFlow {
// -----------------------------------------------------------------------------------------------------------------
// HELPERS

/**
 * @brief <BR>Draw a sensible bezier between two points
 * @param p1 Starting point
 * @param p2 Ending point
 * @param color Color of the curve
 * @param thickness Thickness of the curve
 */
inline static void smart_bezier(const ImVec2 &p1, const ImVec2 &p2, ImU32 color,
                                float thickness);

/**
 * @brief <BR>Collider checker for smart_bezier
 * @details Projects the point "p" orthogonally onto the bezier curve and
 *          checks if the distance is less than the given radius.
 * @param p Point to be tested
 * @param p1 Starting point of smart_bezier
 * @param p2 Ending point of smart_bezier
 * @param radius Lateral width of the hit box
 * @return [TRUE] if "p" is inside the collider
 *
 * Intended to be used in union with smart_bezier();
 */
inline static bool smart_bezier_collider(const ImVec2 &p, const ImVec2 &p1,
                                         const ImVec2 &p2, float radius);

// -----------------------------------------------------------------------------------------------------------------
// CLASSES PRE-DEFINITIONS

template <typename T> class InPin;
template <typename T> class OutPin;
class Pin;
class BaseNode;
class ImNodeFlow;

// -----------------------------------------------------------------------------------------------------------------
// PIN'S PROPERTIES

/**
 * @brief <BR>Basic filters
 * @details List of, ready to use, basic filters. It's possible to create more
 * filters with the help of "ConnectionFilter_MakeCustom".
 */
enum ConnectionFilter_ {
  ConnectionFilter_None = 0,
  ConnectionFilter_SameNode = 1 << 1,
  ConnectionFilter_Bool = 1 << 2,
  ConnectionFilter_Int = 1 << 3,
  ConnectionFilter_Float = 1 << 4,
  ConnectionFilter_Double = 1 << 5,
  ConnectionFilter_String = 1 << 6,
  ConnectionFilter_MakeCustom = 1 << 7,
  ConnectionFilter_Numbers =
      ConnectionFilter_Int | ConnectionFilter_Float | ConnectionFilter_Double
};
typedef long ConnectionFilter;

typedef unsigned long long int PinUID;

/**
 * @brief Extra pin's style setting
 */
struct PinStyleExtras {
  /// @brief Top and bottom spacing
  ImVec2 padding = ImVec2(3.f, 1.f);
  /// @brief Border and background corner rounding
  float bg_radius = 8.f;
  /// @brief Border thickness
  float border_thickness = 1.f;
  /// @brief Background color
  ImU32 bg_color = IM_COL32(23, 16, 16, 0);
  /// @brief Background color when hovered
  ImU32 bg_hover_color = IM_COL32(100, 100, 255, 70);
  /// @brief Border color
  ImU32 border_color = IM_COL32(255, 255, 255, 0);

  /// @brief Link thickness
  float link_thickness = 2.6f;
  /// @brief Link thickness when dragged
  float link_dragged_thickness = 2.2f;
  /// @brief Link thickness when hovered
  float link_hovered_thickness = 3.5f;
  /// @brief Thickness of the outline of a selected link
  float link_selected_outline_thickness = 0.5f;
  /// @brief Color of the outline of a selected link
  ImU32 outline_color = IM_COL32(80, 20, 255, 200);

  /// @brief Spacing between pin content and socket
  float socket_padding = 6.6f;
};

/**
 * @brief Defines the visual appearance of a pin
 */
class PinStyle {
public:
  PinStyle(ImU32 color, int socket_shape, float socket_radius,
           float socket_hovered_radius, float socket_connected_radius,
           float socket_thickness)
      : color(color), socket_shape(socket_shape), socket_radius(socket_radius),
        socket_hovered_radius(socket_hovered_radius),
        socket_connected_radius(socket_connected_radius),
        socket_thickness(socket_thickness) {}

  /// @brief Socket and link color
  ImU32 color;
  /// @brief Socket shape ID
  int socket_shape;
  /// @brief Socket radius
  float socket_radius;
  /// @brief Socket radius when hovered
  float socket_hovered_radius;
  /// @brief Socket radius when connected
  float socket_connected_radius;
  /// @brief Socket outline thickness when empty
  float socket_thickness;
  /// @brief List of less common properties
  PinStyleExtras extra;

public:
  /// @brief <BR>Default cyan style
  static std::shared_ptr<PinStyle> cyan() {
    return std::make_shared<PinStyle>(
        PinStyle(IM_COL32(87, 155, 185, 255), 0, 4.f, 4.67f, 3.7f, 1.f));
  }
  /// @brief <BR>Default green style
  static std::shared_ptr<PinStyle> green() {
    return std::make_shared<PinStyle>(
        PinStyle(IM_COL32(90, 191, 93, 255), 4, 4.f, 4.67f, 4.2f, 1.3f));
  }
  /// @brief <BR>Default blue style
  static std::shared_ptr<PinStyle> blue() {
    return std::make_shared<PinStyle>(
        PinStyle(IM_COL32(90, 117, 191, 255), 0, 4.f, 4.67f, 3.7f, 1.f));
  }
  /// @brief <BR>Default brown style
  static std::shared_ptr<PinStyle> brown() {
    return std::make_shared<PinStyle>(
        PinStyle(IM_COL32(191, 134, 90, 255), 0, 4.f, 4.67f, 3.7f, 1.f));
  }
  /// @brief <BR>Default red style
  static std::shared_ptr<PinStyle> red() {
    return std::make_shared<PinStyle>(
        PinStyle(IM_COL32(191, 90, 90, 255), 0, 4.f, 4.67f, 3.7f, 1.f));
  }
  /// @brief <BR>Default white style
  static std::shared_ptr<PinStyle> white() {
    return std::make_shared<PinStyle>(
        PinStyle(IM_COL32(255, 255, 255, 255), 5, 4.f, 4.67f, 4.2f, 1.f));
  }
};

// -----------------------------------------------------------------------------------------------------------------
// NODE'S PROPERTIES

typedef uintptr_t NodeUID;

/**
 * @brief Defines the visual appearance of a node
 */
class NodeStyle {
public:
  NodeStyle(ImU32 header_bg, ImColor header_title_color, float radius)
      : header_bg(header_bg), header_title_color(header_title_color),
        radius(radius) {}

  /// @brief Body's background color
  ImU32 bg = IM_COL32(55, 64, 75, 255);
  /// @brief Header's background color
  ImU32 header_bg;
  /// @brief Header title color
  ImColor header_title_color;
  /// @brief Border color
  ImU32 border_color = IM_COL32(30, 38, 41, 128);
  /// @brief Border color when selected
  ImU32 border_selected_color = IM_COL32(170, 190, 205, 255);

  /// @brief Body's content padding (Left Top Right Bottom)
  ImVec4 padding = ImVec4(13.7f, 6.f, 13.7f, 2.f);
  /// @brief Edges rounding
  float radius;
  /// @brief Border thickness
  float border_thickness = -1.35f;
  /// @brief Border thickness when selected
  float border_selected_thickness = 2.f;

public:
  /// @brief <BR>Default cyan style
  static std::shared_ptr<NodeStyle> cyan() {
    return std::make_shared<NodeStyle>(IM_COL32(71, 142, 173, 255),
                                       ImColor(233, 241, 244, 255), 6.5f);
  }
  /// @brief <BR>Default green style
  static std::shared_ptr<NodeStyle> green() {
    return std::make_shared<NodeStyle>(IM_COL32(90, 191, 93, 255),
                                       ImColor(233, 241, 244, 255), 6.5f);
  }
  /// @brief <BR>Default red style
  static std::shared_ptr<NodeStyle> red() {
    return std::make_shared<NodeStyle>(IM_COL32(191, 90, 90, 255),
                                       ImColor(233, 241, 244, 255), 6.f);
  }
  /// @brief <BR>Default brown style
  static std::shared_ptr<NodeStyle> brown() {
    return std::make_shared<NodeStyle>(IM_COL32(191, 134, 90, 255),
                                       ImColor(233, 241, 244, 255), 6.5f);
  }
};

// -----------------------------------------------------------------------------------------------------------------
// LINK

/**
 * @brief Link between two Pins of two different Nodes
 */
class Link {
public:
  /**
   * @brief <BR>Construct a link
   * @param left Pointer to the output Pin of the Link
   * @param right Pointer to the input Pin of the Link
   * @param inf Pointer to the Handler that contains the Link
   */
  explicit Link(Pin *left, Pin *right, ImNodeFlow *inf)
      : m_left(left), m_right(right), m_inf(inf) {}

  /**
   * @brief <BR>Destruction of a link
   * @details Deletes references of this links form connected pins
   */
  ~Link();

  /**
   * @brief <BR>Looping function to update the Link
   * @details Draws the Link and updates Hovering and Selected status.
   */
  void update();

  /**
   * @brief <BR>Get Left pin of the link
   * @return Pointer to the Pin
   */
  [[nodiscard]] Pin *left() const { return m_left; }

  /**
   * @brief <BR>Get Right pin of the link
   * @return Pointer to the Pin
   */
  [[nodiscard]] Pin *right() const { return m_right; }

  /**
   * @brief <BR>Get hovering status
   * @return [TRUE] If the link is hovered in the current frame
   */
  [[nodiscard]] bool isHovered() const { return m_hovered; }

  /**
   * @brief <BR>Get selected status
   * @return [TRUE] If the link is selected in the current frame
   */
  [[nodiscard]] bool isSelected() const { return m_selected; }

private:
  ImNodeFlow *m_inf;
  Pin *m_left;
  Pin *m_right;
  bool m_hovered = false;
  bool m_selected = false;
};

// -----------------------------------------------------------------------------------------------------------------
// HANDLER

/**
 * @brief Grid's the color parameters
 */
struct InfColors {
  /// @brief Background of the grid
  ImU32 background = IM_COL32(33, 41, 45, 255);
  /// @brief Main lines of the grid
  ImU32 grid = IM_COL32(200, 200, 200, 40);
  /// @brief Secondary lines
  ImU32 subGrid = IM_COL32(200, 200, 200, 10);
};

/**
 * @brief ALl the grid's appearance parameters. Sizes + Colors
 */
struct InfStyler {
  /// @brief Size of main grid
  float grid_size = 50.f;
  /// @brief Sub-grid divisions for Node snapping
  float grid_subdivisions = 5.f;
  /// @brief ImNodeFlow colors
  InfColors colors;
};

/**
 * @brief Main node editor
 * @details Handles the infinite grid, nodes and links. Also handles all the
 * logic.
 */
class ImNodeFlow {
private:
  static int m_instances;

public:
  /**
   * @brief <BR>Instantiate a new editor with default name.
   * <BR> Editor name will be "FlowGrid + the number of editors"
   */
  ImNodeFlow() : ImNodeFlow("FlowGrid" + std::to_string(m_instances)) {}

  /**
   * @brief <BR>Instantiate a new editor with given name
   * @details Creates a new Node Editor with the given name.
   * @param name Name of the editor
   */
  explicit ImNodeFlow(std::string name) : m_name(std::move(name)) {
    m_instances++;
    m_context.config().extra_window_wrapper = true;
    m_context.config().color = m_style.colors.background;
  }

  /**
   * @brief <BR>Handler loop
   * @details Main update function. Refreshes all the logic and draws
   * everything. Must be called every frame.
   */
  void update();

  /**
   * @brief <BR>Add a node to the grid
   * @tparam T Derived class of <BaseNode> to be added
   * @tparam Params types of optional args to forward to derived class ctor
   * @param pos Position of the Node in grid coordinates
   * @param args Optional arguments to be forwarded to derived class ctor
   * @return Shared pointer of the pushed type to the newly added node
   *
   * Inheritance is checked at compile time, \<T> MUST be derived from BaseNode.
   */
  template <typename T, typename... Params>
  std::shared_ptr<T> addNode(const ImVec2 &pos, Params &&...args);

  /**
   * @brief <BR>Add a node to the grid
   * @tparam T Derived class of <BaseNode> to be added
   * @tparam Params types of optional args to forward to derived class ctor
   * @param pos Position of the Node in screen coordinates
   * @param args Optional arguments to be forwarded to derived class ctor
   * @return Shared pointer of the pushed type to the newly added node
   *
   * Inheritance is checked at compile time, \<T> MUST be derived from BaseNode.
   */
  template <typename T, typename... Params>
  std::shared_ptr<T> placeNodeAt(const ImVec2 &pos, Params &&...args);

  /**
   * @brief <BR>Add a node to the grid using mouse position
   * @tparam T Derived class of <BaseNode> to be added
   * @tparam Params types of optional args to forward to derived class ctor
   * @param args Optional arguments to be forwarded to derived class ctor
   * @return Shared pointer of the pushed type to the newly added node
   *
   * Inheritance is checked at compile time, \<T> MUST be derived from BaseNode.
   */
  template <typename T, typename... Params>
  std::shared_ptr<T> placeNode(Params &&...args);

  /**
   * @brief <BR>Add link to the handler internal list
   * @param link Reference to the link
   */
  void addLink(std::shared_ptr<Link> &link);

  /**
   * @brief <BR>Pop-up when link is "dropped"
   * @details Sets the content of a pop-up that can be displayed when dragging a
   * link in the open instead of onto another pin.
   * @details If "key = ImGuiKey_None" the pop-up will always open when a link
   * is dropped.
   * @param content Function or Lambda containing only the contents of the
   * pop-up and the subsequent logic
   * @param key Optional key required in order to open the pop-up
   */
  void droppedLinkPopUpContent(std::function<void(Pin *dragged)> content,
                               ImGuiKey key = ImGuiKey_None) {
    m_droppedLinkPopUp = std::move(content);
    m_droppedLinkPupUpComboKey = key;
  }

  /**
   * @brief <BR>Pop-up when right-clicking
   * @details Sets the content of a pop-up that can be displayed when
   * right-clicking on the grid.
   * @param content Function or Lambda containing only the contents of the
   * pop-up and the subsequent logic
   */
  void rightClickPopUpContent(std::function<void(BaseNode *node)> content) {
    m_rightClickPopUp = std::move(content);
  }

  /**
   * @brief <BR>Get mouse clicking status
   * @return [TRUE] if mouse is clicked and click hasn't been consumed
   */
  [[nodiscard]] bool getSingleUseClick() const { return m_singleUseClick; }

  /**
   * @brief <BR>Consume the click for the given frame
   */
  void consumeSingleUseClick() { m_singleUseClick = false; }

  /**
   * @brief <BR>Get editor's name
   * @return Const reference to editor's name
   */
  const std::string &getName() { return m_name; }

  /**
   * @brief <BR>Get editor's position
   * @return Const reference to editor's position in screen coordinates
   */
  const ImVec2 &getPos() { return m_context.origin(); }

  /**
   * @brief <BR>Get editor's grid scroll
   * @details Scroll is the offset from the origin of the grid, changes while
   * navigating the grid.
   * @return Const reference to editor's grid scroll
   */
  const ImVec2 &getScroll() { return m_context.scroll(); }

  /**
   * @brief <BR>Get editor's list of nodes
   * @return Const reference to editor's internal nodes list
   */
  std::unordered_map<NodeUID, std::shared_ptr<BaseNode>> &getNodes() {
    return m_nodes;
  }

  /**
   * @brief <BR>Get nodes count
   * @return Number of nodes present in the editor
   */
  uint32_t getNodesCount() { return (uint32_t)m_nodes.size(); }

  /**
   * @brief <BR>Get editor's list of links
   * @return Const reference to editor's internal links list
   */
  const std::vector<std::weak_ptr<Link>> &getLinks() { return m_links; }

  /**
   * @brief <BR>Get zooming viewport
   * @return Const reference to editor's internal viewport for zoom support
   */
  ContainedContext &getGrid() { return m_context; }

  /**
   * @brief <BR>Get dragging status
   * @return [TRUE] if a Node is being dragged around the grid
   */
  [[nodiscard]] bool isNodeDragged() const { return m_draggingNode; }

  /**
   * @brief <BR>Get current style
   * @return Reference to style variables
   */
  InfStyler &getStyle() { return m_style; }

  /**
   * @brief <BR>Set editor's size
   * @param size Editor's size. Set to (0, 0) to auto-fit.
   */
  void setSize(const ImVec2 &size) { m_context.config().size = size; }

  /**
   * @brief <BR>Set dragging status
   * @param state New dragging state
   *
   * The new state will only be updated one at the start of each frame.
   */
  void draggingNode(bool state) { m_draggingNodeNext = state; }

  /**
   * @brief <BR>Set what pin is being hovered
   * @param hovering Pointer to the hovered pin
   */
  void hovering(Pin *hovering) { m_hovering = hovering; }

  /**
   * @brief <BR>Set what node is being hovered
   * @param hovering Pointer to the hovered node
   */
  void hoveredNode(BaseNode *hovering) { m_hoveredNode = hovering; }

  /**
   * @brief <BR>Convert coordinates from screen to grid
   * @param p Point in screen coordinates to be converted
   * @return Point in grid's coordinates
   */
  ImVec2 screen2grid(const ImVec2 &p);

  /**
   * @brief <BR>Convert coordinates from grid to screen
   * @param p Point in grid's coordinates to be converted
   * @return Point in screen coordinates
   */
  ImVec2 grid2screen(const ImVec2 &p);

  /**
   * @brief <BR>Check if mouse is on selected node
   * @return [TRUE] if the mouse is hovering a selected node
   */
  bool on_selected_node();

  /**
   * @brief <BR>Check if mouse is on a free point on the grid
   * @return [TRUE] if the mouse is not hovering a node or a link
   */
  bool on_free_space();

private:
  std::string m_name;
  ContainedContext m_context;

  bool m_singleUseClick = false;

  std::unordered_map<NodeUID, std::shared_ptr<BaseNode>> m_nodes;
  std::vector<std::weak_ptr<Link>> m_links;

  std::function<void(Pin *dragged)> m_droppedLinkPopUp;
  ImGuiKey m_droppedLinkPupUpComboKey = ImGuiKey_None;
  Pin *m_droppedLinkLeft = nullptr;
  std::function<void(BaseNode *node)> m_rightClickPopUp;
  BaseNode *m_hoveredNodeAux = nullptr;

  BaseNode *m_hoveredNode = nullptr;
  bool m_draggingNode = false, m_draggingNodeNext = false;
  Pin *m_hovering = nullptr;
  Pin *m_dragOut = nullptr;

  InfStyler m_style;
};

// -----------------------------------------------------------------------------------------------------------------
// BASE NODE

/**
 * @brief Parent class for custom nodes
 * @details Main class from which custom nodes can be created. All interactions
 * with the main grid are handled internally.
 */
class BaseNode {
public:
  BaseNode() = default;

  /**
   * @brief <BR>Main loop of the node
   * @details Updates position, hovering and selected status, and renders the
   * node. Must be called each frame.
   */
  void update();

  /**
   * @brief <BR>Content of the node
   * @details Function to be implemented by derived custom nodes.
   *          Must contain the body of the node. If left empty the node will
   * only have input and output pins.
   */
  virtual void draw() {}

  /**
   * @brief <BR>Add an Input to the node
   * @details Will add an Input pin to the node with the given name and data
   * type. <BR> <BR> In this case the name of the pin will also be its UID. <BR>
   * <BR> The UID must be unique only in the context of the current node's
   * inputs.
   * @tparam T Type of the data the pin will handle
   * @param name Name of the pin
   * @param defReturn Default return value when the pin is not connected
   * @param filter Connection filter
   * @param style Style of the pin
   * @return Shared pointer to the newly added pin
   */
  template <typename T>
  std::shared_ptr<InPin<T>>
  addIN(const std::string &name, T defReturn,
        ConnectionFilter filter = ConnectionFilter_None,
        std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Add an Input to the node
   * @details Will add an Input pin to the node with the given name and data
   * type. <BR> <BR> The UID must be unique only in the context of the current
   * node's inputs.
   * @tparam T Type of the data the pin will handle
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   * @param name Name of the pin
   * @param defReturn Default return value when the pin is not connected
   * @param filter Connection filter
   * @param style Style of the pin
   * @return Shared pointer to the newly added pin
   */
  template <typename T, typename U>
  std::shared_ptr<InPin<T>>
  addIN_uid(const U &uid, const std::string &name, T defReturn,
            ConnectionFilter filter = ConnectionFilter_None,
            std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Remove input pin
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   */
  template <typename U> void dropIN(const U &uid);

  /**
   * @brief <BR>Remove input pin
   * @param uid Unique identifier of the pin
   */
  void dropIN(const char *uid);

  /**
   * @brief <BR>Show a temporary input pin
   * @details Will show an input pin with the given name.
   *          The pin is created the first time showIN is called and kept alive
   * as long as showIN is called each frame. <BR> <BR> In this case the name of
   * the pin will also be its UID. <BR> <BR> The UID must be unique only in the
   * context of the current node's inputs.
   * @tparam T Type of the data the pin will handle
   * @param name Name of the pin
   * @param defReturn Default return value when the pin is not connected
   * @param filter Connection filter
   * @param style Style of the pin
   * @return Const reference to the value of the connected link for the current
   * frame of defReturn
   */
  template <typename T>
  const T &showIN(const std::string &name, T defReturn,
                  ConnectionFilter filter = ConnectionFilter_None,
                  std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Show a temporary input pin
   * @details Will show an input pin with the given name and UID.
   *          The pin is created the first time showIN_uid is called and kept
   * alive as long as showIN_uid is called each frame. <BR> <BR> The UID must be
   * unique only in the context of the current node's inputs.
   * @tparam T Type of the data the pin will handle
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   * @param name Name of the pin
   * @param defReturn Default return value when the pin is not connected
   * @param filter Connection filter
   * @param style Style of the pin
   * @return Const reference to the value of the connected link for the current
   * frame of defReturn
   */
  template <typename T, typename U>
  const T &showIN_uid(const U &uid, const std::string &name, T defReturn,
                      ConnectionFilter filter = ConnectionFilter_None,
                      std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Add an Output to the node
   * @details Must be called in the node constructor. WIll add an Output pin to
   * the node with the given name and data type. <BR> <BR> In this case the name
   * of the pin will also be its UID. <BR> <BR> The UID must be unique only in
   * the context of the current node's outputs.
   * @tparam T Type of the data the pin will handle
   * @param name Name of the pin
   * @param filter Connection filter
   * @param style Style of the pin
   * @return Shared pointer to the newly added pin. Must be used to set the
   * behaviour
   */
  template <typename T>
  [[nodiscard]] std::shared_ptr<OutPin<T>>
  addOUT(const std::string &name,
         ConnectionFilter filter = ConnectionFilter_None,
         std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Add an Output to the node
   * @details Must be called in the node constructor. WIll add an Output pin to
   * the node with the given name and data type. <BR> <BR> The UID must be
   * unique only in the context of the current node's outputs.
   * @tparam T Type of the data the pin will handle
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   * @param name Name of the pin
   * @param filter Connection filter
   * @param style Style of the pin
   * @return Shared pointer to the newly added pin. Must be used to set the
   * behaviour
   */
  template <typename T, typename U>
  [[nodiscard]] std::shared_ptr<OutPin<T>>
  addOUT_uid(const U &uid, const std::string &name,
             ConnectionFilter filter = ConnectionFilter_None,
             std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Remove output pin
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   */
  template <typename U> void dropOUT(const U &uid);

  /**
   * @brief <BR>Remove output pin
   * @param uid Unique identifier of the pin
   */
  void dropOUT(const char *uid);

  /**
   * @brief <BR>Show a temporary output pin
   * @details Will show an output pin with the given name.
   *          The pin is created the first time showOUT is called and kept alive
   * as long as showOUT is called each frame. <BR> <BR> In this case the name of
   * the pin will also be its UID. <BR> <BR> The UID must be unique only in the
   * context of the current node's outputs.
   * @tparam T Type of the data the pin will handle
   * @param name Name of the pin
   * @param behaviour Function or lambda expression used to calculate output
   * value
   * @param filter Connection filter
   * @param style Style of the pin
   */
  template <typename T>
  void showOUT(const std::string &name, std::function<T()> behaviour,
               ConnectionFilter filter = ConnectionFilter_None,
               std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Show a temporary output pin
   * @details Will show an output pin with the given name.
   *          The pin is created the first time showOUT_uid is called and kept
   * alive as long as showOUT_uid is called each frame. <BR> <BR> The UID must
   * be unique only in the context of the current node's outputs.
   * @tparam T Type of the data the pin will handle
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   * @param name Name of the pin
   * @param behaviour Function or lambda expression used to calculate output
   * value
   * @param filter Connection filter
   * @param style Style of the pin
   */
  template <typename T, typename U>
  void showOUT_uid(const U &uid, const std::string &name,
                   std::function<T()> behaviour,
                   ConnectionFilter filter = ConnectionFilter_None,
                   std::shared_ptr<PinStyle> style = nullptr);

  /**
   * @brief <BR>Get Input value from an InPin
   * @details Get a reference to the value of an input pin, the value is stored
   * in the output pin at the other end of the link.
   * @tparam T Data type
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   * @return Const reference to the value
   */
  template <typename T, typename U> const T &getInVal(const U &uid);

  /**
   * @brief <BR>Get Input value from an InPin
   * @details Get a reference to the value of an input pin, the value is stored
   * in the output pin at the other end of the link.
   * @tparam T Data type
   * @param uid Unique identifier of the pin
   * @return Const reference to the value
   */
  template <typename T> const T &getInVal(const char *uid);

  /**
   * @brief <BR>Get generic reference to input pin
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   * @return Generic pointer to the pin
   */
  template <typename U> Pin *inPin(const U &uid);

  /**
   * @brief <BR>Get generic reference to input pin
   * @param uid Unique identifier of the pin
   * @return Generic pointer to the pin
   */
  Pin *inPin(const char *uid);

  /**
   * @brief <BR>Get generic reference to output pin
   * @tparam U Type of the UID
   * @param uid Unique identifier of the pin
   * @return Generic pointer to the pin
   */
  template <typename U> Pin *outPin(const U &uid);

  /**
   * @brief <BR>Get generic reference to output pin
   * @param uid Unique identifier of the pin
   * @return Generic pointer to the pin
   */
  Pin *outPin(const char *uid);

  /**
   * @brief <BR>Get internal input pins list
   * @return Const reference to node's internal list
   */
  const std::vector<std::shared_ptr<Pin>> &getIns() { return m_ins; }

  /**
   * @brief <BR>Get internal output pins list
   * @return Const reference to node's internal list
   */
  const std::vector<std::shared_ptr<Pin>> &getOuts() { return m_outs; }

  /**
   * @brief <BR>Delete itself
   */
  void destroy() { m_destroyed = true; }

  /*
   * @brief <BR>Get if node must be deleted
   */
  [[nodiscard]] bool toDestroy() const { return m_destroyed; }

  /**
   * @brief <BR>Get hovered status
   * @return [TRUE] if the mouse is hovering the node
   */
  bool isHovered();

  /**
   * @brief <BR>Get node's UID
   * @return Node's unique identifier
   */
  [[nodiscard]] NodeUID getUID() const { return m_uid; }

  /**
   * @brief <BR>Get node name
   * @return Const reference to the node's name
   */
  const std::string &getName() { return m_title; }

  /**
   * @brief <BR>Get node size
   * @return Const reference to the node's size
   */
  const ImVec2 &getSize() { return m_size; }

  /**
   * @brief <BR>Get node position
   * @return Const reference to the node's position
   */
  const ImVec2 &getPos() { return m_pos; }

  /**
   * @brief <BR>Get grid handler bound to node
   * @return Pointer to the handler
   */
  ImNodeFlow *getHandler() { return m_inf; }

  /**
   * @brief <BR>Get node's style
   * @return Shared pointer to the node's style
   */
  const std::shared_ptr<NodeStyle> &getStyle() { return m_style; }

  /**
   * @brief <BR>Get selected status
   * @return [TRUE] if the node is selected
   */
  [[nodiscard]] bool isSelected() const { return m_selected; }

  /**
   * @brief <BR>Get dragged status
   * @return [TRUE] if the node is being dragged
   */
  [[nodiscard]] bool isDragged() const { return m_dragged; }

  /**
   * @brief <BR>Set node's uid
   * @param uid Node's unique identifier
   */
  BaseNode *setUID(NodeUID uid) {
    m_uid = uid;
    return this;
  }

  /**
   * @brief <BR>Set node's name
   * @param name New title
   */
  BaseNode *setTitle(const std::string &title) {
    m_title = title;
    return this;
  }

  /**
   * @brief <BR>Set node's position
   * @param pos Position in grid coordinates
   */
  BaseNode *setPos(const ImVec2 &pos) {
    m_pos = pos;
    m_posTarget = pos;
    return this;
  }

  /**
   * @brief <BR>Set ImNodeFlow handler
   * @param inf Grid handler for the node
   */
  BaseNode *setHandler(ImNodeFlow *inf) {
    m_inf = inf;
    return this;
  }

  /**
   * @brief Set node's style
   * @param style New style
   */
  BaseNode *setStyle(std::shared_ptr<NodeStyle> style) {
    m_style = std::move(style);
    return this;
  }

  /**
   * @brief <BR>Set selected status
   * @param state New selected state
   *
   * Status only updates when updatePublicStatus() is called
   */
  BaseNode *selected(bool state) {
    m_selectedNext = state;
    return this;
  }

  /**
   * @brief <BR>Update the isSelected status of the node
   */
  void updatePublicStatus() { m_selected = m_selectedNext; }

private:
  NodeUID m_uid = 0;
  std::string m_title;
  ImVec2 m_pos, m_posTarget;
  ImVec2 m_size;
  ImNodeFlow *m_inf = nullptr;
  std::shared_ptr<NodeStyle> m_style;
  bool m_selected = false, m_selectedNext = false;
  bool m_dragged = false;
  bool m_destroyed = false;

  std::vector<std::shared_ptr<Pin>> m_ins;
  std::vector<std::pair<int, std::shared_ptr<Pin>>> m_dynamicIns;
  std::vector<std::shared_ptr<Pin>> m_outs;
  std::vector<std::pair<int, std::shared_ptr<Pin>>> m_dynamicOuts;
};

// -----------------------------------------------------------------------------------------------------------------
// PINS

/**
 * @brief Pins type identifier
 */
enum PinType { PinType_Input, PinType_Output };

/**
 * @brief Generic base class for pins
 */
class Pin {
public:
  /**
   * @brief <BR>Generic pin constructor
   * @param name Name of the pin
   * @param filter Connection filter
   * @param kind Specifies Input or Output
   * @param parent Pointer to the Node containing the pin
   * @param inf Pointer to the Grid Handler the pin is in (same as parent)
   * @param style Style of the pin
   */
  explicit Pin(PinUID uid, std::string name, ConnectionFilter filter,
               PinType kind, BaseNode *parent, ImNodeFlow **inf,
               std::shared_ptr<PinStyle> style)
      : m_uid(uid), m_name(std::move(name)), m_filter(filter), m_type(kind),
        m_parent(parent), m_inf(inf), m_style(std::move(style)) {
    if (!m_style)
      m_style = PinStyle::cyan();
  }

  /**
   * @brief <BR>Main loop of the pin
   * @details Updates position, hovering and dragging status, and renders the
   * pin. Must be called each frame.
   */
  void update();

  /**
   * @brief <BR>Draw default pin's socket
   */
  void drawSocket();

  /**
   * @brief <BR>Draw default pin's decoration (border, bg, and hover overlay)
   */
  void drawDecoration();

  /**
   * @brief <BR>Used by output pins to calculate their values
   */
  virtual void resolve() {}

  /**
   * @brief <BR>Custom render function to override Pin appearance
   * @param r Function or lambda expression with new ImGui rendering
   */
  Pin *renderer(std::function<void(Pin *p)> r) {
    m_renderer = std::move(r);
    return this;
  }

  /**
   * @brief <BR>Create link between pins
   * @param other Pointer to the other pin
   */
  virtual void createLink(Pin *other) = 0;

  /**
   * @brief <BR>Set the reference to a link
   * @param link Smart pointer to the link
   */
  virtual void setLink(std::shared_ptr<Link> &link) {}

  /**
   * @brief <BR>Delete link reference
   */
  virtual void deleteLink() = 0;

  /**
   * @brief <BR>Get connected status
   * @return [TRUE] if the pin is connected
   */
  virtual bool isConnected() = 0;

  /**
   * @brief <BR>Get pin's link
   * @return Weak_ptr reference to pin's link
   */
  virtual std::weak_ptr<Link> getLink() { return std::weak_ptr<Link>{}; }

  /**
   * @brief <BR>Get pin's UID
   * @return Unique identifier of the pin
   */
  [[nodiscard]] PinUID getUid() const { return m_uid; }

  /**
   * @brief <BR>Get pin's name
   * @return Const reference to pin's name
   */
  const std::string &getName() { return m_name; }

  /**
   * @brief <BR>Get pin's position
   * @return Const reference to pin's position in grid coordinates
   */
  [[nodiscard]] const ImVec2 &getPos() { return m_pos; }

  /**
   * @brief <BR>Get pin's hit-box size
   * @return Const reference to pin's hit-box size
   */
  [[nodiscard]] const ImVec2 &getSize() { return m_size; }

  /**
   * @brief <BR>Get pin's parent node
   * @return Generic type pointer to pin's parent node. (Node that contains it)
   */
  BaseNode *getParent() { return m_parent; }

  /**
   * @brief <BR>Get pin's type
   * @return The pin type. Either Input or Output
   */
  PinType getType() { return m_type; }

  /**
   * @brief <BR>Get pin's connection filter
   * @return Pin's connection filter configuration
   */
  [[nodiscard]] ConnectionFilter getFilter() const { return m_filter; }

  /**
   * @brief <BR>Get pin's style
   * @return Smart pointer to pin's style
   */
  std::shared_ptr<PinStyle> &getStyle() { return m_style; }

  /**
   * @brief <BR>Get pin's link attachment point (socket)
   * @return Grid coordinates to the attachment point between the link and the
   * pin's socket
   */
  virtual ImVec2 pinPoint() = 0;

  /**
   * @brief <BR>Calculate pin's width pre-rendering
   * @return The with of the pin once it will be rendered
   */
  float calcWidth() { return ImGui::CalcTextSize(m_name.c_str()).x; }

  /**
   * @brief <BR>Set pin's position
   * @param pos Position in screen coordinates
   */
  void setPos(ImVec2 pos) { m_pos = pos; }

protected:
  PinUID m_uid;
  std::string m_name;
  ImVec2 m_pos = ImVec2(0.f, 0.f);
  ImVec2 m_size = ImVec2(0.f, 0.f);
  PinType m_type;
  ConnectionFilter m_filter;
  std::shared_ptr<PinStyle> m_style;
  BaseNode *m_parent = nullptr;
  ImNodeFlow **m_inf;
  std::function<void(Pin *p)> m_renderer;
};

/**
 * @brief Input specific pin
 * @details Derived from the generic class Pin. The input pin owns the link
 * pointer.
 * @tparam T Data type handled by the pin
 */
template <class T> class InPin : public Pin {
public:
  /**
   * @brief <BR>Input pin constructor
   * @param name Name of the pin
   * @param filter Connection filter
   * @param parent Pointer to the Node containing the pin
   * @param defReturn Default return value when the pin is not connected
   * @param inf Pointer to the Grid Handler the pin is in (same as parent)
   * @param style Style of the pin
   */
  explicit InPin(PinUID uid, const std::string &name, ConnectionFilter filter,
                 BaseNode *parent, T defReturn, ImNodeFlow **inf,
                 std::shared_ptr<PinStyle> style)
      : Pin(uid, name, filter, PinType_Input, parent, inf, style),
        m_emptyVal(defReturn) {}

  /**
   * @brief <BR>Create link between pins
   * @param other Pointer to the other pin
   */
  void createLink(Pin *other) override;

  /**
   * @brief <BR>Delete the link connected to the pin
   */
  void deleteLink() override { m_link.reset(); }

  /**
   * @brief <BR>Get connected status
   * @return [TRUE] is pin is connected to a link
   */
  bool isConnected() override { return m_link != nullptr; }

  /**
   * @brief <BR>Get pin's link
   * @return Weak_ptr reference to the link connected to the pin
   */
  std::weak_ptr<Link> getLink() override { return m_link; }

  /**
   * @brief <BR>Get pin's link attachment point (socket)
   * @return Grid coordinates to the attachment point between the link and the
   * pin's socket
   */
  ImVec2 pinPoint() override {
    return m_pos + ImVec2(-m_style->extra.socket_padding, m_size.y / 2);
  }

  /**
   * @brief <BR>Get value carried by the connected link
   * @return Reference to the value of the connected OutPin. Or the default
   * value if not connected
   */
  const T &val();

private:
  std::shared_ptr<Link> m_link;
  T m_emptyVal;
};

/**
 * @brief Output specific pin
 * @details Derived from the generic class Pin. The output pin handles the
 * logic.
 * @tparam T Data type handled by the pin
 */
template <class T> class OutPin : public Pin {
public:
  /**
   * @brief <BR>Output pin constructor
   * @param name Name of the pin
   * @param filter Connection filter
   * @param parent Pointer to the Node containing the pin
   * @param inf Pointer to the Grid Handler the pin is in (same as parent)
   * @param style Style of the pin
   */
  explicit OutPin(PinUID uid, const std::string &name, ConnectionFilter filter,
                  BaseNode *parent, ImNodeFlow **inf,
                  std::shared_ptr<PinStyle> style)
      : Pin(uid, name, filter, PinType_Output, parent, inf, style) {}

  /**
   * @brief <BR>When parent gets deleted, remove the links
   */
  ~OutPin() {
    for (auto &l : m_links)
      if (!l.expired())
        l.lock()->right()->deleteLink();
  }

  /**
   * @brief <BR>Calculate output value based on set behaviour
   */
  void resolve() override { m_val = m_behaviour(); }

  /**
   * @brief <BR>Create link between pins
   * @param other Pointer to the other pin
   */
  void createLink(Pin *other) override;

  /**
   * @brief <BR>Add a connected link to the internal list
   * @param link Pointer to the link
   */
  void setLink(std::shared_ptr<Link> &link) override;

  /**
   * @brief <BR>Delete any expired weak pointers to a (now deleted) link
   */
  void deleteLink() override;

  /**
   * @brief <BR>Get connected status
   * @return [TRUE] is pin is connected to one or more links
   */
  bool isConnected() override { return !m_links.empty(); }

  /**
   * @brief <BR>Get pin's link attachment point (socket)
   * @return Grid coordinates to the attachment point between the link and the
   * pin's socket
   */
  ImVec2 pinPoint() override {
    return m_pos +
           ImVec2(m_size.x + m_style->extra.socket_padding, m_size.y / 2);
  }

  /**
   * @brief <BR>Get output value
   * @return Const reference to the internal value of the pin
   */
  const T &val();

  /**
   * @brief <BR>Set logic to calculate output value
   * @details Used to define the pin behaviour. This is what gets the data from
   * the parent's inputs, and applies the needed logic.
   * @param func Function or lambda expression used to calculate output value
   */
  OutPin<T> *behaviour(std::function<T()> func) {
    m_behaviour = std::move(func);
    return this;
  }

private:
  std::vector<std::weak_ptr<Link>> m_links;
  std::function<T()> m_behaviour;
  T m_val;
};
} // namespace ImFlow

#include "./ImNodeFlow.inl"

#endif
