// Copyright (C) 2018  INRIA
//
// Treerecs is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Treerecs 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

// Include Googletest
#include <gtest/gtest.h>

// Include Bpplib
#include <Bpp/Phyl/Tree/PhyloTree.h>

// Include containers
#include <memory>
#include <treerecs/tools/Statistics.h>
#include <treerecs/containers/Grid.h>
#include <treerecs/containers/Table.h>
#include <treerecs/containers/GeneMap.h>

// Include handlers, tools
#include <treerecs/tools/SpeciesGeneMapper.h>
#include <treerecs/tools/IO/IO.h>
#include <treerecs/tools/utils.h>
#include <treerecs/tools/random.h>

// Include examples paths
#include "examples_paths.h"

using namespace treerecs;

static auto& preferred_separator = IO::preferred_separator; // until C++17

class Utils_tests: public testing::Test {
protected:
virtual void SetUp() {
};

virtual void TearDown() {}

  std::vector<int> vect = {0, 1, 2, 3, 4, 5};
  std::list<int> list = {0, 1, 2, 3, 4, 5};
  std::vector<int> random_vect = {42, 65, 2, 23, 39};
  std::vector<int> random_vect_long = {83, 52, 16, 68, 39, 69, 94, 83,
                                       7, 12, 74, 22, 47, 2, 24, 58, 96,
                                       25, 93, 88, 70};
  std::vector<int> zeros_vect = {0, 0, 0, 0, 0};
};

TEST_F(Utils_tests, testUtils) {
  // Basic function sum of vector's elements
  ASSERT_EQ(54, utils::sum(std::vector<int>({1, 8, 45})));
}

TEST_F(Utils_tests, testStringsTools) {
  // Strings functions
  // Test utils printers
  std::stringstream sstr;
  sstr << list;
  ASSERT_STREQ("[0, 1, 2, 3, 4, 5]", sstr.str().c_str());

  sstr.str(""); //cleaning
  utils::write(sstr, list.begin(), list.end());
  ASSERT_STREQ("{0, 1, 2, 3, 4, 5}", sstr.str().c_str());

  sstr.str(""); //cleaning
  utils::write(sstr, list.begin(), list.end(), "^", ":", "$");
  ASSERT_STREQ("^0:1:2:3:4:5$", sstr.str().c_str());

  // Test string split.
  ASSERT_EQ(5,
            utils::splitString("a; bgb;bgd,  \t;fdsf  ;  ", ";").size());
  ASSERT_EQ(5,
            utils::splitString("a; bgb;bgd,  \t;fdsf  ;;  ", ";").size());
  ASSERT_EQ(6,
            utils::splitString("a; bgb;bgd,  \t;fdsf  ;;  ", ";", false).size());
  ASSERT_EQ(5,
            utils::splitString("a bgb  ;bgd,  \t;fdsf     ;  ", " ").size());
  ASSERT_EQ(2,
            utils::splitString("a bgb  ;bgd,  \t;fdsf     ;  ", "\t").size());
  ASSERT_EQ(3,
            utils::splitString("a bgb  ;bgd,  \t;fdsf     ;  ", ",\t").size());

  // Test stringIsNumber.
  ASSERT_TRUE(utils::stringIsNumber("5"));
  ASSERT_FALSE(utils::stringIsNumber("z"));
  ASSERT_FALSE(utils::stringIsNumber(""));
  ASSERT_FALSE(utils::stringIsNumber(" "));

  // Test string comp (case in/sensitive).
  ASSERT_FALSE(utils::string_comp("a", "A", true));
  ASSERT_FALSE(utils::string_comp("a", "", false));
  ASSERT_FALSE(utils::string_comp("a", "", true));
  ASSERT_TRUE(utils::string_comp("a", "A", false));

  // Test string trim.
  ASSERT_STREQ("Hola que tal ? ",   utils::ltrim("   Hola que tal ? ").c_str());
  ASSERT_STREQ("   Hola que tal ?", utils::rtrim("   Hola que tal ? ").c_str());
  ASSERT_STREQ("Hola que tal ?",    utils::trim("   Hola que tal ? ").c_str());
  ASSERT_STRNE("hola que tal ?",    utils::trim("   Hola que tal ? ").c_str());

  // Test regex match.
  ASSERT_TRUE(utils::strmatch_regex("Hola que tal", "Hola que tal"));
  ASSERT_FALSE(utils::strmatch_regex("Hola que tal", "Hola*"));
  ASSERT_TRUE(utils::strmatch_regex("Hola que tal", "^Hola[a-zA-z ?]*"));
  ASSERT_FALSE(utils::strmatch_regex("Hola que tal", "^Hola[a-zA-Z]*"));

  // Test substring replacement.
  std::string str = "Hola que tal";
  ASSERT_STREQ("Hola to tal", utils::replace(str, "que", "to").c_str());
  ASSERT_STREQ("Hola  tal", utils::replace(str, "que", "").c_str());
  ASSERT_STREQ("Hola que i", utils::replace(str, "tal", "i").c_str());
  ASSERT_STREQ("i", utils::replace(str, str, "i").c_str());
  ASSERT_STREQ("", utils::replace(str, str, "").c_str());
  ASSERT_STREQ("HolX que tXl", utils::replace(str, 'a', 'X').c_str());
  ASSERT_STREQ("Hol", utils::replace(str, 'a', '\0').c_str());
  ASSERT_STREQ("Hola", utils::replace(str, ' ', '\0').c_str());

  // Test substr count.
  ASSERT_EQ(2, utils::count("Hola que tal", "a"));
  ASSERT_EQ(2, utils::count("a; bgb;bgd,  \\t;fdsf  ;  \", \";", "bg"));
  ASSERT_EQ(5, utils::count("1 word, 2 words, three words,\n "
                                "four words, five words.", "word"));

  // Tests filename extraction.
  ASSERT_STREQ("file.txt",
               utils::extractFilename(std::string() + preferred_separator +
                                      "path" + preferred_separator +
                                      "to" + preferred_separator +
                                      "file.txt").c_str());
  ASSERT_STREQ("file.txt",
               utils::extractFilename("file.txt").c_str());
  ASSERT_STREQ("file",
               utils::extractFilename(std::string() + preferred_separator +
                                      "path" + preferred_separator +
                                      "to" + preferred_separator +
                                      "file").c_str());

  // Is yes
  ASSERT_TRUE(utils::isYes("yes"));
  ASSERT_TRUE(utils::isYes("YES"));
  ASSERT_TRUE(utils::isYes("y"));
  ASSERT_TRUE(utils::isYes("Y"));
  ASSERT_TRUE(utils::isYes("true"));
  ASSERT_TRUE(utils::isYes("True"));
  ASSERT_TRUE(utils::isYes("t"));
  ASSERT_TRUE(utils::isYes("T"));
  ASSERT_FALSE(utils::isYes("FALSE"));
  ASSERT_FALSE(utils::isYes("false"));
  ASSERT_FALSE(utils::isYes("X"));
  ASSERT_FALSE(utils::isYes(""));
}

TEST_F(Utils_tests, testMaths) {
  // Test factorials
  ASSERT_EQ(6, utils::factorial(3));
  ASSERT_EQ(24, utils::factorial(4));
  ASSERT_EQ(120, utils::factorial(5));
  ASSERT_EQ(5040, utils::factorial(7));
  ASSERT_EQ(3628800, utils::factorial(10));

  ASSERT_EQ(3, utils::double_factorial(3));
  ASSERT_EQ(8, utils::double_factorial(4));
  ASSERT_EQ(15, utils::double_factorial(5));
  ASSERT_EQ(105, utils::double_factorial(7));
  ASSERT_EQ(3840, utils::double_factorial(10));

  // Test lin. interpolation used by quantiles
  ASSERT_EQ(5,
            static_cast<int>(utils::linearInterpolation<double>(0, 10, 0.5)));

  // Test quantiles computations

  std::vector<int> real_quartiles_of_random_vect {23, 39, 42};

  std::vector<double> comp_quartiles_of_random_vect
      = utils::quantile(random_vect.begin(), random_vect.end(), 4);

  ASSERT_TRUE(utils::comp_all(
      real_quartiles_of_random_vect.begin(),
      real_quartiles_of_random_vect.end(),
      comp_quartiles_of_random_vect.begin(), comp_quartiles_of_random_vect.end()
  ));

  std::vector<double> comp_quartiles_of_zeros_vect
      = utils::quantile(zeros_vect.begin(), zeros_vect.end(), 4);

  std::vector<int> real_quartiles_of_zeros_vect {0, 0, 0};

  ASSERT_TRUE(utils::comp_all(
      real_quartiles_of_zeros_vect.begin(), real_quartiles_of_zeros_vect.end(),
      comp_quartiles_of_zeros_vect.begin(), comp_quartiles_of_zeros_vect.end())
  );

  // Test median
  auto random_vect_sorted = random_vect;
  std::sort(random_vect_sorted.begin(), random_vect_sorted.end());
  const auto median_it1 =
      random_vect_sorted.begin() + random_vect_sorted.size() / 2 - 1;
  const auto median_it2 =
      random_vect_sorted.begin() + random_vect_sorted.size() / 2;
  auto median_random_vect_value = (random_vect_sorted.size() % 2 == 0) ?
                                  (*median_it1 + *median_it2) / 2 : *median_it2;
  ASSERT_EQ(median_random_vect_value,
            utils::quantile(random_vect.begin(), random_vect.end(), 2).front());
}

TEST_F(Utils_tests, testTemplateComparators) {
  // Doubles comparator
  ASSERT_TRUE(utils::double_equivalence(10.00000001, 10.00000001));
  ASSERT_FALSE(utils::double_equivalence(10.0000001, 10.00000001));
  ASSERT_TRUE(utils::double_equivalence(0.0 + 0.000000000001, 1.0e-12));
  ASSERT_TRUE(utils::double_equivalence(10.00000001, 10.0 + 1.0 * 10.0e-9));
  ASSERT_TRUE(utils::double_equivalence(10.00000000000000001,
                                        10.000000000000000000000245));
  ASSERT_FALSE(utils::double_equivalence(1.0001, 1.001));
  ASSERT_FALSE(utils::double_equivalence(1.01, 1.02, 0.001));
  ASSERT_TRUE(utils::double_equivalence(1.01, 1.02, 0.1));

  // Containers comparator
  ASSERT_TRUE(utils::comp_all(vect.begin(), vect.end(),
                              list.begin(), list.end()));
  ASSERT_FALSE(utils::comp_all(vect.begin() + 1, vect.end(),
                               list.begin(), list.end()));

  auto selected_values = utils::matchesValueIndexes(vect.begin(), vect.end(),
                                                    [](int x){ return x < 2; });
  std::vector<std::size_t> truth {0, 1};

  ASSERT_TRUE(utils::comp_all(selected_values.begin(), selected_values.end(),
                              truth.begin(), truth.end()));

  selected_values = utils::matchesValueIndexes(
      random_vect.begin(), random_vect.end(), [](int x){ return x == 23; });
  truth = {3};
  ASSERT_TRUE(utils::comp_all(selected_values.begin(), selected_values.end(),
                              truth.begin(), truth.end()));

  selected_values = utils::matchesValueIndexes(
      random_vect.begin(), random_vect.end(), [](int x){ return x == 1; });
  truth = {};
  ASSERT_TRUE(utils::comp_all(selected_values.begin(), selected_values.end(),
                              truth.begin(), truth.end()));
}

TEST_F(Utils_tests, testStreamTools) {
  // Stream tools
  ASSERT_EQ(6, utils::getStreamObjectSize("azerty", std::cout));
  ASSERT_EQ(1, utils::getStreamObjectSize("#", std::cout));
  ASSERT_EQ(3, utils::getStreamObjectSize("   ", std::cout));
  ASSERT_EQ(3, utils::getStreamObjectSize("   ", std::cerr));

  // writers
  std::vector<int> empty_vect {};
  std::vector<int> vect_int {1, 2, 3};
  std::vector<char> vect_charint {'1', '2', '3'};
  std::vector<std::string> vect_strint {"1", "2", "3"};

  std::string expected_result = "{1, 2, 3}";
  std::stringstream ss_vect_int;
  std::stringstream ss_vect_charint;
  std::stringstream ss_vect_strint;
  utils::write(ss_vect_int, vect_int.begin(), vect_int.end());
  utils::write(ss_vect_charint, vect_charint.begin(), vect_charint.end());
  utils::write(ss_vect_strint, vect_strint.begin(), vect_strint.end());
  ASSERT_TRUE(utils::string_comp(ss_vect_int.str(), expected_result));
  ASSERT_TRUE(utils::string_comp(ss_vect_charint.str(), expected_result));
  ASSERT_TRUE(utils::string_comp(ss_vect_strint.str(), expected_result));

  expected_result = "{}";
  std::stringstream ss_empty_vect;
  utils::write(ss_empty_vect, empty_vect.begin(), empty_vect.end());
  ASSERT_TRUE(utils::string_comp(ss_empty_vect.str(), expected_result));

  // progression bar
  Timer<int> timer(10);
  char expected_progression_print[] = "[>                   ]0%\r";
  std::stringstream observed_progression_print_sstr;
  utils::progressionBar(observed_progression_print_sstr, timer,
                        false, "", true);
  ASSERT_STREQ(expected_progression_print,
               observed_progression_print_sstr.str().c_str());

  timer.next(5);
  char expected_progression_print_50[] = "[==========>         ]50%\r";
  observed_progression_print_sstr.str(""); // reset
  utils::progressionBar(observed_progression_print_sstr, timer,
                        false, "", true);
  ASSERT_STREQ(expected_progression_print_50,
               observed_progression_print_sstr.str().c_str());

  timer.next(5);
  char expected_progression_print_100[] = "[====================]100%\r";
  observed_progression_print_sstr.str(""); // reset
  utils::progressionBar(observed_progression_print_sstr, timer,
                        false, "", true);
  ASSERT_STREQ(expected_progression_print_100,
               observed_progression_print_sstr.str().c_str());

  char expected_progression_print_none[] = "[====================]100%\r"
      "                          \r";
  observed_progression_print_sstr.str(""); // reset
  utils::progressionBar(observed_progression_print_sstr, timer,
                        false, "", true);
  RefreshablePrinter::clean(observed_progression_print_sstr);
  ASSERT_STREQ(expected_progression_print_none,
               observed_progression_print_sstr.str().c_str());
}
