#ifndef __PY_GRAPH__
#define __PY_GRAPH__

//<code(py_graph)>
class py_graph_t : public py_customidamemo_t
{
  typedef py_customidamemo_t inherited;

protected:
  void collect_class_callbacks_ids(pycim_callbacks_ids_t *out) override;

private:
  enum
  {
    GRCODE_HAVE_HINT             = 0x00010000,
    GRCODE_HAVE_EDGE_HINT        = 0x00020000,
    GRCODE_HAVE_CLICKED          = 0x00040000,
    GRCODE_HAVE_DBL_CLICKED      = 0x00080000,
    GRCODE_HAVE_GOTFOCUS         = 0x00100000,
    GRCODE_HAVE_LOSTFOCUS        = 0x00200000,
    GRCODE_HAVE_CREATING_GROUP   = 0x00400000,
    GRCODE_HAVE_DELETING_GROUP   = 0x00800000,
    GRCODE_HAVE_GROUP_VISIBILITY = 0x01000000,
  };
  struct nodetext_cache_t
  {
    qstring text;
    bgcolor_t bgcolor;
    nodetext_cache_t(const nodetext_cache_t &rhs): text(rhs.text), bgcolor(rhs.bgcolor) {}
    nodetext_cache_t(const char *t, bgcolor_t c): text(t), bgcolor(c) {}
    nodetext_cache_t() {}
  };

  class nodetext_cache_map_t: public std::map<int, nodetext_cache_t>
  {
  public:
    nodetext_cache_t *get(int node_id)
    {
      iterator it = find(node_id);
      if ( it == end() )
        return nullptr;
      return &it->second;
    }
    nodetext_cache_t *add(const int node_id, const char *text, bgcolor_t bgcolor = DEFCOLOR)
    {
      return &(insert(std::make_pair(node_id, nodetext_cache_t(text, bgcolor))).first->second);
    }
  };

  bool refresh_needed;
  nodetext_cache_map_t node_cache;

  // instance callback
  ssize_t gr_callback(int code, va_list va);

  // static callback
  static ssize_t idaapi s_callback(void *obj, int code, va_list va)
  {
    // don't perform sanity check for 'grcode_destroyed', since if we called
    // Close() on this object, it'll have been marked for later deletion in the
    // UI, and thus when we end up here, the view has already been destroyed.
    bool found = get_plugin_instance()->pycim_lookup_info.find_by_py_view(nullptr, (py_graph_t *) obj);
    QASSERT(30453, found || code == grcode_destroyed);
    if ( found )
    {
      PYW_GIL_GET;
      return ((py_graph_t *)obj)->gr_callback(code, va);
    }
    else
    {
      return 0;
    }
  }

  // Refresh user-defined graph node number and edges
  // It calls Python method and expects that the user already filled
  // the nodes and edges. The nodes and edges are retrieved and passed to IDA
  void on_user_refresh(interactive_graph_t *g);

  // Retrieves the text for user-defined graph node
  // It expects either a string or a tuple (string, bgcolor)
  bool on_user_text(interactive_graph_t * /*g*/, int node, const char **str, bgcolor_t *bg_color);

  // Retrieves the hint for the user-defined graph
  // Calls Python and expects a string or None
  int on_hint(char **hint, int node);
  int on_edge_hint(char **hint, int src, int dest);
  int _on_hint_epilog(char **hint, ref_t result);

  // graph is being destroyed
  void on_graph_destroyed(interactive_graph_t * /*g*/ = nullptr)
  {
    refresh_needed = true;
    node_cache.clear();
  }

  // graph is being clicked
  int on_clicked(
        graph_viewer_t * /*view*/,
        selection_item_t * /*item1*/,
        graph_item_t *item2)
  {
    // in:  graph_viewer_t *view
    //      selection_item_t *current_item1
    //      graph_item_t *current_item2
    // out: 0-ok, 1-ignore click
    // this callback allows you to ignore some clicks.
    // it occurs too early, internal graph variables are not updated yet
    // current_item1, current_item2 point to the same thing
    // item2 has more information.
    // see also: kernwin.hpp, custom_viewer_click_t
    if ( item2->n == -1 )
      return 1;

    PYW_GIL_CHECK_LOCKED_SCOPE();
    newref_t result(
            PyObject_CallMethod(
                    self.o,
                    (char *)S_ON_CLICK,
                    "i",
                    item2->n));
    PyW_ShowCbErr(S_ON_CLICK);
    return !result || !PyObject_IsTrue(result.o);
  }

  // a graph node has been double clicked
  int on_dblclicked(graph_viewer_t * /*view*/, selection_item_t *item)
  {
    // in:  graph_viewer_t *view
    //      selection_item_t *current_item
    // out: 0-ok, 1-ignore click
    //graph_viewer_t *v   = va_arg(va, graph_viewer_t *);
    //selection_item_t *s = va_arg(va, selection_item_t *);
    PYW_GIL_CHECK_LOCKED_SCOPE();
    newref_t result(
            PyObject_CallMethod(
                    self.o,
                    (char *)S_ON_DBL_CLICK,
                    "i",
                    item->node));
    PyW_ShowCbErr(S_ON_DBL_CLICK);
    return !result || !PyObject_IsTrue(result.o);
  }

  // a graph viewer got focus
  void on_gotfocus(graph_viewer_t * /*view*/)
  {
    if ( !self )
      return;

    PYW_GIL_CHECK_LOCKED_SCOPE();
    newref_t result(
            PyObject_CallMethod(
                    self.o,
                    (char *)S_ON_ACTIVATE,
                    nullptr));
    PyW_ShowCbErr(S_ON_ACTIVATE);
  }

  // a graph viewer lost focus
  void on_lostfocus(graph_viewer_t * /*view*/)
  {
    if ( !self )
      return;

    PYW_GIL_CHECK_LOCKED_SCOPE();
    newref_t result(
            PyObject_CallMethod(
                    self.o,
                    (char *)S_ON_DEACTIVATE,
                    nullptr));
    PyW_ShowCbErr(S_ON_DEACTIVATE);
  }

  // a group is being created
  int on_creating_group(interactive_graph_t * /*my_g*/, intvec_t *my_nodes)
  {
    PYW_GIL_CHECK_LOCKED_SCOPE();
    newref_t py_nodes(PyList_New(my_nodes->size()));
    int i;
    intvec_t::const_iterator p;
    for ( i = 0, p=my_nodes->begin(); p != my_nodes->end(); ++p, ++i )
      PyList_SetItem(py_nodes.o, i, PyInt_FromLong(*p));
    newref_t py_result(
            PyObject_CallMethod(
                    self.o,
                    (char *)S_ON_CREATING_GROUP,
                    "O",
                    py_nodes.o));
    PyW_ShowCbErr(S_ON_CREATING_GROUP);
    return (!py_result || !PyLong_Check(py_result.o))
         ? 1
         : PyLong_AsLong(py_result.o);
  }

  // a group is being deleted
  int on_deleting_group(interactive_graph_t * /*g*/, int /*old_group*/)
  {
    PYW_GIL_CHECK_LOCKED_SCOPE();
    // TODO
    return 0;
  }

  // a group is being collapsed/uncollapsed
  int on_group_visibility(interactive_graph_t * /*g*/, int /*group*/, bool /*expand*/)
  {
    PYW_GIL_CHECK_LOCKED_SCOPE();
    // TODO
    return 0;
  }


  void show()
  {
    TWidget *view;
    if ( get_plugin_instance()->pycim_lookup_info.find_by_py_view(&view, this) )
      display_widget(view, WOPN_DP_TAB);
  }

  void jump_to_node(int nid)
  {
    ref_t nodes(PyW_TryGetAttrString(self.o, S_M_NODES));
    if ( nid >= PyList_Size(nodes.o) )
      return;

    viewer_center_on(view, nid);
    int x, y;

    // will return a place only when a node was previously selected
    place_t *old_pl = get_custom_viewer_place(view, false, &x, &y);
    if ( old_pl != nullptr )
    {
      user_graph_place_t *new_pl = (user_graph_place_t *) old_pl->clone();
      new_pl->node = nid;
      jumpto(view, new_pl, x, y);
      delete new_pl;
    }
  }

  virtual void refresh() override
  {
    refresh_needed = true;
    inherited::refresh();
  }

  int initialize(PyObject *self, const char *title)
  {
    PYW_GIL_CHECK_LOCKED_SCOPE();

    if ( !collect_pyobject_callbacks(self) )
      return -1;

    TWidget *widget = find_widget(title);
    if ( widget == nullptr ) // create new widget
    {
      lookup_entry_t &e = get_plugin_instance()->pycim_lookup_info.new_entry(this);
      // get a unique graph id
      netnode id;
      char grnode[MAXSTR];
      qsnprintf(grnode, sizeof(grnode), "$ pygraph %s", title);
      id.create(grnode);
      // pre-bind 'self', so that 'on_user_refresh()' can complete
      this->self = borref_t(self);
      graph_viewer_t *pview = create_graph_viewer(title, id, s_callback, this, 0);
      this->self = ref_t();
      display_widget(pview, WOPN_DP_TAB);
      newref_t ret(PyObject_CallMethod(self, "hook", nullptr));
      if ( pview != nullptr )
        viewer_fit_window(pview);
      bind(self, pview);
      get_plugin_instance()->pycim_lookup_info.commit(e, view);
    }
    else
    {
      show();
    }

    viewer_fit_window(view);
    return 0;
  }

public:
  py_graph_t()
  {
    // form = nullptr;
    refresh_needed = true;
  }

  static void SelectNode(PyObject *self, int nid)
  {
    if ( nid < 0 )
      return;

    py_graph_t *_this = (py_graph_t *) view_extract_this(self);
    if ( _this == nullptr || !get_plugin_instance()->pycim_lookup_info.find_by_py_view(nullptr, _this) )
      return;

    _this->jump_to_node(nid);
  }

  static py_graph_t *Close(PyObject *self)
  {
    TWidget *view;
    py_graph_t *_this = (py_graph_t *) view_extract_this(self);
    if ( _this == nullptr || !get_plugin_instance()->pycim_lookup_info.find_by_py_view(&view, _this) )
      return nullptr;
    newref_t ret(PyObject_CallMethod(self, "unhook", nullptr));
    close_widget(view, WCLS_CLOSE_LATER);
    return _this;
  }

  static py_graph_t *Show(PyObject *self)
  {
    PYW_GIL_CHECK_LOCKED_SCOPE();

    py_graph_t *py_graph = (py_graph_t *) view_extract_this(self);

    // New instance?
    if ( py_graph == nullptr )
    {
      qstring title;
      if ( !PyW_GetStringAttr(self, S_M_TITLE, &title) )
        return nullptr;

      // Form already created? try to get associated py_graph instance
      // so that we reuse it
      TWidget *view = find_widget(title.c_str());
      if ( view != nullptr )
        get_plugin_instance()->pycim_lookup_info.find_by_view((py_customidamemo_t**) &py_graph, view);

      if ( py_graph == nullptr )
      {
        py_graph = new py_graph_t();
      }
      else
      {
        // unbind so we are rebound
        py_graph->unbind();
        py_graph->bind(self, view);
        py_graph->refresh_needed = true;
        refresh_viewer(view); // cfr graph.hpp how to refresh an existing interactive_graph_t
      }
      if ( py_graph->initialize(self, title.c_str()) < 0 )
      {
        delete py_graph;
        py_graph = nullptr;
      }
    }
    else
    {
      py_graph->show();
    }
    return py_graph;
  }
};

//-------------------------------------------------------------------------
void py_graph_t::collect_class_callbacks_ids(pycim_callbacks_ids_t *out)
{
  inherited::collect_class_callbacks_ids(out);
  out->add(S_ON_REFRESH, 0);
  out->add(S_ON_GETTEXT, 0);
  out->add(S_M_EDGES, -1);
  out->add(S_M_NODES, -1);
  out->add(S_ON_HINT, GRCODE_HAVE_HINT);
  out->add(S_ON_EDGE_HINT, GRCODE_HAVE_EDGE_HINT);
  out->add(S_ON_CLICK, GRCODE_HAVE_CLICKED);
  out->add(S_ON_DBL_CLICK, GRCODE_HAVE_DBL_CLICKED);
  out->add(S_ON_ACTIVATE, GRCODE_HAVE_GOTFOCUS);
  out->add(S_ON_DEACTIVATE, GRCODE_HAVE_LOSTFOCUS);
  out->add(S_ON_CREATING_GROUP, GRCODE_HAVE_CREATING_GROUP);
  out->add(S_ON_DELETING_GROUP, GRCODE_HAVE_DELETING_GROUP);
  out->add(S_ON_GROUP_VISIBILITY, GRCODE_HAVE_GROUP_VISIBILITY);
}

//-------------------------------------------------------------------------
void py_graph_t::on_user_refresh(interactive_graph_t *g)
{
  if ( !refresh_needed )
    return;

  // Check return value to OnRefresh() call
  PYW_GIL_CHECK_LOCKED_SCOPE();
  newref_t ret(PyObject_CallMethod(self.o, (char *)S_ON_REFRESH, nullptr));
  PyW_ShowCbErr(S_ON_REFRESH);
  if ( ret != nullptr && PyObject_IsTrue(ret.o) )
  {
    // Refer to the nodes
    ref_t nodes(PyW_TryGetAttrString(self.o, S_M_NODES));
    if ( ret != nullptr && PyList_Check(nodes.o) )
    {
      // Refer to the edges
      ref_t edges(PyW_TryGetAttrString(self.o, S_M_EDGES));
      if ( ret != nullptr && PyList_Check(edges.o) )
      {
        // Resize the nodes
        int max_nodes = abs(int(PyList_Size(nodes.o)));
        g->clear();
        g->resize(max_nodes);

        // Mark that we refreshed already
        refresh_needed = false;

        // Clear cached nodes
        node_cache.clear();

        // Get the edges
        for ( int i=(int)PyList_Size(edges.o)-1; i >= 0; i-- )
        {
          // Each list item is a sequence (id1, id2)
          borref_t item(PyList_GetItem(edges.o, i));
          if ( !PySequence_Check(item.o) )
            continue;

          // Get and validate each of the two elements in the sequence
          int edge_ids[2];
          int j;
          for ( j=0; j < qnumber(edge_ids); j++ )
          {
            newref_t id(PySequence_GetItem(item.o, j));
            if ( !id || !PyLong_Check(id.o) )
              break;
            int v = int(PyLong_AsLong(id.o));
            if ( v > max_nodes )
              break;
            edge_ids[j] = v;
          }

          // Incomplete?
          if ( j != qnumber(edge_ids) )
            break;

          // Add the edge
          g->add_edge(edge_ids[0], edge_ids[1], nullptr);
        }
      }
    }
  }
}

//-------------------------------------------------------------------------
bool py_graph_t::on_user_text(interactive_graph_t * /*g*/, int node, const char **str, bgcolor_t *bg_color)
{
  // If already cached then return the value
  nodetext_cache_t *c = node_cache.get(node);
  if ( c != nullptr )
  {
    *str = c->text.c_str();
    if ( bg_color != nullptr )
      *bg_color = c->bgcolor;
    return true;
  }

  // Not cached, call Python
  PYW_GIL_CHECK_LOCKED_SCOPE();
  newref_t result(PyObject_CallMethod(self.o, (char *)S_ON_GETTEXT, "i", node));
  PyW_ShowCbErr(S_ON_GETTEXT);
  if ( !result )
    return false;

  bgcolor_t cl = bg_color == nullptr ? DEFCOLOR : *bg_color;
  qstring buf;

  // User returned a string?
  if ( PyUnicode_Check(result.o) )
  {
    PyUnicode_as_qstring(&buf, result.o);
    c = node_cache.add(node, buf.c_str(), cl);
  }
  // User returned a sequence of text and bgcolor
  else if ( PySequence_Check(result.o)
         && PySequence_Size(result.o) == 2 )
  {
    newref_t py_str(PySequence_GetItem(result.o, 0));
    newref_t py_color(PySequence_GetItem(result.o, 1));

    if ( py_str && PyUnicode_Check(py_str.o) )
      PyUnicode_as_qstring(&buf, py_str.o);
    if ( py_color && PyNumber_Check(py_color.o) )
      cl = bgcolor_t(PyLong_AsUnsignedLong(py_color.o));

    c = node_cache.add(node, buf.c_str(), cl);
  }

  *str = c->text.c_str();
  if ( bg_color != nullptr )
    *bg_color = c->bgcolor;

  return true;
}

//-------------------------------------------------------------------------
int py_graph_t::on_hint(char **hint, int node)
{
  PYW_GIL_CHECK_LOCKED_SCOPE();
  newref_t result(PyObject_CallMethod(self.o, (char *)S_ON_HINT, "i", node));
  PyW_ShowCbErr(S_ON_HINT);
  return _on_hint_epilog(hint, result);
}

//-------------------------------------------------------------------------
int py_graph_t::on_edge_hint(char **hint, int src, int dest)
{
  PYW_GIL_CHECK_LOCKED_SCOPE();
  newref_t result(PyObject_CallMethod(self.o, (char *)S_ON_EDGE_HINT, "ii", src, dest));
  PyW_ShowCbErr(S_ON_EDGE_HINT);
  return _on_hint_epilog(hint, result);
}

//-------------------------------------------------------------------------
int py_graph_t::_on_hint_epilog(char **hint, ref_t result)
{
  // 'hint' must be allocated by qalloc() or qstrdup()
  // out: 0-use default hint, 1-use proposed hint
  bool ok = result != nullptr && PyUnicode_Check(result.o);
  if ( ok )
  {
    qstring buf;
    PyUnicode_as_qstring(&buf, result.o);
    *hint = buf.extract();
  }
  return ok;
}

//-------------------------------------------------------------------------
ssize_t py_graph_t::gr_callback(int code, va_list va)
{
  int ret;
  switch ( code )
  {
    //
    case grcode_user_text:
      {
        interactive_graph_t *g  = va_arg(va, interactive_graph_t *);
        int node                = va_arg(va, int);
        const char **result     = va_arg(va, const char **);
        bgcolor_t *bgcolor      = va_arg(va, bgcolor_t *);
        ret = on_user_text(g, node, result, bgcolor);
        break;
      }
      //
    case grcode_destroyed:
      on_graph_destroyed(va_arg(va, interactive_graph_t *));
      ret = 0;
      break;

      //
    case grcode_clicked:
      if ( has_callback(GRCODE_HAVE_CLICKED) )
      {
        graph_viewer_t *view   = va_arg(va, graph_viewer_t *);
        selection_item_t *item = va_arg(va, selection_item_t *);
        graph_item_t *gitem    = va_arg(va, graph_item_t *);
        ret = on_clicked(view, item, gitem);
      }
      else
      {
        // let the click go through
        ret = 0;
      }
      break;
      //
    case grcode_dblclicked:
      {
        bool handled = has_callback(GRCODE_HAVE_DBL_CLICKED);
        if ( handled )
        {
          graph_viewer_t *view = va_arg(va, graph_viewer_t *);
          selection_item_t *item = va_arg(va, selection_item_t *);
          handled = item != nullptr && item->is_node;
          if ( handled )
            ret = on_dblclicked(view, item);
        }
        if ( !handled )
          ret = 0; // We don't want to ignore the double click, but rather
                   // fallback to the default behavior (e.g., double-clicking
                   // on an edge will to jump to the node on the other side
                   // of that edge.)
      }
      break;
      //
    case grcode_gotfocus:
      if ( has_callback(GRCODE_HAVE_GOTFOCUS) )
        on_gotfocus(va_arg(va, graph_viewer_t *));

      ret = 0;
      break;
      //
    case grcode_lostfocus:
      if ( has_callback(GRCODE_HAVE_LOSTFOCUS) )
        on_lostfocus(va_arg(va, graph_viewer_t *));

      ret = 0;
      break;
      //
    case grcode_user_refresh:
      on_user_refresh(va_arg(va, interactive_graph_t *));

      ret = 1;
      break;
      //
    case grcode_user_hint:
      {
        /*interactive_graph_t *g =*/ va_arg(va, interactive_graph_t *);
        int node = va_arg(va, int);
        int src = va_arg(va, int);
        int dest = va_arg(va, int);
        char **hint = va_arg(va, char **);
        if ( node == -1 && has_callback(GRCODE_HAVE_EDGE_HINT) )
          ret = on_edge_hint(hint, src, dest);
        else if ( node >= 0 && has_callback(GRCODE_HAVE_HINT) )
          ret = on_hint(hint, node);
        else
          ret = 0;
      }
      break;
      //
    case grcode_creating_group:      // a group is being created
      if ( has_callback(GRCODE_HAVE_CREATING_GROUP) )
      {
        interactive_graph_t *g = va_arg(va, interactive_graph_t*);
        intvec_t *nodes = va_arg(va, intvec_t*);
        ret = on_creating_group(g, nodes);
      }
      else
      {
        ret = 0; // Ok to create
      }
      break;
      //
    case grcode_deleting_group:      // a group is being deleted
      if ( has_callback(GRCODE_HAVE_DELETING_GROUP) )
      {
        interactive_graph_t *g = va_arg(va, interactive_graph_t*);
        int old_group = va_arg(va, int);
        ret = on_deleting_group(g, old_group);
      }
      else
      {
        ret = 0; // Ok to delete
      }
      break;
      //
    case grcode_group_visibility:    // a group is being collapsed/uncollapsed
      if ( has_callback(GRCODE_HAVE_GROUP_VISIBILITY) )
      {
        interactive_graph_t *g = va_arg(va, interactive_graph_t*);
        int group = va_arg(va, int);
        bool expand = bool(va_arg(va, int));
        ret = on_group_visibility(g, group, expand);
      }
      else
      {
        ret = 0; // Ok.
      }
      break;
      //
    default:
      ret = 0;
      break;
  }
  // grcode_changed_graph,       // new graph has been set
  // grcode_user_size,           // calculate node size for user-defined graph
  // grcode_user_title,          // render node title of a user-defined graph
  // grcode_user_draw,           // render node of a user-defined graph
  return ret;
}

//-------------------------------------------------------------------------
bool pyg_show(PyObject *self)
{
  return py_graph_t::Show(self) != nullptr;
}

void pyg_close(PyObject *self)
{
  py_graph_t *pyg = py_graph_t::Close(self);
  if ( pyg != nullptr )
    delete pyg;
}

void pyg_select_node(PyObject *self, int nid)
{
  py_graph_t::SelectNode(self, nid);
}
//</code(py_graph)>

//--------------------------------------------------------------------------

//<inline(py_graph)>
void pyg_close(PyObject *self);
void pyg_select_node(PyObject *self, int nid);
bool pyg_show(PyObject *self);
//</inline(py_graph)>
#endif
