// (c) Facebook, Inc. and its affiliates. Confidential and proprietary.

#include "test_utils.h"

#include <ATen/core/ivalue.h>
#include <gtest/gtest.h>
#include <torch/csrc/jit/ir/irparser.h>
#include <torch/csrc/jit/runtime/graph_executor.h>
#include <torch/csrc/jit/runtime/graph_iterator.h>
#include <torch/csrc/jit/runtime/static/impl.h>
#include <torch/csrc/jit/runtime/static/memory_planner.h>
#include <torch/csrc/jit/runtime/static/passes.h>

#ifndef AT_PER_OPERATOR_HEADERS
#include <ATen/Functions.h>
#else
#include <ATen/ops/allclose.h>
#endif

#include <memory>
#include <unordered_map>

using namespace torch::jit;
using namespace torch;
using c10::IValue;

namespace torch {
namespace jit {
namespace test {

namespace {

class GraphExecutorWrapper {
 public:
  GraphExecutorWrapper() = default;

  explicit GraphExecutorWrapper(const std::shared_ptr<Graph>& graph)
      : graph_exec_(graph, "") {}

  c10::IValue operator()(const std::vector<c10::IValue>& args) {
    Stack stack(args);
    graph_exec_.run(stack);

    if (stack.size() == 1) {
      return stack[0];
    }
    return c10::ivalue::Tuple::create(stack);
  }

 private:
  GraphExecutor graph_exec_;
};

// Test scripts passed to testStaticRuntime can either be IR or JIT.
// The logic for running the script and producing a corresponding StaticModule
// is a bit different for each case. This logic is encapsulated within concrete
// implementations of this class, and testStaticRuntime is only aware of this
// interface.
class StaticRuntimeTestContext {
 public:
  virtual ~StaticRuntimeTestContext() = default;

  virtual IValue getExpected(const std::vector<IValue>& args) = 0;
  virtual StaticModule makeStaticModule(
      const StaticModuleOptions& opt) const = 0;
};

class ModuleStaticRuntimeTestContext : public StaticRuntimeTestContext {
 public:
  explicit ModuleStaticRuntimeTestContext(const std::string& source_jit)
      : module_("module") {
    module_.define(source_jit);
  }

  IValue getExpected(const std::vector<IValue>& args) override {
    return module_.forward(args);
  }

  StaticModule makeStaticModule(const StaticModuleOptions& opt) const override {
    return torch::jit::StaticModule(
        module_, /* is_frozen */ false, opt, /* sample_inputs */ {});
  }

 private:
  Module module_;
};

class GraphStaticRuntimeContext : public StaticRuntimeTestContext {
 public:
  explicit GraphStaticRuntimeContext(const std::string& source_ir) {
    graph_ = std::make_shared<Graph>();
    std::unordered_map<std::string, Value*> vmap;
    parseIR(source_ir, graph_.get(), vmap);

    graph_exec_ = GraphExecutorWrapper(graph_);
  }

  IValue getExpected(const std::vector<IValue>& args) override {
    return graph_exec_(args);
  }

  StaticModule makeStaticModule(const StaticModuleOptions& opt) const override {
    return StaticModule(graph_, opt, /* sample_inputs */ {});
  }

 private:
  std::shared_ptr<Graph> graph_;
  GraphExecutorWrapper graph_exec_;
};

std::unique_ptr<StaticRuntimeTestContext> makeTestContext(
    const std::string& source) {
  try {
    return std::make_unique<ModuleStaticRuntimeTestContext>(source);
    // Could not parse as TorchScript, assume it's IR
  } catch (const std::runtime_error&) {
    return std::make_unique<GraphStaticRuntimeContext>(source);
  }
}

void compareTensorLists(
    const std::vector<IValue>& l, /* expects */
    const std::vector<IValue>& r, /* values */
    const bool use_allclose,
    const bool use_equalnan) {
  EXPECT_TRUE(l.size() == r.size());
  for (int i = 0; i < l.size(); ++i) {
    ASSERT_TRUE(l[i].isTensor());
    ASSERT_TRUE(r[i].isTensor());
    VLOG(2) << "expect " << i << ": \n" << l[i] << std::endl;
    VLOG(2) << "output " << i << ": \n" << r[i] << std::endl;
    if (!l[i].toTensor().defined()) {
      EXPECT_TRUE(!r[i].toTensor().defined());
    } else {
      if (use_allclose) {
        EXPECT_TRUE(at::allclose(
            l[i].toTensor(),
            r[i].toTensor(),
            /*rtol*/ 1e-05,
            /*atol*/ 1e-08,
            use_equalnan));
      } else {
        EXPECT_TRUE(l[i].toTensor().equal(r[i].toTensor()));
      }
    }
  }
}

} // namespace

void compareResults(
    const IValue& expect,
    const IValue& actual,
    const bool use_allclose,
    const bool use_equalnan) {
  if (expect.isTensor()) {
    VLOG(2) << "expect " << expect.toTensor() << std::endl;
    VLOG(2) << "output " << actual.toTensor() << std::endl;
    EXPECT_TRUE(actual.isTensor());
    if (use_allclose) {
      EXPECT_TRUE(at::allclose(
          expect.toTensor(),
          actual.toTensor(),
          /*rtol*/ 1e-05,
          /*atol*/ 1e-08,
          use_equalnan));
    } else {
      EXPECT_TRUE(expect.toTensor().equal(actual.toTensor()));
    }
    return;
  } else if (expect.isTuple()) {
    EXPECT_TRUE(actual.isTuple());
    auto lhs = expect.toTupleRef().elements();
    auto rhs = actual.toTupleRef().elements();
    EXPECT_TRUE(lhs.size() == rhs.size());
    for (size_t i = 0; i < lhs.size(); i++) {
      compareResults(lhs[i], rhs[i]);
    }
  } else if (expect.isList()) {
    EXPECT_TRUE(actual.isList());
    auto lhs = expect.toList();
    auto rhs = actual.toList();
    EXPECT_TRUE(lhs.size() == rhs.size());
    for (size_t i = 0; i < lhs.size(); i++) {
      compareResults(lhs[i], rhs[i]);
    }
  } else if (expect.isGenericDict()) {
    EXPECT_TRUE(actual.isGenericDict());
    auto lhs = expect.toGenericDict();
    auto rhs = actual.toGenericDict();
    EXPECT_TRUE(lhs.size() == rhs.size());
    for (auto& lh : lhs) {
      auto f = rhs.find(lh.key());
      EXPECT_FALSE(f == rhs.end());
      compareResults(lh.value(), f->value());
    }
  } else {
    // fall back to the default comparison impl in IValue
    EXPECT_TRUE(expect == actual);
  }
}

at::Tensor getTensor(const at::IValue& ival) {
  if (ival.isTensor()) {
    return ival.toTensor();
  } else if (ival.isTensorList()) {
    auto tensor_vec = ival.toTensorVector();
    TORCH_CHECK(tensor_vec.size() == 1);
    return tensor_vec[0];
  } else if (ival.isTuple()) {
    auto tuple = ival.toTuple();
    auto ivalue_vec = tuple->elements();
    TORCH_CHECK(ivalue_vec.size() == 1);
    return ivalue_vec[0].toTensor();
  } else {
    CAFFE_THROW("Unknown input IValue");
  }
}

Node* getNodeWithKind(const StaticModule& smodule, const std::string& kind) {
  return smodule.findNodeWithKindForTesting(kind);
}

Node* getNodeWithKind(std::shared_ptr<Graph>& graph, const std::string& kind) {
  const auto symbol = c10::Symbol::fromQualString(kind);
  DepthFirstGraphNodeIterator it(graph);
  for (auto* node = it.next(); node != nullptr; node = it.next()) {
    if (node->kind() == symbol) {
      return node;
    }
  }
  return nullptr;
}

bool hasNodeWithKind(const StaticModule& smodule, const std::string& kind) {
  return getNodeWithKind(smodule, kind) != nullptr;
}

bool hasNodeWithKind(std::shared_ptr<Graph>& graph, const std::string& kind) {
  return getNodeWithKind(graph, kind) != nullptr;
}

std::shared_ptr<Graph> getGraphFromScript(const std::string& jit_script) {
  script::Module module("module");
  module.define(jit_script);

  Method method = module.get_method("forward");
  return module.get_method("forward").graph();
}

std::shared_ptr<Graph> getGraphFromIR(const std::string& ir) {
  auto graph = std::make_shared<Graph>();
  std::unordered_map<std::string, Value*> vmap;
  parseIR(ir, graph.get(), vmap);
  return graph;
}

void compareResultsWithJIT(
    StaticRuntime& runtime,
    const std::shared_ptr<Graph>& graph,
    const std::vector<c10::IValue>& args,
    const bool use_allclose,
    const bool use_equalnan) {
  GraphExecutorWrapper graph_exec(graph);
  auto expected = graph_exec(args);
  auto actual = runtime(args, {});
  runtime.check_for_memory_leak();
  compareResults(expected, actual, use_allclose, use_equalnan);
}

void testStaticRuntime(
    const std::string& source,
    const std::vector<IValue>& args,
    const std::vector<IValue>& args2,
    const bool use_allclose,
    const bool use_equalnan,
    const bool check_resize) {
  auto test_context = makeTestContext(source);

  std::vector<IValue> args_tensors, args_copy;
  for (const auto& ival : args) {
    if (ival.isTensor()) {
      args_tensors.emplace_back(ival);
      const at::Tensor& t = ival.toTensor();
      args_copy.emplace_back(t.clone());
    }
  }

  auto expect = test_context->getExpected(args);

  for (bool enable_out_variant : {true, false}) {
    for (bool manage_output_tensors : {true, false}) {
      for (bool enable_tensorexpr_fusion : {true, false}) {
        if (!enable_out_variant && manage_output_tensors) {
          continue;
        }
        // run static runtime three times
        // 1st run: collect allocation profiles (args)
        // 2nd run: exercise memory planner and resizing with args2
        // 3rd run: run with args again
        StaticModuleOptions opts{
            .enable_out_variant = enable_out_variant,
            .optimize_memory = enable_out_variant,
            .manage_output_tensors = manage_output_tensors,
            .enable_tensorexpr_fusion = enable_tensorexpr_fusion};
        auto smodule = test_context->makeStaticModule(opts);
        StaticRuntime runtime(smodule);
        auto actual = runtime(args, {});
        if (actual.isTensor()) {
          EXPECT_GE(smodule.num_nodes(), 2)
              << "If we only have one node, the output of the op we are testing is "
              << "not being managed by the memory planner! A failure here "
              << "can typically be fixed by clone()ing the output of the test script.";
        }
        runtime.check_for_memory_leak();
        // first run
        VLOG(2) << "enable_out_variant: " << enable_out_variant;
        VLOG(2) << "manage_output_tensors: " << manage_output_tensors;
        VLOG(2) << "enable_tensorexpr_fusion: " << enable_tensorexpr_fusion;
        VLOG(2) << "args: " << args;
        VLOG(2) << "args2: " << args2;
        VLOG(2) << "expect: " << expect;
        VLOG(2) << "actual: " << actual;
        compareResults(expect, actual, use_allclose, use_equalnan);
        VLOG(2) << "first run comparison done";
        if (manage_output_tensors) {
          actual = IValue();
          runtime.deallocateOutputTensors();
          runtime.checkOutputTensorMemoryLeaks();
        }

        if (!args2.empty()) {
          auto* memory_planner = runtime.get_memory_planner();
          size_t managed_bytes =
              memory_planner ? memory_planner->total_managed() : 0;

          // Run static runtime again with inputs of a different shape.
          expect = test_context->getExpected(args2);
          actual = runtime(args2, {});
          runtime.check_for_memory_leak();
          VLOG(2) << "comparing with args2";
          compareResults(expect, actual, use_allclose, use_equalnan);
          VLOG(2) << "second run comparison done";
          if (manage_output_tensors) {
            actual = IValue();
            runtime.deallocateOutputTensors();
            runtime.checkOutputTensorMemoryLeaks();
          }

          size_t new_managed_bytes =
              memory_planner ? memory_planner->total_managed() : 0;
          if (check_resize && new_managed_bytes > 0) {
            EXPECT_GT(new_managed_bytes, managed_bytes);
          }

          // Run static runtime again with an input of the shape observed during
          // the profile run.
          expect = test_context->getExpected(args);
          actual = runtime(args, {});
          runtime.check_for_memory_leak();
          // third run
          VLOG(2) << "comparing third run";
          compareResults(expect, actual, use_allclose, use_equalnan);
          VLOG(2) << "third run comparison done";
          if (manage_output_tensors) {
            actual = IValue();
            runtime.deallocateOutputTensors();
            runtime.checkOutputTensorMemoryLeaks();
          }
        } else {
          // run static runtime again to exercise the memory planner
          // and allocate managed tensors.
          actual = runtime(args, {});
          runtime.check_for_memory_leak();
          VLOG(2) << "comparing second run with same args";
          compareResults(expect, actual, use_allclose, use_equalnan);
          VLOG(2) << "second run comparison done";
          if (manage_output_tensors) {
            actual = IValue();
            runtime.deallocateOutputTensors();
            runtime.checkOutputTensorMemoryLeaks();
          }
          // third run to use the allocated managed tensors.
          actual = runtime(args, {});
          runtime.check_for_memory_leak();
          if (manage_output_tensors) {
            actual = IValue();
            runtime.deallocateOutputTensors();
            runtime.checkOutputTensorMemoryLeaks();
          }
        }
      }
    }
  }

  // make sure inputs were not modified
  VLOG(2) << "Printing out input tensors";
  compareTensorLists(args_tensors, args_copy, use_allclose, use_equalnan);
}

bool hasProcessedNodeWithName(
    torch::jit::StaticModule& smodule,
    const char* name) {
  return smodule.findNodeWithKindForTesting(name) != nullptr;
}

} // namespace test
} // namespace jit
} // namespace torch
