// ****************************************************************************
//
//          Aevol - An in silico experimental evolution platform
//
// ****************************************************************************
//
// Copyright: See the AUTHORS file provided with the package or <www.aevol.fr>
// Web: http://www.aevol.fr/
// E-mail: See <http://www.aevol.fr/contact/>
// Original Authors : Guillaume Beslon, Carole Knibbe, David Parsons
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// ****************************************************************************

/// TODO: add unit tests
/// Why should there always be points (X_MIN, 0),(X_MAX, 0) ?
/// Many tests for == equality between doubles. Should't we check mod ε?

#include "AeTime.h"
#include "Point.h"
#include "Discrete_Double_Fuzzy.h"
#include "macros.h"
#include "utility.h"
#include "utils.h"

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>

using std::cout;
using std::endl;
using std::fabs;

namespace aevol {

Discrete_Double_Fuzzy::Discrete_Double_Fuzzy(size_t vector_size) {
  nb_intervals_ = vector_size - 1;
  nb_intervals_d_ = static_cast<double>(nb_intervals_);
  points_.resize(vector_size, 0);
}

void Discrete_Double_Fuzzy::simplify() {
  // NOT USEFUL
}

/// Add a triangle to the fuzzy set.
/// \param mean abscissa of its apex
/// \param half_width half width of the triangle
/// \param height ordinate of the apex
void Discrete_Double_Fuzzy::add_triangle(fuzzy_x_t mean,
                                         fuzzy_x_t half_width,
                                         fuzzy_y_t height,
                                         bool) {
  assert(half_width > 0.0);
  assert(X_MIN <= mean and mean <= X_MAX);
  assert(W_MIN <= half_width);
  // assert(MIN_H <= height and height <= MAX_H); Not necessarily because the concentration can be > 1

  const fuzzy_x_t x_threshold = 1e-15; // TODO: should it not be the machine epsilon?
  const fuzzy_y_t y_threshold = 1e-15; // TODO: should it not be the machine epsilon?
                                       // if not, it should at least be a class constant

  if (fabs(half_width) < x_threshold or fabs(height) < y_threshold)
    return;

  fuzzy_x_t x0 = mean - half_width;
  fuzzy_x_t x1 = mean;
  fuzzy_x_t x2 = mean + half_width;

  auto loop_A_start = static_cast<size_t>(std::max(std::ceil(x0 * nb_intervals_d_), 0.));
  loop_A_start      = loop_A_start > nb_intervals_ ? nb_intervals_ : loop_A_start;

  auto loop_A_end = static_cast<size_t>(std::max(std::ceil(x1 * nb_intervals_d_), 0.));
  loop_A_end      = loop_A_end > nb_intervals_ ? nb_intervals_ : loop_A_end;

  for (auto i = loop_A_start; i < loop_A_end; ++i) {
    points_[i] += (((i / nb_intervals_d_) - x0) / (x1 - x0)) * height;
  }

  // Compute the second equation of the triangle
  // Updating value between x1 and x2
  auto loop_B_start = static_cast<size_t>(std::max(std::ceil(x1 * nb_intervals_d_), 0.));
  loop_B_start = size_t(loop_B_start) > nb_intervals_ ? nb_intervals_ : loop_B_start;

  auto loop_B_end = static_cast<size_t>(std::max(std::ceil(x2 * nb_intervals_d_), 0.));
  if (loop_B_end > nb_intervals_) {
    points_[nb_intervals_ - 1] += height * ((x2 - 1.0) / (x2 - x1));
  }
  loop_B_end = loop_B_end > nb_intervals_ ? nb_intervals_ : loop_B_end;

  for (auto i = loop_B_start; i < loop_B_end; i++) {
    points_[i] += height * ((x2 - (i / nb_intervals_d_)) / (x2 - x1));
  }
}

/// Add a fuzzy set to the current one.
///
/// Should actually be called `operator+=()`.
///
/// Semantically speaking, we deal with fuzzy sets over the same
/// range. So adding two fuzzy sets sums up to adding the probability
/// functions.
void Discrete_Double_Fuzzy::add(const AbstractFuzzy& f, bool) {
  auto& fs = dynamic_cast<const Discrete_Double_Fuzzy&>(f);

  for (auto i = decltype(points_.size()){0}; i < points_.size(); ++i) {
    points_[i] += fs.points_[i];
  }
}

/// Substract to the current fuzzy set.
///
/// TODO: Dumb version (?), to be completed.
void Discrete_Double_Fuzzy::sub(const AbstractFuzzy& f, bool) {
  auto& fs = dynamic_cast<const Discrete_Double_Fuzzy&>(f);

  for (auto i = decltype(points_.size()){0}; i < points_.size(); ++i) {
    points_[i] -= fs.points_[i];
  }
}

void Discrete_Double_Fuzzy::copy(const AbstractFuzzy& f, bool) {
  auto& fs = dynamic_cast<const Discrete_Double_Fuzzy&>(f);

  for (auto i = decltype(points_.size()){0}; i < points_.size(); ++i) {
    points_[i] = fs.points_[i];
  }
}

void Discrete_Double_Fuzzy::normalize(const fuzzy_area_t& area) {
  const auto orig_area = get_geometric_area();
  if (orig_area == 0.)
    return;

  const auto norm_factor = area / orig_area;

  for (auto& pt: points_) {
    pt *= norm_factor;
  }
}

fuzzy_area_t Discrete_Double_Fuzzy::get_geometric_area(bool) const {
  return get_geometric_area(X_MIN, X_MAX);
}

fuzzy_area_t Discrete_Double_Fuzzy::get_geometric_area(fuzzy_x_t x_start, fuzzy_x_t x_stop) const {
  // Precondition: X_MIN ≤ x_start < x_stop ≤ X_MAX
  assert(X_MIN <= x_start and x_start < x_stop and x_stop <= X_MAX);
  fuzzy_area_t area = 0;

  for (auto fuzzy_idx = decltype(points_.size()){0}; fuzzy_idx < points_.size() - 1; ++fuzzy_idx) {
    area += fabs(
        ((points_[fuzzy_idx] +
          points_[fuzzy_idx + 1]) /
         (nb_intervals_d_ * 2)));
  }

  return area;
}

/// Probability function gets clipped either upwise ou downwise.
///
/// `pf` := min(`pf`, `upper_bound`)
///
///            X    above: removed               |
///           / \                                |
///          /   \               X      bound    |
/// --------o-----o-------------o-o--------      |
///        /       \   X       /   \             |
///       X         \ / \     /     \            |
///                  X   \   /       X           |
///                       \ /                    |
///      underneath: kept  X                     |

/// `pf` := max(`pf`, `lower_bound`)
void Discrete_Double_Fuzzy::clip(clipping_direction direction, fuzzy_y_t bound) {
  for (auto i = decltype(points_.size()){0}; i < points_.size(); ++i) {
    if ((direction == AbstractFuzzy::clipping_direction::min and
         points_[i] < bound) or
        (direction == AbstractFuzzy::clipping_direction::max and
         points_[i] > bound))
      points_[i] = bound;
  }
}

bool Discrete_Double_Fuzzy::is_identical_to(const AbstractFuzzy&, double) const {
  assert(false);
  return false;
}

/// Set all points ordinate to 0
///
// TODO <david.parsons@inria.fr> Not sure if it's useful.
void Discrete_Double_Fuzzy::reset() {
  for (auto i = decltype(points_.size()){0}; i < points_.size(); ++i) {
    points_[i] = 0.0;
  }
}

void Discrete_Double_Fuzzy::clear() { reset(); }

Point Discrete_Double_Fuzzy::point(size_t i) const {
  return Point((i / nb_intervals_d_), points_[i]);
}

void Discrete_Double_Fuzzy::add_point(fuzzy_x_t x, fuzzy_y_t y) {
  auto i = static_cast<size_t>(std::round(x * nb_intervals_d_));
  if (i < points_.size()) {
    points_[i] = y;
  }
}

void Discrete_Double_Fuzzy::print() const {
  for (auto i = decltype(points_.size()){0}; i < points_.size(); ++i) {
    printf("[%f : %f]\n", i / nb_intervals_d_, points_[i]);
  }
  printf("\n");
}

mxifstream& Discrete_Double_Fuzzy::do_extraction(mxifstream& is) {
  is >> std::ws;
  assert(is.get() == '<');
  is >> nb_intervals_;
  assert(is.get() == ';');
  is >> nb_intervals_d_;
  assert(is.get() == ';');
  is.read(reinterpret_cast<char*>(points_.data()), points_.size() * sizeof(points_.data()));
  assert(is.get() == '>');
  return is;
}

mxofstream& Discrete_Double_Fuzzy::do_insertion(mxofstream& os) const {
  os << '<' << nb_intervals_ << ';' << nb_intervals_d_ << ';';
  os.write(reinterpret_cast<const char*>(points_.data()), points_.size() * sizeof(points_.data()));
  os << '>';
  return os;
}

}  // namespace aevol
