#include "caffe2/core/context.h"
#include "caffe2/core/tensor.h"
#include "caffe2/core/types.h"
#include "caffe2/opt/converter.h"
#include "caffe2/opt/distributed.h"
#include "caffe2/proto/caffe2.pb.h"
#include "caffe2/python/dlpack.h"
#include "caffe2/python/pybind_state_registry.h"
#include "caffe2/utils/proto_utils.h"
#include "nomnigraph/Converters/Dot.h"
#include "nomnigraph/Graph/Algorithms.h"
#include "nomnigraph/Representations/NeuralNet.h"

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

using ListCasterBase = pybind11::detail::list_caster<
    std::vector<nom::repr::NNGraph::NodeRef>,
    nom::repr::NNGraph::NodeRef>;
namespace pybind11 {
namespace detail {
template <>
struct type_caster<std::vector<nom::repr::NNGraph::NodeRef>> : ListCasterBase {
  static handle cast(
      const std::vector<nom::repr::NNGraph::NodeRef>& src,
      return_value_policy,
      handle parent) {
    return ListCasterBase::cast(src, return_value_policy::reference, parent);
  }
  static handle cast(
      const std::vector<nom::repr::NNGraph::NodeRef>* src,
      return_value_policy pol,
      handle parent) {
    return cast(*src, pol, parent);
  }
};
} // namespace detail
} // namespace pybind11

namespace caffe2 {
namespace python {

using namespace nom::repr;

namespace {

std::map<std::string, std::string> NNPrinter(
    typename nom::repr::NNGraph::NodeRef node) {
  std::map<std::string, std::string> labelMap;
  assert(node->data() && "Node doesn't have data, can't render it");
  if (isa<nom::repr::NeuralNetOperator>(node->data())) {
    auto* op = dyn_cast<nom::repr::NeuralNetOperator>(node->data().get());
    labelMap["label"] = op->getName();
    labelMap["shape"] = "box";
  } else if (isa<nom::repr::Data>(node->data())) {
    auto tensor = dyn_cast<nom::repr::NeuralNetData>(node->data().get());
    labelMap["label"] = tensor->getName();
  }
  return labelMap;
};

using Graph = nom::Graph<py::object>;
std::map<std::string, std::string> GraphPrinter(typename Graph::NodeRef node) {
  std::map<std::string, std::string> labelMap;
  assert(node->data() && "Node doesn't have data, can't render it");
  labelMap["label"] = py::str(node->data());
  return labelMap;
};

} // namespace

void addNomnigraphMethods(pybind11::module& m) {
  // Generic Graph methods
  py::class_<Graph> graph(m, "Graph");
  py::class_<nom::Node<py::object>> node(m, "Node");
  py::class_<nom::Edge<py::object>> edge(m, "Edge");
  graph.def(py::init<>())
      .def(
          "__repr__",
          [](Graph* g) {
            return nom::converters::convertToDotString(g, GraphPrinter);
          })
      .def(
          "createEdge",
          [](Graph* g, Graph::NodeRef a, Graph::NodeRef b) {
            return g->createEdge(a, b);
          },
          py::return_value_policy::reference_internal)
      .def(
          "createNode",
          [](Graph* g, py::object obj) {
            return g->createNode(std::move(obj));
          },
          py::return_value_policy::reference_internal);

  // NNModule methods
  m.def("NNModuleFromProtobuf", [](py::bytes def) {
    caffe2::NetDef proto;
    CAFFE_ENFORCE(ParseProtoFromLargeString(def.cast<std::string>(), &proto));
    std::vector<NNGraph::NodeRef> ns;
    auto nn = caffe2::convertToNNModule(proto, false, &ns);
    return std::pair<NNModule, std::vector<NNGraph::NodeRef>>(
        std::move(nn), ns);
  });

  m.def(
      "NNModuleFromProtobufDistributed",
      [](py::bytes def, std::map<std::string, py::bytes> blobToDeviceMap) {
        std::map<std::string, caffe2::DeviceOption> m;
        for (const auto& el : blobToDeviceMap) {
          caffe2::DeviceOption d;
          CAFFE_ENFORCE(
              ParseProtoFromLargeString(el.second.cast<std::string>(), &d));
          m[el.first] = d;
        }

        caffe2::NetDef proto;
        CAFFE_ENFORCE(
            ParseProtoFromLargeString(def.cast<std::string>(), &proto));

        return caffe2::convertToNNModule(proto, m);
      });

  m.def("replaceProducer", &nn::replaceProducer);
  m.def("replaceAllUsesWith", &nn::replaceAllUsesWith);
  m.def("replaceAsConsumer", &nn::replaceAsConsumer);

  py::class_<NNModule> nnmodule(m, "NNModule");
  nnmodule.def(py::init<>())
      .def(
          "dataFlow",
          [](NNModule* nn) -> NNGraph* { return &nn->dataFlow; },
          py::return_value_policy::reference_internal)
      .def(
          "createUniqueDataNode",
          &NNModule::createUniqueDataNode,
          py::return_value_policy::reference_internal)
      .def(
          "convertToCaffe2Proto",
          [](NNModule& nn, py::object def) {
            CAFFE_ENFORCE(
                pybind11::hasattr(def, "SerializeToString"),
                "convertToCaffe2Proto takes either no args",
                "a NetDef");
            auto str = def.attr("SerializeToString")();
            caffe2::NetDef proto;
            proto.ParseFromString(py::bytes(str));
            auto new_proto = caffe2::convertToCaffe2Proto(nn, proto);
            std::string out;
            new_proto.SerializeToString(&out);
            return py::bytes(out);
          })
      .def(
          "getExecutionOrder",
          [](NNModule& nn) {
            nn::coalesceInsertedDataDependencies(&nn);
            std::vector<NNGraph::NodeRef> out;
            auto sccs = nom::algorithm::tarjans(&nn.controlFlow);
            for (const auto& scc : sccs) {
              for (const auto& bb : scc.getNodes()) {
                for (const auto& instr : bb->data().getInstructions()) {
                  out.emplace_back(instr);
                }
              }
            }
            return out;
          },
          py::return_value_policy::reference_internal)
      .def("replaceSubgraph", &NNModule::replaceSubgraph)
      .def("deleteSubgraph", &NNModule::deleteSubgraph);

  auto getTensors = [](NNGraph* g) {
    return nn::nodeIterator<nom::repr::Tensor>(*g);
  };
  auto getOperators = [](NNGraph* g) {
    return nn::nodeIterator<NeuralNetOperator>(*g);
  };
  // NNGraph methods
  py::class_<NNGraph> nngraph(m, "NNGraph");
  nngraph
      .def(
          "__repr__",
          [](NNGraph* g) {
            return nom::converters::convertToDotString(g, NNPrinter);
          })
      .def(
          "createEdge",
          [](NNGraph* g, NNGraph::NodeRef a, NNGraph::NodeRef b) {
            CAFFE_ENFORCE(
                (nn::is<NeuralNetOperator>(a) && nn::is<NeuralNetData>(b)) ||
                    (nn::is<NeuralNetOperator>(b) && nn::is<NeuralNetData>(a)),
                "Edges must exist between NeuralNetOperator and NeuralNetData");
            g->createEdge(a, b);
          })
      .def("deleteEdge", &NNGraph::deleteEdge)
      .def(
          "deleteEdge",
          [](NNGraph* g, NNGraph::NodeRef a, NNGraph::NodeRef b) {
            auto edge = g->getEdgeIfExists(a, b);
            if (edge) {
              g->deleteEdge(edge);
            }
          })
      .def(
          "createNode",
          [](NNGraph* g, GenericOperator& op) {
            return g->createNode(
                nom::util::make_unique<GenericOperator>(op.getName()));
          },
          py::return_value_policy::reference_internal)
      .def(
          "createNode",
          [](NNGraph* g, nom::repr::Tensor& tensor) {
            return g->createNode(
                nom::util::make_unique<nom::repr::Tensor>(tensor.getName()));
          },
          py::return_value_policy::reference_internal)
      .def(
          "createNode",
          [](NNGraph* g, py::object op_def) {
            CAFFE_ENFORCE(
                pybind11::hasattr(op_def, "SerializeToString"),
                "createNode takes either OperatorDef",
                "or ng.NeuralNetOperator");
            auto str = op_def.attr("SerializeToString")();
            OperatorDef op;
            op.ParseFromString(py::bytes(str));
            if (op.input().size() || op.output().size()) {
              LOG(WARNING)
                  << "Input and output specifications are "
                  << "dropped when converting a single operator to nomnigraph. "
                  << "Use ng.NNModule(NetDef&) to preserve these.";
            }
            return g->createNode(convertToNeuralNetOperator(op));
          },
          py::return_value_policy::reference_internal)
      .def("deleteNode", &NNGraph::deleteNode)
      .def(
          "replaceNode",
          [](NNGraph* g, NNGraph::NodeRef old_node, NNGraph::NodeRef new_node) {
            g->replaceNode(old_node, new_node);
          })
      .def(
          "getMutableNodes",
          &NNGraph::getMutableNodes,
          py::return_value_policy::reference_internal)
      .def_property_readonly(
          "nodes",
          &NNGraph::getMutableNodes,
          py::return_value_policy::reference_internal)
      .def_property_readonly(
          "operators",
          getOperators,
          py::return_value_policy::reference_internal)
      .def_property_readonly(
          "tensors", getTensors, py::return_value_policy::reference_internal);

  // Node level methods
  using NodeType = nom::Node<std::unique_ptr<nom::repr::Value>>;
  py::class_<NodeType> noderef(m, "NodeRef");
  auto getName = [](NNGraph::NodeRef n) {
    if (nn::is<nom::repr::Tensor>(n)) {
      return nn::get<nom::repr::Tensor>(n)->getName();
    } else if (nn::is<NeuralNetOperator>(n)) {
      return nn::get<NeuralNetOperator>(n)->getName();
    }
    return std::string("Unknown");
  };
  auto getType = [](NNGraph::NodeRef n) {
    if (nn::is<nom::repr::Tensor>(n)) {
      return "Tensor";
    } else if (nn::is<NeuralNetOperator>(n)) {
      return "Operator";
    }
    return "Unknown";
  };
  auto getOperator = [](NNGraph::NodeRef n) {
    CAFFE_ENFORCE(nn::is<NeuralNetOperator>(n));
    return nn::get<NeuralNetOperator>(n);
  };
  auto getTensor = [](NNGraph::NodeRef n) {
    CAFFE_ENFORCE(nn::is<nom::repr::Tensor>(n));
    return nn::get<nom::repr::Tensor>(n);
  };
  auto getInputs = [](NNGraph::NodeRef n) {
    CAFFE_ENFORCE(nn::is<NeuralNetOperator>(n));
    return nn::getInputs(n);
  };
  auto getOutputs = [](NNGraph::NodeRef n) {
    CAFFE_ENFORCE(nn::is<NeuralNetOperator>(n));
    return nn::getOutputs(n);
  };
  auto getProducer = [](NNGraph::NodeRef n) {
    CAFFE_ENFORCE(nn::is<NeuralNetData>(n));
    return nn::getProducer(n);
  };
  auto getConsumers = [](NNGraph::NodeRef n) {
    CAFFE_ENFORCE(nn::is<NeuralNetData>(n));
    return nn::getConsumers(n);
  };
  auto setAnnotation = [](NNGraph::NodeRef n, Caffe2Annotation& annot) {
    auto* nnOp = nn::get<NeuralNetOperator>(n);
    nnOp->setAnnotation(nom::util::make_unique<Caffe2Annotation>(annot));
  };
  auto getAnnotation = [](NNGraph::NodeRef n) {
    return getOrAddCaffe2Annotation(n);
  };

  noderef
      .def(
          "isOperator",
          [](NNGraph::NodeRef n) { return nn::is<NeuralNetOperator>(n); })
      .def(
          "isTensor",
          [](NNGraph::NodeRef n) { return nn::is<nom::repr::Tensor>(n); })
      .def("getType", getType)
      .def_property_readonly("type", getType)
      .def("getName", getName)
      .def_property_readonly("name", getName)
      .def(
          "getOperator",
          getOperator,
          py::return_value_policy::reference_internal)
      .def("getTensor", getTensor, py::return_value_policy::reference_internal)
      .def_property_readonly(
          "operator", getOperator, py::return_value_policy::reference)
      .def_property_readonly(
          "tensor", getTensor, py::return_value_policy::reference)
      .def("getInputs", getInputs, py::return_value_policy::reference)
      .def("getOutputs", getOutputs, py::return_value_policy::reference)
      .def("hasProducer", [](NNGraph::NodeRef n) { return nn::hasProducer(n); })
      .def("getProducer", getProducer, py::return_value_policy::reference)
      .def("getConsumers", getConsumers, py::return_value_policy::reference)
      .def_property_readonly(
          "inputs", getInputs, py::return_value_policy::reference)
      .def_property_readonly(
          "outputs", getOutputs, py::return_value_policy::reference)
      .def_property_readonly(
          "producer", getProducer, py::return_value_policy::reference)
      .def_property_readonly(
          "consumers", getConsumers, py::return_value_policy::reference)
      .def("getAnnotation", getAnnotation, py::return_value_policy::reference)
      .def("setAnnotation", setAnnotation)
      .def_property(
          "annotation",
          getAnnotation,
          setAnnotation,
          py::return_value_policy::reference)
      .def(
          "getOperatorPredecessors",
          [](NNGraph::NodeRef n) {
            CAFFE_ENFORCE(nn::is<NeuralNetOperator>(n));
            std::vector<NNGraph::NodeRef> pred;
            for (const auto& inEdge : n->getInEdges()) {
              auto data = inEdge->tail();
              if (nn::hasProducer(data)) {
                pred.emplace_back(nn::getProducer(data));
              }
            }
            return pred;
          },
          py::return_value_policy::reference)
      .def(
          "getOperatorSuccessors",
          [](NNGraph::NodeRef n) {
            CAFFE_ENFORCE(nn::is<NeuralNetOperator>(n));
            std::vector<NNGraph::NodeRef> succ;
            for (const auto& outEdge : n->getOutEdges()) {
              auto data = outEdge->head();
              for (const auto& consumer : nn::getConsumers(data)) {
                succ.emplace_back(consumer);
              }
            }
            return succ;
          },
          py::return_value_policy::reference);

  py::class_<NeuralNetOperator, GenericOperator> nnop(m, "NeuralNetOperator");
  py::class_<nom::repr::Tensor> nndata(m, "NeuralNetData");

  nnop.def(py::init<std::string>()).def("getName", &NeuralNetOperator::getName);
  nndata.def(py::init<std::string>()).def("getName", &NeuralNetData::getName);

  // Subgraph matching API
  py::class_<NNSubgraph> nnsubgraph(m, "NNSubgraph");
  nnsubgraph.def(py::init<>())
      .def("__len__", [](NNSubgraph& s) { return s.getNodes().size(); })
      .def(
          "__repr__",
          [](NNSubgraph* g) {
            return nom::converters::convertToDotString<NNGraph>(*g, NNPrinter);
          })
      .def(
          "addNode",
          [](NNSubgraph* sg, NNGraph::NodeRef node) { sg->addNode(node); })
      .def(
          "induceEdges",
          [](NNSubgraph* sg) { nom::algorithm::induceEdges(sg); })
      .def_property_readonly(
          "nodes",
          [](NNSubgraph& s) {
            std::vector<NNGraph::NodeRef> out;
            for (auto n : s.getNodes()) {
              out.emplace_back(n);
            }
            return out;
          },
          py::return_value_policy::reference)
      .def("hasNode", [](NNSubgraph& s, NNGraph::NodeRef n) {
        return s.hasNode(n);
      });

  py::class_<nn::NNMatchGraph> nnMatchGraph(m, "NNMatchGraph");
  nnMatchGraph.def(py::init<>());

  using MatchPredicateType = nom::Node<nn::NNMatchPredicate>;
  py::class_<MatchPredicateType> nnMatchPredicate(m, "MatchPredicateRef");

  nnMatchGraph
      .def(
          "createEdge",
          [](nn::NNMatchGraph* g,
             nn::NNMatchGraph::NodeRef a,
             nn::NNMatchGraph::NodeRef b) { g->createEdge(a, b); })
      .def(
          "createNode",
          [](nn::NNMatchGraph* g, GenericOperator& op, bool strict) {
            auto opName = op.getName();
            auto match = [opName](NNGraph::NodeRef node) {
              NOM_REQUIRE_OR_RET_FALSE(nn::is<NeuralNetOperator>(node));
              auto nnOp = nn::get<NeuralNetOperator>(node);
              return opName == nnOp->getName();
            };
            auto node = nn::NNMatchPredicate(match);
            if (!strict) {
              node.nonTerminal();
            }
            return g->createNode(std::move(node));
          },
          py::return_value_policy::reference_internal,
          py::arg("node"),
          py::arg("strict") = false)
      .def(
          "createNode",
          [](nn::NNMatchGraph* g, nom::repr::Tensor& tensor, bool strict) {
            auto node = nn::NNMatchPredicate(nn::is<nom::repr::Tensor>);
            if (!strict) {
              node.nonTerminal();
            }
            return g->createNode(std::move(node));
          },
          py::return_value_policy::reference_internal,
          py::arg("tensor"),
          py::arg("strict") = false)
      .def(
          "createNode",
          [](nn::NNMatchGraph* g, bool strict) {
            auto match = [](NNGraph::NodeRef node) { return true; };
            auto node = nn::NNMatchPredicate(match);
            if (!strict) {
              node.nonTerminal();
            }
            return g->createNode(std::move(node));
          },
          py::return_value_policy::reference_internal,
          py::arg("strict") = false)
      .def(
          "getMutableNodes",
          [](nn::NNMatchGraph* g) { return g->getMutableNodes(); },
          py::return_value_policy::reference_internal);

  m.def("matchSubgraph", [](NNGraph::NodeRef node, nn::NNMatchGraph* mg) {
    // Get root node or node in root cycle
    auto match_node = *nom::algorithm::tarjans(mg).back().getNodes().begin();
    auto result = mg->isSubgraphMatch(node, match_node, false);
    if (result.isMatch()) {
      return *result.getMatchedSubgraph();
    }
    return NNSubgraph();
  });

  // Annotation API
  py::class_<Caffe2Annotation> annotation(m, "Annotation");
  annotation.def(py::init<>())
      .def("setDevice", &Caffe2Annotation::setDevice)
      .def("getDevice", &Caffe2Annotation::getDevice)
      .def("setDeviceType", &Caffe2Annotation::setDeviceType)
      .def("getDeviceType", &Caffe2Annotation::getDeviceType)
      .def("setKeyNode", &Caffe2Annotation::setKeyNode)
      .def(
          "getKeyNode",
          &Caffe2Annotation::getKeyNode,
          py::return_value_policy::reference)
      .def("setLengthNode", &Caffe2Annotation::setLengthNode)
      .def(
          "getLengthNode",
          &Caffe2Annotation::getLengthNode,
          py::return_value_policy::reference)
      .def("setComponentLevels", &Caffe2Annotation::setComponentLevels)
      .def("getComponentLevels", &Caffe2Annotation::getComponentLevels)
      .def("hasDeviceOption", &Caffe2Annotation::hasDeviceOption)
      .def_property(
          "device_option",
          [](Caffe2Annotation& annot) {
            auto DeviceOption = py::module::import("caffe2.proto.caffe2_pb2")
                                    .attr("DeviceOption");
            auto proto = annot.getDeviceOption();
            std::string serialized_proto;
            proto.SerializeToString(&serialized_proto);
            auto py_device_opt = DeviceOption();
            py_device_opt.attr("ParseFromString")(py::bytes(serialized_proto));
            return py_device_opt;
          },
          [](Caffe2Annotation& annot, py::object& def) {
            CAFFE_ENFORCE(
                pybind11::hasattr(def, "SerializeToString"),
                "device_option can only be set to a DeviceOption");
            auto str = def.attr("SerializeToString")();
            caffe2::DeviceOption proto;
            proto.ParseFromString(py::bytes(str));
            annot.setDeviceOption(proto);
          },
          py::return_value_policy::reference)
      .def_property(
          "operator_def",
          [](Caffe2Annotation& annot) {
            auto opDef = py::module::import("caffe2.proto.caffe2_pb2")
                                    .attr("OperatorDef");
            auto proto = annot.getOperatorDef();
            std::string serialized_proto;
            proto.SerializeToString(&serialized_proto);
            auto py_op_def= opDef();
            py_op_def.attr("ParseFromString")(py::bytes(serialized_proto));
            return py_op_def;
          },
          [](Caffe2Annotation& annot, py::object& def) {
            CAFFE_ENFORCE(
                pybind11::hasattr(def, "SerializeToString"),
                "operator_def can only be set to an OperatorDef");
            auto str = def.attr("SerializeToString")();
            caffe2::OperatorDef proto;
            proto.ParseFromString(py::bytes(str));
            annot.setOperatorDef(proto);
          },
          py::return_value_policy::reference);
}

REGISTER_PYBIND_ADDITION(addNomnigraphMethods);

} // namespace python
} // namespace caffe2
