/**
 * Some part of the source code is from the Rodinia benchmark suite.
 * Link quoted in the README of the original implementation: http://weather.unisys.com/hurricane/
 */

#include <boost/math/special_functions/next.hpp>
#include <boost/program_options.hpp>
#include <iostream>
#include <tuple>

#include "../device_hopper/core.h"

#include <omp.h>
#include <sys/mman.h>

#define RODINIA_NN_EPSILON 10e-05
#define REPOSITORY_PATH std::string(std::getenv("PLASTICITY_ROOT"))
#define PREFERRED_DEVICE CPU

using namespace device_hopper;

typedef struct latLong
{
    float lat;
    float lng;
} LatLong;

void gen_data(struct latLong *locations, int numRecods) {
    for (size_t record_i = 0; record_i < numRecods; ++record_i) {
        locations[record_i].lat = ((float) (7 + rand() % 63)) + ((float) rand() / (float) 0x7fffffff);
        locations[record_i].lng = ((float) (rand() % 358)) + ((float) rand() / (float) 0x7fffffff);
    }
}

bool verifyResults(
        float* ocl_recordDistances,
        int numRecords,
        struct latLong *locations,
        float latitude,
        float longitude) {

    float *ref_distances = (float *)malloc(sizeof(float) * numRecords);

    // calculate distances on CPU
    omp_set_num_threads(4);
#pragma omp parallel for
    for (int i = 0; i < numRecords; i++) {
        const struct latLong location = locations[i];
        ref_distances[i] = (float) sqrt((latitude  - location.lat) * (latitude  - location.lat) +
                                        (longitude - location.lng) * (longitude - location.lng));
    }

    // compare to ocl result
    bool results_are_correct = true;
    float largest_difference = 0;
    float current_difference = 0;
    for (int i = 0; i < numRecords; i++) {
        current_difference = abs(ref_distances[i] - ocl_recordDistances[i]);
        if (current_difference > largest_difference) largest_difference = current_difference;
        if (current_difference > RODINIA_NN_EPSILON) {
            if (results_are_correct) {
                    std::cout << "Distance mismatch at index " << i << " by: " <<
                              abs(ref_distances[i] - ocl_recordDistances[i]) << "\n";
                    std::cout << "OCL: " << std::setprecision(10) << ocl_recordDistances[i] << "\n";
                    std::cout << "Ref: " << std::setprecision(10) << ref_distances[i] << "\n";
                    //std::cout << "Record: " << records[i].recString << std::endl;
                    results_are_correct = false;
            }
        }
    }
    std::cout << "Largest epsilon: " << largest_difference << std::endl;

    free(ref_distances);
    return results_are_correct;
}

DEVICE_HOPPER_MAIN(int argc, char* argv[]) {
    DEVICE_HOPPER_SETUP

    // Parse CLI parameters
    boost::program_options::options_description desc("Options");
    desc.add_options() ("problem-size", boost::program_options::value<long>(), "Sample count");
    boost::program_options::variables_map vm;
    boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm);
    size_t problem_size = 0;
    if (vm.count("problem-size") == 0) {
        std::cerr << "Error: Problem size is missing." << std::endl;
        std::exit(EXIT_FAILURE);
    } else {
        // This parameter is passed to the application by the Python scripts that automate the experiments.
        problem_size = vm["problem-size"].as<long>();
    }

    // TODO The data paths are currently all hard coded
    // Choose the problem size
    std::string data_dir;
    std::string data_file;
    int numRecords = 0;
    if (problem_size == 1) {
        data_dir = "small";
        data_file = "list81920k_4.txt";
        numRecords = 81920 * 1000;
    } else if (problem_size == 2) {
        data_dir = "medium";
        data_file = "list148480k_8.txt";
        numRecords = 148480 * 1000;
    } else if (problem_size == 3) {
        data_dir = "large";
        data_file = "list368640k_8.txt";
        numRecords = 368640 * 1000;
    } else {
        std::cerr << "Error: Unknown problem size" << std::endl;
        std::exit(EXIT_FAILURE);
    }
    // TODO this is currently not used
    std::string benchmark_data_root = REPOSITORY_PATH + "/benchmarks/input_data/rodinia/nn/" + data_dir;

    // Allocate input buffer
    struct latLong *locations = (struct latLong *) device_hopper::malloc(numRecords, sizeof(struct latLong));

    // Load input data
    gen_data(locations, numRecords);

    // Allocate output buffer
    float *recordDistances = (float *) device_hopper::malloc(numRecords, sizeof(float));

    // Default kernel parameters
    float latitude=30.0;
    float longitude=90.0;

    // Create parallel for
    parallel_for pf(0, numRecords, [=]DEVICE_HOPPER_LAMBDA() {
        int i = GET_ITERATION();
        struct latLong *latLong = locations+i;
        if (i < numRecords) { // Not necessary with our programming model
            float *dist = recordDistances + i;
            *dist = (float) sqrt((latitude-latLong->lat)  * (latitude-latLong->lat) +
                                 (longitude-latLong->lng) * (longitude-latLong->lng));
        }
    });
    // Register buffers and describe accesses
    pf.add_buffer_access_patterns(
        device_hopper::buf(locations,       direction::IN,  pattern::SUCCESSIVE_SUBSECTIONS(pf.batch_size)),
        device_hopper::buf(recordDistances, direction::OUT, pattern::SUCCESSIVE_SUBSECTIONS(pf.batch_size)));
    // Add scalar kernel parameters
    pf.add_scalar_parameters(latitude, longitude, numRecords);
    // Set optional tuning parameters and call run()
    pf.opt_set_batch_size(256).opt_set_simple_indices(true).opt_set_is_idempotent(true).run();

    // Check if the results are correct
    if (!verifyResults(
            recordDistances,
            numRecords,
            locations,
            latitude,
            longitude)) {
        // Do not change these messages and the return codes.
        // The Python scripts that automate the experiments expect them.
        std::cout << "Error: The results are incorrect" << std::endl;
        return EXIT_FAILURE;
    } else {
        std::cout << "Info: The results are correct" << std::endl;
    }

    free(locations);
    free(recordDistances);

    return EXIT_SUCCESS;
}


#include "IntervalTreeVisitor.h"
#include "Logger.h"
#include <exception>
#include <boost/lexical_cast.hpp>
#include "HydLaError.h"

namespace hydla
{
namespace interval
{

using namespace hydla::symbolic_expression;

itvd IntervalTreeVisitor::pi = kv::constants<itvd>::pi();
itvd IntervalTreeVisitor::e = kv::constants<itvd>::e();

IntervalTreeVisitor::IntervalTreeVisitor()
{
}

itvd IntervalTreeVisitor::get_interval_value(const node_sptr& node, itvd *t, parameter_map_t *map)
{
  time_interval = t;
  parameter_map = map;
  HYDLA_LOGGER_DEBUG_VAR(get_infix_string(node));
  accept(node);
  if(current_value.is_integer)
    return itvd(current_value.integer);
  else
    return current_value.interval_value;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Plus> node)
{
  accept(node->get_lhs());
  IntervalOrInteger lhs = current_value;
  accept(node->get_rhs());
  IntervalOrInteger rhs = current_value;
  current_value = lhs + rhs;
  // HYDLA_LOGGER_DEBUG("Plus : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Subtract> node)
{
  accept(node->get_lhs());
  IntervalOrInteger lhs = current_value;
  accept(node->get_rhs());
  IntervalOrInteger rhs = current_value;
  current_value = lhs - rhs;
  // HYDLA_LOGGER_DEBUG("Subtract : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Times> node)
{
  accept(node->get_lhs());
  IntervalOrInteger lhs = current_value;
  accept(node->get_rhs());
  IntervalOrInteger rhs = current_value;
  current_value = lhs * rhs;
  // HYDLA_LOGGER_DEBUG("Times : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Divide> node)
{
  accept(node->get_lhs());
  IntervalOrInteger lhs = current_value;
  accept(node->get_rhs());
  IntervalOrInteger rhs = current_value;
  current_value = lhs / rhs;
  // HYDLA_LOGGER_DEBUG("Divide : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Power> node)
{
  accept(node->get_lhs());
  IntervalOrInteger lhs = current_value;
  accept(node->get_rhs());
  IntervalOrInteger rhs = current_value;

  itvd base;
  if(lhs.is_integer)
    base = itvd(lhs.integer);
  else
    base = lhs.interval_value;
  // TODO: avoid string comparison
  if(rhs.is_integer)
  {
    current_value.interval_value = pow(base, rhs.integer);
  }
  else if(get_infix_string(node->get_rhs()) == "1/2")
  {
    current_value.interval_value = sqrt(base);
  }
  else if(get_infix_string(node->get_rhs()) == "-1/2")
  {
    HYDLA_LOGGER_DEBUG_VAR(base);
    current_value.interval_value = 1/sqrt(base);
  }
  else
  {
    current_value.interval_value = pow(base, rhs.interval_value);
  }

  current_value.is_integer = false;
  // HYDLA_LOGGER_DEBUG("Power : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Negative> node)
{
  accept(node->get_child());
  current_value = -current_value;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Positive> node)
{
  accept(node->get_child());
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Pi> node)
{
  current_value.interval_value = pi;
  current_value.is_integer = false;
  // HYDLA_LOGGER_DEBUG("Pi : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::E> node)
{
  current_value.interval_value = e;
  current_value.is_integer = false;
  // HYDLA_LOGGER_DEBUG("E : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Number> node)
{
  std::string number_str = node->get_number();

  // try translation to int
  try
  {
    int integer = boost::lexical_cast<int>(number_str);
    current_value.is_integer = true;
    current_value.integer = integer;
    // HYDLA_LOGGER_DEBUG("Number : ", current_value.integer);
    // HYDLA_LOGGER_NODE_VALUE;
    return;
  }
  catch(const boost::bad_lexical_cast &e)
  {
    // do nothing
  }

  itvd itv = itvd(number_str);
  current_value.interval_value = itv;
  current_value.is_integer = false;
  // HYDLA_LOGGER_DEBUG("Number : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Float> node)
{
  current_value.interval_value = itvd(node->get_number());
  current_value.is_integer = false;
  // HYDLA_LOGGER_DEBUG("Float : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Function> node)
{
  std::string name = node->get_name();
  itvd arg;
  if(name == "sin")
  {
    if(node->get_arguments_size() != 1)
    {
      invalid_node(*node);
    }
    accept(node->get_argument(0));

    if(current_value.is_integer)
    {
      arg = itvd(current_value.integer);
    }
    else
    {
      arg = current_value.interval_value;
    }

    current_value.interval_value = sin(arg);
    current_value.is_integer = false;
    // HYDLA_LOGGER_DEBUG("Sin : ", current_value.interval_value);
  }
  else if(name == "cos")
  {
    if(node->get_arguments_size() != 1)
    {
      invalid_node(*node);
    }
    accept(node->get_argument(0));

    if(current_value.is_integer)
    {
      arg = itvd(current_value.integer);
    }
    else
    {
      arg = current_value.interval_value;
    }

    current_value.interval_value = cos(arg);
    current_value.is_integer = false;
    // HYDLA_LOGGER_DEBUG("Cos : ", current_value.interval_value);
  }
  else if(name == "tan")
  {
    if(node->get_arguments_size() != 1)
    {
      invalid_node(*node);
    }
    accept(node->get_argument(0));

    if(current_value.is_integer)
    {
      arg = itvd(current_value.integer);
    }
    else
    {
      arg = current_value.interval_value;
    }

    current_value.interval_value = tan(arg);
    current_value.is_integer = false;
  }
  else if(name == "log")
  {
    HYDLA_LOGGER_DEBUG_VAR(get_infix_string(node));
    if(node->get_arguments_size() != 1)
    {
      invalid_node(*node);
    }
    accept(node->get_argument(0));

    if(current_value.is_integer)
    {
      arg = itvd(current_value.integer);
    }
    else
    {
      arg = current_value.interval_value;
    }

    current_value.interval_value = log(arg);
    current_value.is_integer = false;
    // HYDLA_LOGGER_DEBUG("Log : ", current_value.interval_value);
  }
  else if(name == "sinh")
  {
    if(node->get_arguments_size() != 1)
    {
      invalid_node(*node);
    }
    accept(node->get_argument(0));

    if(current_value.is_integer)
    {
      arg = itvd(current_value.integer);
    }
    else
    {
      arg = current_value.interval_value;
    }

    current_value.interval_value = sinh(arg);
    current_value.is_integer = false;
  }
  else if(name == "cosh")
  {
    if(node->get_arguments_size() != 1)
    {
      invalid_node(*node);
    }
    accept(node->get_argument(0));

    if(current_value.is_integer)
    {
      arg = itvd(current_value.integer);
    }
    else
    {
      arg = current_value.interval_value;
    }

    current_value.interval_value = cosh(arg);
    current_value.is_integer = false;
  }
  else if(name == "tanh")
  {
    if(node->get_arguments_size() != 1)
    {
      invalid_node(*node);
    }
    accept(node->get_argument(0));

    if(current_value.is_integer)
    {
      arg = itvd(current_value.integer);
    }
    else
    {
      arg = current_value.interval_value;
    }

    current_value.interval_value = tanh(arg);
    current_value.is_integer = false;
  }
  else
  {
    invalid_node(*node);
  }
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::SymbolicT> node)
{
  if(time_interval == nullptr)invalid_node(*node);
  current_value.interval_value = *time_interval;
  current_value.is_integer = false;
  // HYDLA_LOGGER_DEBUG("SymbolicT : ", current_value.interval_value);
  return;
}

void IntervalTreeVisitor::visit(boost::shared_ptr<hydla::symbolic_expression::Parameter> node)
{
  if(parameter_map == nullptr)invalid_node(*node);
  parameter_t param(node->get_name(),
                    node->get_differential_count(),
                    node->get_phase_id());
  auto param_it = parameter_map->find(param);
  if(param_it == parameter_map->end())invalid_node(*node);

  range_t range = param_it->second;;

  if(range.unique())
  {
    accept(range.get_unique_value().get_node());
  }
  else
  {
    value_t lower_value = (range.get_lower_bound()).value;
    accept(lower_value.get_node());
    itvd lower_itvd;
    if(current_value.is_integer)
      lower_itvd = itvd(current_value.integer);
    else
      lower_itvd = current_value.interval_value;

    value_t uppper_value = (range.get_upper_bound()).value;
    accept(uppper_value.get_node());
    itvd upper_itvd;
    if(current_value.is_integer)
      upper_itvd = itvd(current_value.integer);
    else
      upper_itvd = current_value.interval_value;

    current_value.interval_value = itvd(lower_itvd.lower(), upper_itvd.upper());
    current_value.is_integer = false;
  }
}

void IntervalTreeVisitor::invalid_node(symbolic_expression::Node &node)
{
  throw HYDLA_ERROR("invalid node: " + node.get_string());
}

void IntervalTreeVisitor::debug_print(std::string str, itvd x)
{
  std::cout << str << x << "\n";
}

#define DEFINE_INVALID_NODE(NODE_NAME)                                \
  void IntervalTreeVisitor::visit(boost::shared_ptr<NODE_NAME> node)  \
{                                                                \
  HYDLA_LOGGER_DEBUG("");                                        \
  invalid_node(*node);                                           \
}

DEFINE_INVALID_NODE(Variable)
DEFINE_INVALID_NODE(Differential)

DEFINE_INVALID_NODE(ConstraintDefinition)
DEFINE_INVALID_NODE(ProgramDefinition)
DEFINE_INVALID_NODE(ConstraintCaller)
DEFINE_INVALID_NODE(ProgramCaller)
DEFINE_INVALID_NODE(Constraint)
DEFINE_INVALID_NODE(Ask)
DEFINE_INVALID_NODE(Tell)

DEFINE_INVALID_NODE(Equal)
DEFINE_INVALID_NODE(UnEqual)

DEFINE_INVALID_NODE(Less)
DEFINE_INVALID_NODE(LessEqual)

DEFINE_INVALID_NODE(Greater)
DEFINE_INVALID_NODE(GreaterEqual)

DEFINE_INVALID_NODE(LogicalAnd)
DEFINE_INVALID_NODE(LogicalOr)

DEFINE_INVALID_NODE(Weaker)
DEFINE_INVALID_NODE(Parallel)

DEFINE_INVALID_NODE(Always)

DEFINE_INVALID_NODE(Previous)

DEFINE_INVALID_NODE(Print)
DEFINE_INVALID_NODE(PrintPP)
DEFINE_INVALID_NODE(PrintIP)
DEFINE_INVALID_NODE(Scan)
DEFINE_INVALID_NODE(Exit)
DEFINE_INVALID_NODE(Abort)
DEFINE_INVALID_NODE(SVtimer)

DEFINE_INVALID_NODE(Not)

DEFINE_INVALID_NODE(UnsupportedFunction)

DEFINE_INVALID_NODE(ImaginaryUnit)
DEFINE_INVALID_NODE(Infinity)
DEFINE_INVALID_NODE(True)
DEFINE_INVALID_NODE(False)

DEFINE_INVALID_NODE(ProgramList)
DEFINE_INVALID_NODE(ConditionalProgramList)
DEFINE_INVALID_NODE(ExpressionList)
DEFINE_INVALID_NODE(ConditionalExpressionList)
DEFINE_INVALID_NODE(EachElement)
DEFINE_INVALID_NODE(DifferentVariable)
DEFINE_INVALID_NODE(ExpressionListElement)
DEFINE_INVALID_NODE(ExpressionListCaller)
DEFINE_INVALID_NODE(ExpressionListDefinition)
DEFINE_INVALID_NODE(ProgramListElement)
DEFINE_INVALID_NODE(ProgramListCaller)
DEFINE_INVALID_NODE(ProgramListDefinition)
DEFINE_INVALID_NODE(Range)
DEFINE_INVALID_NODE(Union)
DEFINE_INVALID_NODE(Intersection)
DEFINE_INVALID_NODE(SumOfList)
DEFINE_INVALID_NODE(MulOfList)
DEFINE_INVALID_NODE(SizeOfList)

} // namespace interval
} // namespace hydla


#ifndef GENERATER_H
#define GENERATER_H
#include <array>
#include <boost/range/adaptor/indexed.hpp>
#include <boost/range/adaptor/reversed.hpp>
#include <string>
#include <unordered_map>

#include "geoutil.hpp"
const double PI = 3.1415926535897932384626;
bool computeSweep(const Polygon_2& in, const FT offset, const Direction_2& dir,
                  bool counter_clockwise, std::vector<Point_2>* waypoints) {
  waypoints->clear();
  if (in.is_clockwise_oriented()) return false;
  Line_2 sweep(Point_2(0.0, 0.0), dir);
  std::vector<Point_2> sorted_pts = sortVerticesToLine(in, sweep);
  sweep = Line_2(sorted_pts.front(), dir);
  Vector_2 offset_vector = sweep.perpendicular(sorted_pts.front()).to_vector();
  offset_vector = offset * offset_vector /
                  std::sqrt(CGAL::to_double(offset_vector.squared_length()));
  const CGAL::Aff_transformation_2<K> kOffset(CGAL::TRANSLATION, offset_vector);
  Segment_2 sweep_segment;
  bool intersect = false;
  bool has_sweep_segment =
      findSweepSegment(in, sweep, &sweep_segment, intersect);
  if (intersect == true) {
    waypoints->clear();
    return false;
  }
  while (has_sweep_segment == true) {
    // cout << "inter: " << intersect << endl;
    if (intersect == true) {
      waypoints->clear();
      return false;
    }
    // Align sweep segment.
    if (counter_clockwise) sweep_segment = sweep_segment.opposite();
    // Connect previous sweep.
    waypoints->push_back(sweep_segment.source());
    if (!sweep_segment.is_degenerate())
      waypoints->push_back(sweep_segment.target());
    // Offset sweep.
    sweep = sweep.transform(kOffset);
    has_sweep_segment = findSweepSegment(in, sweep, &sweep_segment, intersect);
    Segment_2 prev_sweep_segment =
        counter_clockwise ? sweep_segment.opposite() : sweep_segment;
    if (!has_sweep_segment &&
        !((!waypoints->empty() &&
           *std::prev(waypoints->end(), 1) == sorted_pts.back()) ||
          (waypoints->size() > 1 &&
           *std::prev(waypoints->end(), 2) == sorted_pts.back()))) {
      sweep = Line_2(sorted_pts.back(), dir);
      has_sweep_segment =
          findSweepSegment(in, sweep, &sweep_segment, intersect);
      // Do not add super close sweep.
      if (CGAL::squared_distance(sweep_segment, prev_sweep_segment) < 0.1)
        break;
    }
    // Swap directions.
    counter_clockwise = !counter_clockwise;
  }
  return true;
}
bool computeConvexSweep(Polygon_2& in, FT offset, vector<Point_2>* waypoints) {
  Polygon_2 convexhull;
  CGAL::convex_hull_2(in.vertices_begin(), in.vertices_end(),
                      std::back_inserter(convexhull));
  simplifyPolygon(convexhull);
  Direction_2 bestdir = getBestEdgeDirection(convexhull);

  bool counter_clockwise = true;
  return computeSweep(in, offset, bestdir, counter_clockwise, waypoints);
}
/*
calculate total length of waypoints
*/
double CalculatePathlength(const vector<Point_2> waypoints) {
  int sz = waypoints.size();
  if (sz < 2)
    return 0;
  else {
    double pathlength = 0;
    for (int i = 0; i < sz - 1; i++) {
      pathlength += distance(waypoints[i], waypoints[i + 1]);
    }
    return pathlength;
  }
}
/*
find SecondOptimaldir in all sweepdir
*/
bool computeSecondOptimalSweep(const Polygon_2& in, const FT offset,
                               Direction_2* SecondOptimaldir) {
  Polygon_2 convexhull;
  CGAL::convex_hull_2(in.vertices_begin(), in.vertices_end(),
                      std::back_inserter(convexhull));
  simplifyPolygon(convexhull);
  bool counter_clockwise = true;
  Direction_2 dir;
  bool hasAcceptableSweep = false;
  double min_distance = std::numeric_limits<double>::max();
  for (EdgeConstIterator edge = convexhull.edges_begin();
       edge != convexhull.edges_end(); ++edge) {
    double Vdistance = std::numeric_limits<double>::min();
    // if not has intersection
    vector<Point_2> waypoints;
    if (computeSweep(in, offset, edge->direction(), counter_clockwise,
                     &waypoints) == true) {
      for (VertexIterator V = convexhull.vertices_begin();
           V != convexhull.vertices_end(); V++) {
        Line_2 edgeLine(edge->source(), edge->target());
        Vdistance = max(Vdistance,
                        CGAL::to_double(CGAL::squared_distance(*V, edgeLine)));
      }
      hasAcceptableSweep = true;
      if (min_distance > Vdistance + eps) {
        min_distance = Vdistance;
        dir = edge->direction();
      }
    }
    //    cout << "hello" << endl;
  }
  if (hasAcceptableSweep == false) return false;
  *SecondOptimaldir = dir;
  return true;
}
/*
try to compute a bestsweep ,otherwise second optimal sweeps;
*/
bool ComputeAllSweep(const Polygon_2& in, double footprint_width,
                     double horizontalOverwrap,
                     vector<vector<Point_2> >* cluster_sweeps,
                     Direction_2& Dir) {
  FT offset = footprint_width * (1 - horizontalOverwrap);
  bool counter_clockwise = true;
  vector<Point_2> sweep;
  // compute bestdir
  Polygon_2 convexhull;
  CGAL::convex_hull_2(in.vertices_begin(), in.vertices_end(),
                      std::back_inserter(convexhull));
  simplifyPolygon(convexhull);
  Direction_2 bestdir = getBestEdgeDirection(convexhull);
  if (computeSweep(in, offset, bestdir, counter_clockwise, &sweep) == false) {
    // can not generate a no-insert zigzag path
    if (computeSecondOptimalSweep(in, offset, &bestdir) == false) {
      cluster_sweeps->clear();
      return false;
    }
  }
  // compute cluster_waypoints
  computeSweep(in, offset, bestdir, counter_clockwise, &sweep);
  cluster_sweeps->push_back(sweep);  // counter_clockwise

  std::reverse(sweep.begin(), sweep.end());
  cluster_sweeps->push_back(sweep);  // reverse
  computeSweep(in, offset, bestdir, !counter_clockwise, &sweep);
  cluster_sweeps->push_back(sweep);  // clockwise
  std::reverse(sweep.begin(), sweep.end());
  cluster_sweeps->push_back(sweep);  // reverse
  Dir = bestdir;
  return true;
}

#endif


//===- conv.cc ------------------------------------------------------------===//
//
//                       The CIM Hardware Simulator Project
//
// See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
#include "conv.hh"
#include "diagnostic/msgHandling.hh"
#include <Eigen/Dense>

namespace cimHW {

enum class AutoPadType {
  NOTSET = 0,
  VALID = 1,
  SAME_UPPER = 2,
  SAME_LOWER = 3,
};

inline AutoPadType StringToAutoPadType(const std::string& str) {
  if (str == "NOTSET") {  // in onnx spec, default value is "NOTSET"
    return AutoPadType::NOTSET;
  }
  if (str == "VALID") {
    return AutoPadType::VALID;
  }
  if (str == "SAME_UPPER") {
    return AutoPadType::SAME_UPPER;
  }
  if (str == "SAME_LOWER") {
    return AutoPadType::SAME_LOWER;
  }
  // default, use NOTSET
  return AutoPadType::NOTSET;
}

dim_type computePadAndOutputShape(
    const int in_dim,
    const int kernel,
    const int stride,
    const int dilation,
    AutoPadType pad_type,
    int* pad_head,
    int* pad_tail)
{
  const int dkernel = dilation * (kernel - 1) + 1;
  int out_dim = 0;
  if (pad_type == AutoPadType::NOTSET)
  {
    out_dim = static_cast<int>(static_cast<float>(in_dim + *pad_head + *pad_tail - dkernel) / stride + 1);
  }
  else
  {
    switch (pad_type) {
      // VALID is nopad
      case AutoPadType::VALID:
        *pad_head = 0;
        *pad_tail = 0;
        out_dim = (in_dim - dkernel) / stride + 1;
        break;
      case AutoPadType::SAME_UPPER:
      case AutoPadType::SAME_LOWER:
      {
        assert(dilation == 1 && "Dilation not supported for AutoPadType::SAME_UPPER or AutoPadType::SAME_LOWER.");
        int legacy_target_size = (in_dim + stride - 1) / stride;
        int pad_needed = (legacy_target_size - 1) * stride + kernel - in_dim;
        out_dim = (in_dim + pad_needed - dkernel) / stride + 1;

        // pad the extra to the end if SAME_UPPER, or to the begining if SAME_LOWER
        *pad_head = (pad_type == AutoPadType::SAME_LOWER)? (pad_needed + 1) / 2 : pad_needed / 2;
        *pad_tail = pad_needed - *pad_head;
      }
      break;
      default:
        assert(false && "pad type not supported.");
    }
  }
  return out_dim;
}

// fetch all posible data cubes (along with kernel height and width) used to do matmul with kernel matrix
void im2col(const float *data_im, const int channels, const int height, const int width,
            const int kernel_h, const int kernel_w,
            const int pad_t, const int pad_b,
            const int pad_l, const int pad_r,
            const int stride_h, const int stride_w,
            const int dilation_h, const int dilation_w,
            float *data_col)
{
  auto is_a_ge_zero_and_a_lt_b = [](int a, int b) { return static_cast<unsigned>(a) < static_cast<unsigned>(b); };
  auto get_conv_output_size = [](const int intput_size,
                                 const int kernel_size,
                                 const int pad_begin,
                                 const int pad_end,
                                 const int stride_step,
                                 const int dilation_rate)
  {
    return (intput_size + pad_begin + pad_end - (dilation_rate * (kernel_size - 1) + 1)) / stride_step + 1;
  };

  const int output_h = get_conv_output_size(height, kernel_h, pad_t, pad_b, stride_h, dilation_h);
  const int output_w = get_conv_output_size(width, kernel_w, pad_l, pad_r, stride_w, dilation_w);
  const int channel_size = height * width;

  for (int channel = channels; channel--; data_im += channel_size)
  {
    for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++)
    {
      for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++)
      {
        int input_row = -pad_t + kernel_row * dilation_h;
        for (int output_rows = output_h; output_rows; output_rows--)
        {
          if (!is_a_ge_zero_and_a_lt_b(input_row, height))
          {
            for (int output_cols = output_w; output_cols; output_cols--)
            {
              *(data_col++) = 0;
            }
          }
          else
          {
            int input_col = -pad_l + kernel_col * dilation_w;
            for (int output_col = output_w; output_col; output_col--)
            {
              if (is_a_ge_zero_and_a_lt_b(input_col, width))
              {
                *(data_col++) = data_im[input_row * width + input_col];
              }
              else
              {
                *(data_col++) = 0;
              }
              input_col += stride_w;
            } // output_col loop
          }
          input_row += stride_h;
        } // output_rows loop
      } // kernel_col loop
    } // kernel_row loop
  } // channel loop
}

// fetch all posible data cubes (along with kernel D1, D2, D3....Dd) used to do matmul with kernel matrix
void im2colNd(const float* data_img, const int* im_shape, const int* col_shape,
              const int* m_kernel_shape, const int N,
              const int* pad,
              const int* stride,
              const int* dilation,
              float* data_col,
              const int padding_value = 0)
{
  int kernel_size = 1;
  for (int i = 0; i < N; ++i) {
    kernel_size *= m_kernel_shape[i];
  }
  int channels_col = col_shape[0];
  std::vector<int> d_offset(N, 0);
  std::vector<int> d_iter(N, 0);
  for (int c_col = 0; c_col < channels_col; ++c_col) {
    // Loop over spatial axes in reverse order to compute a per-axis offset.
    int offset = c_col;
    for (int d_i = N - 1; d_i >= 0; --d_i) {
      if (d_i < N - 1) {
        offset /= m_kernel_shape[d_i + 1];
      }
      d_offset[d_i] = offset % m_kernel_shape[d_i];
    }
    for (bool incremented = true; incremented;) {
      // Loop over spatial axes in forward order to compute the indices in the
      // image and column, and whether the index lies in the padding.
      int index_col = c_col;
      int index_im = c_col / kernel_size;
      bool is_padding = false;
      for (int d_i = 0; d_i < N; ++d_i) {
        int d = d_iter[d_i];
        int d_im = d * stride[d_i] - pad[d_i] + d_offset[d_i] * dilation[d_i];
        is_padding |= d_im < 0 || d_im >= im_shape[d_i + 1];
        index_col *= col_shape[d_i + 1];
        index_col += d;
        index_im *= im_shape[d_i + 1];
        index_im += d_im;
      }
      if (is_padding) {
        data_col[index_col] = padding_value;
      } else {
        data_col[index_col] = data_img[index_im];
      }
      // Loop over spatial axes in reverse order to choose an index,
      // like counting.
      incremented = false;
      for (int d_i = N - 1; d_i >= 0; --d_i) {
        int d_max = col_shape[d_i + 1];
        if (d_iter[d_i] == d_max - 1) {
          d_iter[d_i] = 0;
        } else {  // d_iter[d_i] < d_max - 1
          ++d_iter[d_i];
          incremented = true;
          break;
        }
      }
    }  // while(incremented) {
  }    // for (int c = 0; c < channels_col; ++c) {
}

cimHWConvOp::cimHWConvOp(
  const std::string &configFilePath,
  const_element_type* input_X_, const_dim_type input_X_ndim_, const_dim_type* input_X_dims_,
  const_element_type* input_W_, const_dim_type input_W_ndim_, const_dim_type* input_W_dims_,
  const_element_type* input_B_, const_dim_type input_B_ndim_, const_dim_type* input_B_dims_,
  element_type* output_Y_, const_dim_type output_Y_ndim_, const_dim_type* output_Y_dims_,
  const char* auto_pad_,
  const_dim_type* dilations_, const_dim_type number_of_dilations_,
  const_dim_type group_,
  const_dim_type* kernel_shape_, const_dim_type number_of_kernel_shape_,
  const_dim_type* pads_, const_dim_type number_of_pads_,
  const_dim_type* strides_, const_dim_type number_of_strides_)
  : cimHWOp("Conv")
  , m_cimCU(configFilePath)
  , m_input_X(input_X_)
  , m_input_W(input_W_)
  , m_input_B(input_B_)
  , m_output_Y(output_Y_)
{
  verbose1(opName());

  // input validation
  if(input_X_ndim_ <= 2)
    error("Minimum dimension of X is 3");
  if(input_W_ndim_ <= 2)
    error("Minimum dimension of W is 3");

  m_NInputs = input_X_dims_[0];
  if(m_NInputs != 1)
    error("Only support 1 batch now");

  // basic property of conv
  m_Channel       = input_X_dims_[1];
  m_OutputChannel = input_W_dims_[0];
  m_input_X_shape = std::vector<dim_type>(input_X_dims_, input_X_dims_ + input_X_ndim_);
  m_input_W_shape = std::vector<dim_type>(input_W_dims_, input_W_dims_ + input_W_ndim_);
  if(m_Channel != input_W_dims_[1])
    error("Channel of X and W is not consistent");

  // check kernel related properties
  // find out kernel shape, which is the shape excludes M and C from weights
  m_kernel_shape  = std::vector<dim_type>(input_W_dims_ + 2, input_W_dims_ + input_W_ndim_);
  m_input_B_shape = std::vector<dim_type>(input_B_dims_, input_B_dims_ + input_B_ndim_);
  // check dilation
  m_dilations = std::vector<dim_type>(dilations_, dilations_ + number_of_dilations_);
  if (m_dilations.empty()) {
    m_dilations.resize(m_kernel_shape.size(), 1);
  }
  // check group
  m_group = group_;
  // check pads
  m_pads = std::vector<dim_type>(pads_, pads_ + number_of_pads_);
  if (m_pads.empty()) {
    m_pads.resize(m_kernel_shape.size() * 2, 0);
  }
  // check strides
  m_strides = std::vector<dim_type>(strides_, strides_ + number_of_strides_);
  if (m_strides.empty()) {
    m_strides.resize(m_kernel_shape.size(), 1);
  }

  // infer the shape of output and output_Y from input
  m_image_shape = std::vector<dim_type>(input_X_dims_ + 2, input_X_dims_ + input_X_ndim_);
  m_output_Y_shape = std::vector<dim_type> ({m_NInputs, m_OutputChannel});
  for(int i=0; i< m_image_shape.size(); i++) {
    m_output_Y_shape.push_back(computePadAndOutputShape(m_image_shape[i], m_kernel_shape[i], m_strides[i], m_dilations[i],
                                                        StringToAutoPadType(std::string(auto_pad_)),
                                                        &m_pads[i], &m_pads[ m_image_shape.size()+i]));
  }
  for(int i=2; i<m_output_Y_shape.size(); i++)
    m_output_shape.push_back(m_output_Y_shape[i]);
}

void cimHWConvOp::simulate()
{
  // calculate the size of each tensor
  dim_type m_input_size  = std::accumulate( m_image_shape.cbegin()  ,  m_image_shape.cend()  , 1, std::multiplies<dim_type>());
  dim_type m_weight_size = std::accumulate(m_input_W_shape.cbegin() , m_input_W_shape.cend() , 1, std::multiplies<dim_type>());
  dim_type m_kernel_size = std::accumulate(m_kernel_shape.cbegin()  , m_kernel_shape.cend()  , 1, std::multiplies<dim_type>());
  dim_type m_output_size = std::accumulate(m_output_shape.cbegin()  , m_output_shape.cend()  , 1, std::multiplies<dim_type>());

  // calculate the extended number for easy use
  const dim_type X_offset      = m_Channel / m_group * m_input_size;
  const dim_type W_offset      = m_weight_size / m_group;
  const dim_type kernel_offset = m_kernel_size * m_Channel / m_group;
  const dim_type kernel_dim    = m_Channel / m_group * m_kernel_size;
  const dim_type kernel_rank   = m_kernel_shape.size();

  std::vector<dim_type> col_buffer_shape;
  // Pointwise convolutions can use the original input tensor in place,
  // otherwise a temporary buffer is required for the im2col transform.
  // Todo: pointwise conv can do inplace gemm without dataCube and col_buffer
  //if (kernel_size != 1/* || !HasStridesOneAndNoPadding()*/) {
  if (kernel_rank != 2) {
    // only used in in2colNd
    col_buffer_shape.reserve(1 + m_output_shape.size());
    col_buffer_shape.push_back(kernel_dim);
    col_buffer_shape.insert(col_buffer_shape.end(), m_output_shape.begin(), m_output_shape.end());
  }
  //}

  // Object is to get matrix product N*M(output_channels) x dataCubes(output_image_size) = kernel * dataCubes
  MatrixXfRowMajor result = MatrixXfRowMajor::Zero(m_OutputChannel, m_output_size);
  // prepare dataCubes = dataCubes(output_image_size) x (kernel_size), column-major
  MatrixXfRowMajor dataCubes = MatrixXfRowMajor::Zero(kernel_dim, m_output_size);
  for (int group_id = 0; group_id < m_group; ++group_id)
  {
    // apply im2col for conv2d
    if (kernel_rank == 2) {

      im2col(m_input_X + group_id * X_offset, m_Channel / m_group,  m_image_shape[0],  m_image_shape[1],
               m_kernel_shape[0], m_kernel_shape[1],
               m_pads[0], m_pads[2],
               m_pads[1], m_pads[3],
               m_strides[0], m_strides[1],
               m_dilations[0], m_dilations[1],
               dataCubes.data());
    }
    else
    // otherwise, apply im2colNd, including conv1d and more than conv3d
    {
      im2colNd(m_input_X + group_id * X_offset, m_input_X_shape.data() + 1, col_buffer_shape.data(),
              m_kernel_shape.data(), kernel_rank,
              m_pads.data(),
              m_strides.data(),
              m_dilations.data(),
              dataCubes.data());
    }

    // prepare kernel =  M(output_channels)/group x (kernel_size)
    Eigen::Map<const MatrixXfRowMajor> kernel(m_input_W + group_id * W_offset + group_id * kernel_offset, m_OutputChannel / m_group, kernel_dim);
    // send datacube and each kernel by row to ComputeUnit
    for(int i=0; i<kernel.rows(); i++)
      result.block(group_id * m_OutputChannel / m_group, 0, m_OutputChannel / m_group, m_output_size).row(i) =
                   m_cimCU.compute(dataCubes, kernel.row(i).transpose());
  }

  // finally, plus bias to every kernel
  if(m_input_B != nullptr)
  {
    MatrixXfRowMajor bias_mat(m_OutputChannel, m_output_size);
    bias_mat.colwise() = Eigen::Map<const Eigen::VectorXf>(m_input_B, m_input_B_shape[0]);
    result += bias_mat;
  }

  // copy back to output
  memcpy(m_output_Y, result.data(), sizeof(numType) * m_OutputChannel * m_output_size);
  verbose1("\n");
}

}  // namespace cimHW