// Copyright (c) 2019 CNRS
// Authors: Joseph Mirabel
//

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
// DAMAGE.

#include <hpp/constraints/differentiable-function.hh>
#include <hpp/constraints/implicit.hh>
#include <hpp/core/config-projector.hh>
#include <hpp/core/config-validations.hh>
#include <hpp/core/configuration-shooter.hh>
#include <hpp/core/path-validation.hh>
#include <hpp/core/problem.hh>
#include <hpp/core/roadmap.hh>
#include <hpp/manipulation/path-planner/end-effector-trajectory.hh>
#include <hpp/manipulation/steering-method/end-effector-trajectory.hh>
#include <hpp/pinocchio/device-sync.hh>
#include <hpp/pinocchio/liegroup-element.hh>
#include <hpp/pinocchio/util.hh>
#include <hpp/util/exception-factory.hh>
#include <pinocchio/multibody/data.hpp>

namespace hpp {
namespace manipulation {
namespace pathPlanner {
typedef manipulation::steeringMethod::EndEffectorTrajectory SM_t;
typedef manipulation::steeringMethod::EndEffectorTrajectoryPtr_t SMPtr_t;

EndEffectorTrajectoryPtr_t EndEffectorTrajectory::create(
    const core::ProblemConstPtr_t& problem) {
  EndEffectorTrajectoryPtr_t ptr(new EndEffectorTrajectory(problem));
  ptr->init(ptr);
  return ptr;
}

EndEffectorTrajectoryPtr_t EndEffectorTrajectory::createWithRoadmap(
    const core::ProblemConstPtr_t& problem, const core::RoadmapPtr_t& roadmap) {
  EndEffectorTrajectoryPtr_t ptr(new EndEffectorTrajectory(problem, roadmap));
  ptr->init(ptr);
  return ptr;
}

void EndEffectorTrajectory::tryConnectInitAndGoals() {}

void EndEffectorTrajectory::startSolve() {
  // core::PathPlanner::startSolve();
  // problem().checkProblem ();
  if (!problem()->robot()) {
    std::string msg("No device in problem.");
    hppDout(error, msg);
    throw std::runtime_error(msg);
  }

  if (!problem()->initConfig()) {
    std::string msg("No init config in problem.");
    hppDout(error, msg);
    throw std::runtime_error(msg);
  }

  // Tag init and goal configurations in the roadmap
  roadmap()->resetGoalNodes();

  SMPtr_t sm(HPP_DYNAMIC_PTR_CAST(SM_t, problem()->steeringMethod()));
  if (!sm)
    throw std::invalid_argument(
        "Steering method must be of type "
        "hpp::manipulation::steeringMethod::EndEffectorTrajectory");

  if (!sm->constraints() || !sm->constraints()->configProjector())
    throw std::invalid_argument(
        "Steering method constraint has no ConfigProjector.");
  core::ConfigProjectorPtr_t constraints(sm->constraints()->configProjector());

  const constraints::ImplicitPtr_t& trajConstraint = sm->trajectoryConstraint();
  if (!trajConstraint)
    throw std::invalid_argument(
        "EndEffectorTrajectory has no trajectory constraint.");
  if (!sm->trajectory())
    throw std::invalid_argument("EndEffectorTrajectory has no trajectory.");

  const core::NumericalConstraints_t& ncs = constraints->numericalConstraints();
  bool ok = false;
  for (std::size_t i = 0; i < ncs.size(); ++i) {
    if (ncs[i] == trajConstraint) {
      ok = true;
      break;  // Same pointer
    }
    // Here, we do not check the right hand side on purpose.
    // if (*ncs[i] == *trajConstraint) {
    if (ncs[i]->functionPtr() == trajConstraint->functionPtr() &&
        ncs[i]->comparisonType() == trajConstraint->comparisonType()) {
      ok = true;
      // TODO We should only modify the path constraint.
      // However, only the pointers to implicit constraints are copied
      // while we would like the implicit constraints to be copied as well.
      ncs[i]->rightHandSideFunction(sm->trajectory());
      break;  // logically identical
    }
  }
  if (!ok) {
    HPP_THROW(std::logic_error,
              "EndEffectorTrajectory could not find "
              "constraint "
                  << trajConstraint->function());
  }
}

void EndEffectorTrajectory::oneStep() {
  SMPtr_t sm(HPP_DYNAMIC_PTR_CAST(SM_t, problem()->steeringMethod()));
  if (!sm)
    throw std::invalid_argument(
        "Steering method must be of type "
        "hpp::manipulation::steeringMethod::EndEffectorTrajectory");
  if (!sm->trajectoryConstraint())
    throw std::invalid_argument(
        "EndEffectorTrajectory has no trajectory constraint.");
  if (!sm->trajectory())
    throw std::invalid_argument("EndEffectorTrajectory has no trajectory.");

  if (!sm->constraints() || !sm->constraints()->configProjector())
    throw std::invalid_argument(
        "Steering method constraint has no ConfigProjector.");
  core::ConfigProjectorPtr_t constraints(sm->constraints()->configProjector());

  core::ConfigValidationPtr_t cfgValidation(problem()->configValidations());
  core::PathValidationPtr_t pathValidation(problem()->pathValidation());
  core::ValidationReportPtr_t cfgReport;
  core::PathValidationReportPtr_t pathReport;

  core::interval_t timeRange(sm->timeRange());

  // Generate a vector if configuration where the first one is the initial
  // configuration and the following ones are random configurations
  std::vector<core::Configuration_t> qs(
      configurations(*problem()->initConfig()));
  if (qs.empty()) {
    hppDout(info, "Failed to generate initial configs.");
    return;
  }

  // Generate a valid initial configuration.
  bool success = false;
  bool resetRightHandSide = true;
  std::size_t i;

  vector_t times(nDiscreteSteps_ + 1);
  matrix_t steps(problem()->robot()->configSize(), nDiscreteSteps_ + 1);

  // Discretize definition interval of the steering method into times
  times[0] = timeRange.first;
  for (int j = 1; j < nDiscreteSteps_; ++j)
    times[j] = timeRange.first +
               j * (timeRange.second - timeRange.first) / nDiscreteSteps_;
  times[nDiscreteSteps_] = timeRange.second;

  // For each random configuration,
  //   - compute initial configuration of path by projecting the random
  //     configuration (initial configuration for the first time),
  //   - compute following samples by projecting current sample after
  //     updating right hand side.
  // If failure, try next random configuration.
  // Failure can be due to
  //   - projection,
  //   - collision of final configuration,
  //   - validation of path (for collision mainly).
  for (i = 0; i < qs.size(); ++i) {
    if (resetRightHandSide) {
      constraints->rightHandSideAt(times[0]);
      resetRightHandSide = false;
    }
    Configuration_t& q(qs[i]);
    if (!constraints->apply(q)) continue;
    if (!cfgValidation->validate(q, cfgReport)) continue;
    resetRightHandSide = true;

    steps.col(0) = q;
    success = true;
    for (int j = 1; j <= nDiscreteSteps_; ++j) {
      constraints->rightHandSideAt(times[j]);
      hppDout(info, "RHS: " << setpyformat
                            << constraints->rightHandSide().transpose());
      steps.col(j) = steps.col(j - 1);
      if (!constraints->apply(steps.col(j))) {
        hppDout(info, "Failed to generate destination config.\n"
                          << setpyformat << *constraints
                          << "\nq=" << one_line(q));
        success = false;
        break;
      }
    }
    if (!success) continue;
    success = false;

    // Test collision of final configuration.
    if (!cfgValidation->validate(steps.col(nDiscreteSteps_), cfgReport)) {
      hppDout(info, "Destination config is in collision.");
      continue;
    }

    core::PathPtr_t path = sm->projectedPath(times, steps);
    if (!path) {
      hppDout(info, "Steering method failed.\n"
                        << setpyformat << "times: " << one_line(times) << '\n'
                        << "configs:\n"
                        << condensed(steps.transpose()) << '\n');
      continue;
    }

    core::PathPtr_t validPart;
    if (!pathValidation->validate(path, false, validPart, pathReport)) {
      hppDout(info, "Path is in collision.");
      continue;
    }

    // In case of success,
    //   - insert q_init as initial configuration of the roadmap,
    //   - insert final configuration as goal node in the roadmap,
    //   - add a roadmap edge between them and stop.
    roadmap()->initNode(make_shared<Configuration_t>(steps.col(0)));
    core::NodePtr_t init = roadmap()->initNode();
    core::NodePtr_t goal = roadmap()->addGoalNode(
        make_shared<Configuration_t>(steps.col(nDiscreteSteps_)));
    roadmap()->addEdge(init, goal, path);
    success = true;
    if (feasibilityOnly_) break;
  }
}

std::vector<core::Configuration_t> EndEffectorTrajectory::configurations(
    const core::Configuration_t& q_init) {
  if (!ikSolverInit_) {
    std::vector<core::Configuration_t> configs(nRandomConfig_ + 1);
    configs[0] = q_init;
    for (int i = 1; i < nRandomConfig_ + 1; ++i)
      problem()->configurationShooter()->shoot(configs[i]);
    return configs;
  }

  // TODO Compute the target and call ikSolverInit_
  // See https://gepgitlab.laas.fr/airbus-xtct/hpp_airbus_xtct for an
  // example using IKFast.
  throw std::runtime_error(
      "Using an IkSolverInitialization is not implemented yet");
}

EndEffectorTrajectory::EndEffectorTrajectory(
    const core::ProblemConstPtr_t& problem)
    : core::PathPlanner(problem) {}

EndEffectorTrajectory::EndEffectorTrajectory(
    const core::ProblemConstPtr_t& problem, const core::RoadmapPtr_t& roadmap)
    : core::PathPlanner(problem, roadmap) {}

void EndEffectorTrajectory::checkFeasibilityOnly(bool enable) {
  feasibilityOnly_ = enable;
}

void EndEffectorTrajectory::init(const EndEffectorTrajectoryWkPtr_t& weak) {
  core::PathPlanner::init(weak);
  weak_ = weak;
  nRandomConfig_ = 10;
  nDiscreteSteps_ = 1;
  feasibilityOnly_ = true;
}
}  // namespace pathPlanner
}  // namespace manipulation
}  // namespace hpp