//
// Created by Zhongshi Jiang on 4/11/17.
//

#include <Eigen/Core>
#include <Eigen/StdVector>
#include "tetrahedron_tetrahedron_adjacency.h"
#include "tetrahedron_tuple.h"
#include "retain_tetrahedral_adjacency.h"
#include "edge_removal.h"

#include <iostream>
#include <tuple>
#include <set>
#include <algorithm>
#include <map>
#include <list>
#include <iterator>
namespace igl {
namespace dev {

struct face_removal_neighbor_test {
  template<typename DerivedT, typename DerivedTT,
           typename DerivedTTif, typename DerivedTTie>
  std::tuple<double, double, std::list<int>, std::list<std::tuple<int, int>>>
  recurse(int ti, int fi, int ei, bool ai,
          std::vector<DerivedT> &T,
          std::vector<DerivedTT> &TT,
          std::vector<DerivedTTif> &TTif,
          std::vector<DerivedTTie> &TTie,
          int a, int b,
          const std::function<double(int, int,
                                     int, int)> &tet_quality,
          const std::function<bool(int, int,
                                   int, int)> &orient3D) {
    using namespace igl::dev;

    auto u = tet_tuple_get_vert(ti, fi, ei, ai, T, TT, TTif, TTie);
    auto w = tet_tuple_get_vert(ti, fi, ei, !ai, T, TT, TTif, TTie);

    auto q_uw = tet_quality(a, b, u, w);

    // revolve around the edge : count one-ring tet and decide non-boundary.
    bool flag = true;
    int revolving_face_num = 0;
    std::tuple<int, int, int> other_sandwiched_face;
    int f = fi, t = ti, e = ei;
    do {
      // swtch t/f
      if (tet_tuple_is_on_boundary(t, f, e, true, T, TT, TTif, TTie)) {
        flag = false;
        break;
      }
      std::tie(t, f, e) = std::make_tuple(TT[t][f], TTif[t][f],
                                          TTie[t](f, e));
      f = igl::dev::tetrahedron_local_FF(f, (e + 2) % 3);

      revolving_face_num++;
      if (revolving_face_num == 2) {
        other_sandwiched_face = std::make_tuple(t, f, e);
      }
      if (revolving_face_num > 4) {
        flag = false;
        break;
      }
    } while (t != ti);

    if(revolving_face_num != 4) flag = false;

    if (flag) {
      std::tie(t, f, e) = other_sandwiched_face;
      int v = tet_tuple_get_vert(t, f, (e + 2) % 3, true, T, TT, TTif, TTie);

      auto j_uv = orient3D(a, b, u, v);
      auto j_vw = orient3D(a, b, v, w);
      auto j_wu = orient3D(a, b, w, u);

      double o_uv, n_uv, o_vw, n_vw;

      std::list<int> h_uv, h_vw;
      std::list<std::tuple<int, int>> dt_uv, dt_vw;
      if ((int) j_uv + (int) j_vw + (int) j_wu >= 2) {
        std::tie(o_uv, n_uv, h_uv, dt_uv) =
            recurse(t, f, (e + 1) % 3, false,
                    T, TT, TTif, TTie,
                    a, b, tet_quality, orient3D);
        std::tie(o_vw, n_vw, h_vw, dt_vw) =
            recurse(t, f, (e + 2) % 3, false,
                    T, TT, TTif, TTie,
                    a, b, tet_quality, orient3D);

        auto q_old = (std::min)({tet_quality(a, u, v, w), tet_quality(u, v, w,
                                                                    b),
                               o_uv, o_vw});
        auto q_new = (std::min)(n_uv, n_vw);

        if (q_new > q_old || q_new > q_uw) {
//        one_ring_of_removal.insert(current_pos, v);
          h_uv.push_back(v);
          h_uv.splice(h_uv.end(), h_vw);
          dt_uv.emplace_back(t, f);
          dt_uv.splice(dt_uv.end(), dt_vw);
          return std::make_tuple(q_old, q_new, h_uv, dt_uv);
        }
      }
    }

    return std::make_tuple(INFINITY, q_uw, std::list<int>(),
                           std::list<std::tuple<int, int>>());
  };
};

template<typename DerivedT, typename DerivedTT,
         typename DerivedTTif, typename DerivedTTie>
bool tet_tuple_multi_face_removal(int ti,
                                  int fi,
                                  int ei,
                                  bool ai,
                                  std::function<double(int, int,
                                                       int, int)> tet_quality,
                                  std::function<bool(int, int,
                                                     int, int)> orient3D,
                                  std::vector<DerivedT> &T,
                                  std::vector<DerivedTT> &TT,
                                  std::vector<DerivedTTif> &TTif,
                                  std::vector<DerivedTTie> &TTie,
                                  std::vector<int> &new_tets_id) {

  if (igl::dev::tet_tuple_is_on_boundary(ti, fi, ei, ai,
                                         T, TT, TTif, TTie))
    return false;

  int apex_a, apex_b;
  double q_old = tet_quality(T[ti](0), T[ti](1), T[ti](2), T[ti](3));
  double q_new = INFINITY;
  {
    // get apex_a: switch f/e/v
    auto t = ti, f = fi, e = ei;
    auto a = true;
    igl::dev::tet_tuple_switch_face(t, f, e, a, T, TT, TTif, TTie);
    igl::dev::tet_tuple_switch_edge(t, f, e, a, T, TT, TTif, TTie);
    apex_a = igl::dev::tet_tuple_get_vert(t, f, e, !a,
                                          T, TT, TTif, TTie);
  }
  {
    auto t = ti, f = fi, e = ei;
    auto a = true;
    igl::dev::tet_tuple_switch_tet(t, f, e, a, T, TT, TTif, TTie);
    igl::dev::tet_tuple_switch_face(t, f, e, a, T, TT, TTif, TTie);
    igl::dev::tet_tuple_switch_edge(t, f, e, a, T, TT, TTif, TTie);
    apex_b = igl::dev::tet_tuple_get_vert(t, f, e, !a,
                                          T, TT, TTif, TTie);
    q_old = (std::min)(q_old, tet_quality(T[t](0), T[t](1), T[t](2), T[t](3)));
  }

  assert(q_old > 0);

  face_removal_neighbor_test neighbor_test;
  std::list<int> polygon;
  std::list<std::tuple<int, int>> delete_faces{std::make_tuple(ti, fi)};;
  for (auto e:{0, 1, 2}) {
    std::list<int> added_points;
    decltype(delete_faces) local_del_faces;
    double local_q_old, local_q_new;
    polygon.push_back(igl::dev::tet_tuple_get_vert(ti, fi, e, ai,
                                                   T, TT, TTif, TTie));
    std::tie(local_q_old, local_q_new, added_points, local_del_faces) =
        neighbor_test.recurse(ti, fi, e, ai, T, TT, TTif, TTie, apex_a, apex_b,
                              tet_quality, orient3D);

    q_old = (std::min)(q_old, local_q_old);
    q_new = (std::min)(q_new, local_q_new);
    polygon.splice(polygon.end(), added_points);
    delete_faces.splice(delete_faces.end(), local_del_faces);
  }
  if (q_new <= q_old)
    return false;
  if (q_new < 0) {
//    std::cout<<"Warning q_new < 0 in Multiface removal"<<std::endl;
    return false;
  }
  // delete those sandwiched between a/b
  std::set<int> delete_tets;
  std::set<int> influence_id;
  for (auto tf:delete_faces) {
    int t, f;
    std::tie(t, f) = tf;
    delete_tets.insert(t);
    delete_tets.insert(TT[t][f]);
  }

  for (auto t:delete_tets)
    for (auto f:{0, 1, 2, 3}) influence_id.insert(TT[t][f]);
  influence_id.erase(-1);

  std::vector<Eigen::RowVector4i> new_tets;
  auto p = std::rbegin(polygon);
  auto q = std::next(p);
  for (; q != std::rend(polygon); ++p, ++q) {
    new_tets.emplace_back(apex_a, apex_b, *p, *q);
  }
  if (!new_tets.empty())
    new_tets.emplace_back(apex_a, apex_b, *p, *std::rbegin(polygon));

  std::set<int> surround_id;
  std::set_difference(influence_id.begin(), influence_id.end(),
                      delete_tets.begin(), delete_tets.end(),
                      std::inserter(surround_id, surround_id.begin()));

  // additional procedure for the recomputation of new tets.
  // is actually reusable for retain_tet_adj. But not much.
  new_tets_id.clear();
  new_tets_id.insert(new_tets_id.end(), delete_tets.begin(), delete_tets.end());
  for (int i = 0, loop_num = new_tets.size() - delete_tets.size(); i < loop_num;
       i++)
    new_tets_id.push_back(i + T.size());
  if (delete_tets.size() > new_tets.size()) new_tets_id.resize(new_tets.size());

#ifndef NDEBUG
  {
    double qq = INFINITY;
    for (auto r:new_tets) {
      double new_qq = tet_quality(r[0], r[1], r[2], r[3]);
      if (qq > new_qq)
        qq = new_qq;
    }
    assert(qq == q_new);
  }
  assert(q_new > 0);

#endif
  retain_tetrahedral_adjacency(delete_tets, surround_id, new_tets, T, TT, TTif,
                               TTie);
#ifndef NDEBUG
  {
    double qq = INFINITY;
    for (auto i:new_tets_id) {
      auto r = T[i];
      double new_qq = tet_quality(r[0], r[1], r[2], r[3]);
      if (qq > new_qq)
        qq = new_qq;
    }
    assert(qq == q_new);
  }
#endif
  return true;
};

template<typename DerivedT, typename DerivedTT,
         typename DerivedTTif, typename DerivedTTie>
bool tet_tuple_multi_face_removal_force(int ti,
                                        int fi,
                                        int ei,
                                        bool ai,
                                        std::function<double(int,
                                                             int,
                                                             int,
                                                             int)> tet_quality,
                                        std::function<bool(int, int,
                                                           int, int)> orient3D,
                                        std::vector<DerivedT> &T,
                                        std::vector<DerivedTT> &TT,
                                        std::vector<DerivedTTif> &TTif,
                                        std::vector<DerivedTTie> &TTie) {

  if (igl::dev::tet_tuple_is_on_boundary(ti, fi, ei, ai,
                                         T, TT, TTif, TTie))
    return false;

  int apex_a, apex_b;
  double q_old = tet_quality(T[ti](0), T[ti](1), T[ti](2), T[ti](3));
  double q_new = INFINITY;
  {
    // get apex_a: switch f/e/v
    auto t = ti, f = fi, e = ei;
    auto a = true;
    igl::dev::tet_tuple_switch_face(t, f, e, a, T, TT, TTif, TTie);
    igl::dev::tet_tuple_switch_edge(t, f, e, a, T, TT, TTif, TTie);
    apex_a = igl::dev::tet_tuple_get_vert(t, f, e, !a,
                                          T, TT, TTif, TTie);
  }
  {
    auto t = ti, f = fi, e = ei;
    auto a = true;
    igl::dev::tet_tuple_switch_tet(t, f, e, a, T, TT, TTif, TTie);
    igl::dev::tet_tuple_switch_face(t, f, e, a, T, TT, TTif, TTie);
    igl::dev::tet_tuple_switch_edge(t, f, e, a, T, TT, TTif, TTie);
    apex_b = igl::dev::tet_tuple_get_vert(t, f, e, !a,
                                          T, TT, TTif, TTie);
    q_old = (std::min)(q_old, tet_quality(T[t](0), T[t](1), T[t](2), T[t](3)));
  }

  assert(q_old > 0);

  face_removal_neighbor_test neighbor_test;
  std::list<int> polygon;
  std::list<std::tuple<int, int>> delete_faces{std::make_tuple(ti, fi)};;
  for (auto e:{0, 1, 2}) {
    std::list<int> added_points;
    decltype(delete_faces) local_del_faces;
    double local_q_old, local_q_new;
    polygon.push_back(igl::dev::tet_tuple_get_vert(ti, fi, e, ai,
                                                   T, TT, TTif, TTie));
    std::tie(local_q_old, local_q_new, added_points, local_del_faces) =
        neighbor_test.recurse(ti, fi, e, ai, T, TT, TTif, TTie, apex_a, apex_b,
                              tet_quality, orient3D);

    q_old = (std::min)(q_old, local_q_old);
    q_new = (std::min)(q_new, local_q_new);
    polygon.splice(polygon.end(), added_points);
    delete_faces.splice(delete_faces.end(), local_del_faces);
  }
  // delete those sandwiched between a/b
  std::set<int> delete_tets;
  std::set<int> influence_id;
  for (auto tf:delete_faces) {
    int t, f;
    std::tie(t, f) = tf;
    delete_tets.insert(t);
    delete_tets.insert(TT[t][f]);
  }

  for (auto t:delete_tets)
    for (auto f:{0, 1, 2, 3}) influence_id.insert(TT[t][f]);
  influence_id.erase(-1);

  std::vector<Eigen::RowVector4i> new_tets;
  auto p = std::rbegin(polygon);
  auto q = std::next(p);
  for (; q != std::rend(polygon); ++p, ++q) {
    new_tets.emplace_back(apex_a, apex_b, *p, *q);
  }
  if (!new_tets.empty())
    new_tets.emplace_back(apex_a, apex_b, *p, *std::rbegin(polygon));

  std::set<int> surround_id;
  std::set_difference(influence_id.begin(), influence_id.end(),
                      delete_tets.begin(), delete_tets.end(),
                      std::inserter(surround_id, surround_id.begin()));

#ifndef NDEBUG
  {
    double qq = INFINITY;
    for (auto r:new_tets) {
      double new_qq = tet_quality(r[0], r[1], r[2], r[3]);
      if (qq > new_qq)
        qq = new_qq;
    }
    assert(qq == q_new);
  }
  assert(q_new >= 0);

#endif
  retain_tetrahedral_adjacency(delete_tets, surround_id, new_tets, T, TT, TTif,
                               TTie);
  return true;
};

}
}

#ifdef IGL_STATIC_LIBRARY
template bool igl::dev::tet_tuple_multi_face_removal<Eigen::Matrix<int, 1, 4,
                                                                   1, 1, 4>,
                                                     Eigen::Matrix<int,
                                                                   1,
                                                                   4,
                                                                   1,
                                                                   1,
                                                                   4>,
                                                     Eigen::Matrix<int,
                                                                   1,
                                                                   4,
                                                                   1,
                                                                   1,
                                                                   4>,
                                                     Eigen::Matrix<int,
                                                                   4,
                                                                   3,
                                                                   0,
                                                                   4,
                                                                   3> >(int,
                                                                        int,
                                                                        int,
                                                                        bool,
                                                                        std::function<
                                                                            double(
                                                                                int,
                                                                                int,
                                                                                int,
                                                                                int)>,
                                                                        std::function<
                                                                            bool(
                                                                                int,
                                                                                int,
                                                                                int,
                                                                                int)>,
                                                                        std::vector<
                                                                            Eigen::Matrix<
                                                                                int,
                                                                                1,
                                                                                4,
                                                                                1,
                                                                                1,
                                                                                4>,
                                                                            std::allocator<
                                                                                Eigen::Matrix<
                                                                                int,
                                                                                1,
                                                                                4,
                                                                                1,
                                                                                1,
                                                                                4> > > &,
                                                                        std::vector<
                                                                            Eigen::Matrix<
                                                                                int,
                                                                                1,
                                                                                4,
                                                                                1,
                                                                                1,
                                                                                4>,
                                                                            std::allocator<
                                                                                Eigen::Matrix<
                                                                                int,
                                                                                1,
                                                                                4,
                                                                                1,
                                                                                1,
                                                                                4> > > &,
                                                                        std::vector<
                                                                            Eigen::Matrix<
                                                                                int,
                                                                                1,
                                                                                4,
                                                                                1,
                                                                                1,
                                                                                4>,
                                                                            std::allocator<
                                                                                Eigen::Matrix<
                                                                                int,
                                                                                1,
                                                                                4,
                                                                                1,
                                                                                1,
                                                                                4> > > &,
                                                                        std::vector<
                                                                            Eigen::Matrix<
                                                                                int,
                                                                                4,
                                                                                3,
                                                                                0,
                                                                                4,
                                                                                3>,
                                                                            std::allocator<
                                                                                Eigen::Matrix<
                                                                                int,
                                                                                4,
                                                                                3,
                                                                                0,
                                                                                4,
                                                                                3> > > &,
                                                                        std::vector<
                                                                            int,
                                                                            std::allocator<
                                                                                int> > &);

template bool igl::dev::tet_tuple_multi_face_removal_force<Eigen::Matrix<int,
                                                                         1,
                                                                         4,
                                                                         1,
                                                                         1,
                                                                         4>,
                                                           Eigen::Matrix<int,
                                                                         1,
                                                                         4,
                                                                         1,
                                                                         1,
                                                                         4>,
                                                           Eigen::Matrix<int,
                                                                         1,
                                                                         4,
                                                                         1,
                                                                         1,
                                                                         4>,
                                                           Eigen::Matrix<int,
                                                                         4,
                                                                         3,
                                                                         0,
                                                                         4,
                                                                         3> >(
    int,
    int,
    int,
    bool,
    std::function<double(int, int, int, int)>,
    std::function<bool(int, int, int, int)>,
    std::vector<Eigen::Matrix<int, 1, 4, 1, 1, 4>,
                     std::allocator<Eigen::Matrix<int,
                                                       1,
                                                       4,
                                                       1,
                                                       1,
                                                       4> > > &,
    std::vector<Eigen::Matrix<int, 1, 4, 1, 1, 4>,
                     std::allocator<Eigen::Matrix<int,
                                                       1,
                                                       4,
                                                       1,
                                                       1,
                                                       4> > > &,
    std::vector<Eigen::Matrix<int, 1, 4, 1, 1, 4>,
                     std::allocator<Eigen::Matrix<int,
                                                       1,
                                                       4,
                                                       1,
                                                       1,
                                                       4> > > &,
    std::vector<Eigen::Matrix<int, 4, 3, 0, 4, 3>,
                     std::allocator<Eigen::Matrix<int,
                                                       4,
                                                       3,
                                                       0,
                                                       4,
                                                       3> > > &);

#endif


#include "fight.h"
#include "gamedata.h"
#include "rng.h"
#include "util.h"

#include <algorithm>
#include <boost/assert.hpp>
#include <iostream>

namespace nibashared {

void magic_ex::heatup() { cd = real_magic.cd; }
void magic_ex::cooldown(int ticks) { cd = std::max(cd - ticks, 0); }

int fightable::ticks_calc() const {
    if (progress >= FIGHT_MAX_PROG)
        return 0;
    // this rounds up
    // for example, the current progress is 649, speed is 35
    // (1000 - 649 + 35 - 1) / 35 = 11 ticks to finish the 351 progress
    // say the progress is now 651, speed is still 35
    // (1000 - 651 + 35 - 1) / 35 = 10 ticks to finish the 349 progress
    return (FIGHT_MAX_PROG - progress + char_data.stats.speed - 1) / char_data.stats.speed;
}

template<typename Rng>
int fightable::damage_calc(const magic &chosen_magic, const fightable &defender, Rng &rng) const {
    int accuracy =
        static_cast<int>(char_data.stats.accuracy *
                         (100.0 / (char_data.stats.accuracy + defender.char_data.stats.accuracy)));
    if (accuracy <= rng(0, 99)) {
        CPRINT("the attack was evaded!");
        return 0;
    }

    int physical_damage = rng(char_data.stats.attack_min, char_data.stats.attack_max);
    double physical_damage_multiplier = chosen_magic.multiplier / 100.0;
    double physical_damage_reduction =
        defender.char_data.stats.defence / (defender.char_data.stats.defence + DEFENCE_EXTENSION);

    int inner_damage{chosen_magic.inner_damage};
    double inner_damage_multiplier = 1 + char_data.stats.inner_power / INNER_BASE;
    double inner_damage_reduction;
    property inner_property{chosen_magic.inner_property};
    switch (inner_property) {
    case property::gold:
        inner_damage_reduction = defender.char_data.stats.gold_res / 100.0;
        break;
    case property::wood:
        inner_damage_reduction = defender.char_data.stats.wood_res / 100.0;
        break;
    case property::water:
        inner_damage_reduction = defender.char_data.stats.water_res / 100.0;
        break;
    case property::fire:
        inner_damage_reduction = defender.char_data.stats.fire_res / 100.0;
        break;
    case property::earth:
        inner_damage_reduction = defender.char_data.stats.earth_res / 100.0;
        break;
    default:
        BOOST_ASSERT(inner_property);
        inner_damage_reduction = 1.0;
        break;
    }
    CPRINT("the attack landed!")
    CPRINT("physical " << physical_damage << " " << physical_damage_multiplier << " "
                       << physical_damage_reduction);
    CPRINT("inner " << inner_damage << " " << inner_damage_multiplier << " "
                    << inner_damage_reduction);
    return static_cast<int>(physical_damage * physical_damage_multiplier *
                                (1 - physical_damage_reduction) +
                            inner_damage * inner_damage_multiplier * (1 - inner_damage_reduction));
}

template int fightable::damage_calc<rng_client>(const magic &, const fightable &,
                                                rng_client &) const;
template int fightable::damage_calc<rng_server>(const magic &, const fightable &,
                                                rng_server &) const;

int fightable::threat_calc() const {
    return char_data.stats.attack_min + char_data.stats.attack_max;
}

// note that return also makes sure the real magic is there if has value
std::optional<std::size_t> fightable::pick_magic_idx() {
    // potential optimization: precompute the sequence of magics
    for (std::size_t i = 0; i < magics.size(); ++i) {
        if (magics[i].cd == 0 && magics[i].real_magic.mp_cost < char_data.stats.mp) {
            return i;
        }
    }
    return {};
}

fight::fight(std::vector<fightable> &&friends, std::vector<fightable> &&enemies) {
    friends_ = friends.size();
    enemies_ = enemies.size();
    nibautil::for_each(friends, [](auto &f) { f.team = 0; });
    nibautil::for_each(enemies, [](auto &f) { f.team = 1; });
    std::move(friends.begin(), friends.end(), std::back_inserter(all_));
    std::move(enemies.begin(), enemies.end(), std::back_inserter(all_));
    for (std::size_t i = 0; i < all_.size(); ++i) {
        all_.at(i).idx = i;
    }
    // sort by speed, use the 'arbitrary' idx to break tie
    std::sort(all_.begin(), all_.end(), [](const auto &lhs, const auto &rhs) {
        return std::tie(lhs.char_data.stats.speed, lhs.idx) <
               std::tie(rhs.char_data.stats.speed, rhs.idx);
    });
    for (std::size_t i = 0; i < all_.size(); ++i) {
        all_.at(i).idx = i;
    }
}

template<typename Rng>
int fight::go(Rng &rng) {
    CPRINT("fight begins" << std::endl);
    std::size_t team_alive_count[]{friends_, enemies_};
    std::vector<std::size_t> alive;
    alive.reserve(all_.size());
    for (;;) {
        if (team_alive_count[0] == 0 || team_alive_count[1] == 0)
            break;
        // all alive indices
        for (std::size_t i = 0; i < all_.size(); ++i) {
            if (all_.at(i).char_data.stats.hp <= 0) {
                continue;
            }
            alive.push_back(i);
        }

        using tick_idx_p = std::pair<int, std::size_t>;
        // find the person that is the fastest to act
        tick_idx_p min_tick{all_.at(alive[0]).ticks_calc(), alive[0]};
        // figure out the ticks to make it to the next action
        for (std::size_t i = 1; i < alive.size(); ++i) {
            tick_idx_p tick{all_.at(alive[i]).ticks_calc(), alive[i]};
            if (tick < min_tick)
                min_tick = tick;
        }

        auto &[ticks, attacker_idx] = min_tick;
        auto &attacker = all_.at(attacker_idx);

        // keep track of elasped ticks
        elapsed_ticks_ += ticks;

        // make everyone progress and find the target, to break ties, again use idx
        using threat_idx_p = std::pair<int, std::size_t>;
        // it's unknown which idx is the first enemy
        // we use (threat=-1, idx=attacker) as a placeholder
        threat_idx_p max_threat{-1, attacker.idx};
        for (auto idx : alive) {
            auto &p = all_.at(idx);
            p.progress += p.char_data.stats.speed * ticks;
            // clang has some problems dealing with lambdas and structured bindings
            for (auto &mex : p.magics) {
                mex.cooldown(ticks);
            }
            // maybe optimization: maintain a priority list (heap) of top threats
            if (p.team != attacker.team) {
                threat_idx_p threat{p.threat_calc(), idx};
                if (threat > max_threat)
                    max_threat = threat;
            }
        }
        BOOST_ASSERT(max_threat.second != attacker.idx);
        auto &defender = all_.at(max_threat.second);

        // now pick a magic to use
        auto chosen_magic_idx = attacker.pick_magic_idx();

        // use just puts it on cd, extra + 1
        int dmg = 0;
        if (chosen_magic_idx) {
            auto &chosen_magicex = attacker.magics[(*chosen_magic_idx)];
            chosen_magicex.heatup();
            dmg = attacker.damage_calc(chosen_magicex.real_magic, defender, rng);
            attacker.char_data.stats.mp -= chosen_magicex.real_magic.mp_cost;
            CPRINT(attacker.char_data.name << " uses magic " << *chosen_magic_idx
                                           << chosen_magicex.real_magic.name << " dealt " << dmg
                                           << " on " << defender.char_data.name);
        } else {
            dmg = attacker.damage_calc(DEFAULT_MAGIC, defender, rng);
            CPRINT(attacker.char_data.name << " uses magic " << DEFAULT_MAGIC.name << " dealt "
                                           << dmg << " on " << defender.char_data.name);
        }

        defender.char_data.stats.hp -= dmg;

        CPRINT(defender.char_data.name << " hp remains " << defender.char_data.stats.hp
                                       << std::endl);

        if (defender.char_data.stats.hp <= 0) {
            team_alive_count[defender.team] -= 1;
        }
        attacker.progress = 0;
        alive.clear();
    }
    CPRINT(team_alive_count[0] << " " << team_alive_count[1]);
    return team_alive_count[1] == 0 ? 0 : 1;
}

template int fight::go<rng_client>(rng_client &rng);
template int fight::go<rng_server>(rng_server &rng);

int fight::elapsed_ticks() const { return elapsed_ticks_; }

fightable &fight::my_status() { return all_.at(0); }

// will move this elsewhere
battlestats stats_computer(const attributes &attr) {
    battlestats stats{};
    stats.attack_min = attr.strength * 2;
    stats.attack_max = attr.strength * 2;
    stats.evasion = attr.dexterity;
    stats.speed = attr.dexterity;
    stats.accuracy = attr.dexterity;
    stats.hp = 5 * attr.physique;
    stats.defence = attr.physique;
    stats.mp = 5 * attr.spirit;
    stats.inner_power = attr.spirit;
    return stats; // RVO
}

fightable setup_self(nibashared::character &&raw_character,
                     const std::vector<nibashared::magic> &magics,
                     const std::vector<nibashared::equipment> &equips,
                     const std::vector<int> &magic_ids) {
    fightable fight_character;
    fight_character.char_data = std::move(raw_character);
    fight_character.char_data.stats += stats_computer(fight_character.char_data.attrs);
    for (const auto &magic : magics) {
        if (std::find(magic_ids.begin(), magic_ids.end(), magic.magic_id) == magic_ids.end()) {
            continue;
        }
        CPRINT(fight_character.char_data.name << " equipped magic " << magic.name);
        fight_character.char_data.stats += magic.stats;
        // NOTE, aggregation initialization is copy... well fine
        // also push_back is complaining designated initialization for reasons I don't understand
        fight_character.magics.emplace_back(magic_ex{.cd = 0, .real_magic = magic});
    }
    for (const auto &equipment : equips) {
        CPRINT(fight_character.char_data.name << " has item " << equipment.name);
        fight_character.char_data.stats += equipment.stats;
    }
    return fight_character; // RVO
}

fightable setup_fightable(int id) {
    auto raw_character = getdata().at<character>(id);
    std::vector<nibashared::magic> magics;
    std::vector<nibashared::equipment> equips;
    std::vector<int> magic_ids;
    nibautil::for_each(raw_character.active_magic, [&magics, &magic_ids](auto &magic_id) {
        magic_ids.push_back(magic_id);
        magics.push_back(getdata().at<magic>(magic_id));
    });
    nibautil::for_each(raw_character.equipments, [&equips](auto &equip_id) {
        equips.push_back(getdata().at<equipment>(equip_id));
    });
    // std::cout << magics << std::endl;
    // std::cout << equips << std::endl;
    return setup_self(std::move(raw_character), magics, equips, magic_ids);
}

std::pair<std::vector<fightable>, std::vector<fightable>>
prep_fight(nibashared::sessionstate &session, int id_you) {
    CPRINT("prep " << (*(session.player)).name << " " << id_you);
    std::pair<std::vector<fightable>, std::vector<fightable>> ret;
    auto &magic_ids = session.data.equipped_magic_ids;
    // copy player data into raw_character
    nibashared::character raw_character{.name = (*(session.player)).name,
                                        .character_id = -1,
                                        .description = {},
                                        .attrs = (*(session.player)).attrs,
                                        // leave these 3 blank
                                        .stats = {},
                                        .equipments = {},
                                        .active_magic = {}};
    ret.first.push_back(
        setup_self(std::move(raw_character), session.data.magics, session.data.equips, magic_ids));
    ret.second.push_back(setup_fightable(id_you));
    return ret; // RVO
}

} // namespace nibashared


/*
 Copyright (C) 2016 Quaternion Risk Management Ltd
 All rights reserved.

 This file is part of ORE, a free-software/open-source library
 for transparent pricing and risk analysis - http://opensourcerisk.org

 ORE is free software: you can redistribute it and/or modify it
 under the terms of the Modified BSD License.  You should have received a
 copy of the license along with this program.
 The license is also available online at <http://opensourcerisk.org>

 This program is distributed on the basis that it will form a useful
 contribution to risk analytics and model standardisation, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
*/

#include "swaptionmarketdata.hpp"
#include "toplevelfixture.hpp"
#include "yieldcurvemarketdata.hpp"
#include <boost/test/unit_test.hpp>

#include <qle/termstructures/swaptionvolatilityconverter.hpp>
#include <qle/termstructures/swaptionvolcube2.hpp>

#include <ql/indexes/swap/euriborswap.hpp>
#include <ql/pricingengines/blackformula.hpp>

#include <boost/assign/list_of.hpp>

using namespace QuantExt;
using namespace QuantLib;
using namespace boost::unit_test_framework;
using namespace boost::assign;

namespace {
// Variables to be used in the test
struct CommonVars {
    // Constructor
    CommonVars() : referenceDate(5, Feb, 2016) {
        // Reference date
        Settings::instance().evaluationDate() = referenceDate;

        // Link ibor index to correct forward curve
        boost::shared_ptr<IborIndex> iborIndex = conventions.floatIndex->clone(yieldCurves.forward6M);

        // Create underlying swap conventions
        swapConventions = boost::make_shared<SwapConventions>(conventions.settlementDays, conventions.fixedTenor,
                                                              conventions.fixedCalendar, conventions.fixedConvention,
                                                              conventions.fixedDayCounter, iborIndex);

        // Set up the various swaption matrices
        atmNormalVolMatrix = boost::make_shared<SwaptionVolatilityMatrix>(
            referenceDate, conventions.fixedCalendar, conventions.fixedConvention, atmVols.optionTenors,
            atmVols.swapTenors, atmVols.nVols, Actual365Fixed(), true, Normal);

        atmLogNormalVolMatrix = boost::make_shared<SwaptionVolatilityMatrix>(
            referenceDate, conventions.fixedCalendar, conventions.fixedConvention, atmVols.optionTenors,
            atmVols.swapTenors, atmVols.lnVols, Actual365Fixed(), true, ShiftedLognormal);

        // boost::make_shared can only handle 9 parameters
        atmShiftedLogNormalVolMatrix_1 = boost::shared_ptr<SwaptionVolatilityMatrix>(new SwaptionVolatilityMatrix(
            referenceDate, conventions.fixedCalendar, conventions.fixedConvention, atmVols.optionTenors,
            atmVols.swapTenors, atmVols.slnVols_1, Actual365Fixed(), true, ShiftedLognormal, atmVols.shifts_1));

        atmShiftedLogNormalVolMatrix_2 = boost::shared_ptr<SwaptionVolatilityMatrix>(new SwaptionVolatilityMatrix(
            referenceDate, conventions.fixedCalendar, conventions.fixedConvention, atmVols.optionTenors,
            atmVols.swapTenors, atmVols.slnVols_2, Actual365Fixed(), true, ShiftedLognormal, atmVols.shifts_2));
    }

    // Members
    Date referenceDate;
    SwaptionConventionsEUR conventions;
    SwaptionVolatilityEUR atmVols;
    YieldCurveEUR yieldCurves;
    boost::shared_ptr<SwapConventions> swapConventions;
    boost::shared_ptr<SwaptionVolatilityStructure> atmNormalVolMatrix;
    boost::shared_ptr<SwaptionVolatilityStructure> atmLogNormalVolMatrix;
    boost::shared_ptr<SwaptionVolatilityStructure> atmShiftedLogNormalVolMatrix_1;
    boost::shared_ptr<SwaptionVolatilityStructure> atmShiftedLogNormalVolMatrix_2;
    SavedSettings backup;
};
} // namespace

BOOST_FIXTURE_TEST_SUITE(QuantExtTestSuite, qle::test::TopLevelFixture)

BOOST_AUTO_TEST_SUITE(SwaptionVolatilityConverterTest)

BOOST_AUTO_TEST_CASE(testNormalToLognormal) {
    BOOST_TEST_MESSAGE("Testing conversion of swaption vols from normal to lognormal...");

    CommonVars vars;

    // Tolerance used in boost check
    Real tolerance = 0.00001;

    // Set up the converter (Normal -> Lognormal with no shifts)
    SwaptionVolatilityConverter converter(vars.referenceDate, vars.atmNormalVolMatrix, vars.yieldCurves.discountEonia,
                                          vars.yieldCurves.discountEonia, vars.swapConventions, vars.swapConventions,
                                          1 * Years, 30 * Years, ShiftedLognormal);

    // Get back converted volatility structure and test result on pillar points
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();
    Volatility targetVol = 0.0;
    Volatility outVol = 0.0;
    Real dummyStrike = 0.0;
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            Period optionTenor = vars.atmVols.optionTenors[i];
            Period swapTenor = vars.atmVols.swapTenors[j];
            targetVol = vars.atmVols.lnVols[i][j];
            outVol = convertedsvs->volatility(optionTenor, swapTenor, dummyStrike);
            BOOST_CHECK_SMALL(outVol - targetVol, tolerance);
        }
    }
}

BOOST_AUTO_TEST_CASE(testLognormalToNormal) {
    BOOST_TEST_MESSAGE("Testing conversion of swaption vols from lognormal to normal...");

    CommonVars vars;

    // Tolerance used in boost check
    Real tolerance = 0.00001;

    // Set up the converter (Lognormal with no shifts -> Normal)
    SwaptionVolatilityConverter converter(vars.referenceDate, vars.atmLogNormalVolMatrix,
                                          vars.yieldCurves.discountEonia, vars.yieldCurves.discountEonia,
                                          vars.swapConventions, vars.swapConventions, 1 * Years, 30 * Years, Normal);

    // Get back converted volatility structure and test result on pillar points
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();
    Volatility targetVol = 0.0;
    Volatility outVol = 0.0;
    Real dummyStrike = 0.0;
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            Period optionTenor = vars.atmVols.optionTenors[i];
            Period swapTenor = vars.atmVols.swapTenors[j];
            targetVol = vars.atmVols.nVols[i][j];
            outVol = convertedsvs->volatility(optionTenor, swapTenor, dummyStrike);
            BOOST_CHECK_SMALL(outVol - targetVol, tolerance);
        }
    }
}

BOOST_AUTO_TEST_CASE(testNormalToShiftedLognormal) {
    BOOST_TEST_MESSAGE("Testing conversion of swaption vols from normal to shifted lognormal...");

    CommonVars vars;

    // Tolerance used in boost check
    Real tolerance = 0.00001;

    // Set up the converter (Normal -> Shifted Lognormal with shift set 1)
    SwaptionVolatilityConverter converter(vars.referenceDate, vars.atmNormalVolMatrix, vars.yieldCurves.discountEonia,
                                          vars.yieldCurves.discountEonia, vars.swapConventions, vars.swapConventions,
                                          1 * Years, 30 * Years, ShiftedLognormal, vars.atmVols.shifts_1);

    // Get back converted volatility structure and test result on pillar points
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();
    Volatility targetVol = 0.0;
    Volatility outVol = 0.0;
    Real dummyStrike = 0.0;
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            Period optionTenor = vars.atmVols.optionTenors[i];
            Period swapTenor = vars.atmVols.swapTenors[j];
            targetVol = vars.atmVols.slnVols_1[i][j];
            outVol = convertedsvs->volatility(optionTenor, swapTenor, dummyStrike);
            BOOST_CHECK_SMALL(outVol - targetVol, tolerance);
        }
    }
}

BOOST_AUTO_TEST_CASE(testShiftedLognormalToShiftedLognormal) {
    BOOST_TEST_MESSAGE("Testing conversion of swaption vols from shifted lognormal to shifted lognormal...");

    CommonVars vars;

    // Tolerance used in boost check
    Real tolerance = 0.00001;

    // Set up the converter (Normal -> Shifted Lognormal with shift set 1)
    SwaptionVolatilityConverter converter(vars.referenceDate, vars.atmShiftedLogNormalVolMatrix_1,
                                          vars.yieldCurves.discountEonia, vars.yieldCurves.discountEonia,
                                          vars.swapConventions, vars.swapConventions, 1 * Years, 30 * Years,
                                          ShiftedLognormal, vars.atmVols.shifts_2);

    // Get back converted volatility structure and test result on pillar points
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();
    Volatility targetVol = 0.0;
    Volatility outVol = 0.0;
    Real dummyStrike = 0.0;
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            Period optionTenor = vars.atmVols.optionTenors[i];
            Period swapTenor = vars.atmVols.swapTenors[j];
            targetVol = vars.atmVols.slnVols_2[i][j];
            outVol = convertedsvs->volatility(optionTenor, swapTenor, dummyStrike);
            BOOST_CHECK_SMALL(outVol - targetVol, tolerance);
        }
    }
}

BOOST_AUTO_TEST_CASE(testShiftedLognormalToNormal) {
    BOOST_TEST_MESSAGE("Testing conversion of swaption vols from shifted lognormal to normal...");

    CommonVars vars;

    // Tolerance used in boost check
    Real tolerance = 0.00001;

    // Set up the converter (Shifted Lognormal with shift set 2 -> Normal)
    SwaptionVolatilityConverter converter(vars.referenceDate, vars.atmShiftedLogNormalVolMatrix_2,
                                          vars.yieldCurves.discountEonia, vars.yieldCurves.discountEonia,
                                          vars.swapConventions, vars.swapConventions, 1 * Years, 30 * Years, Normal);

    // Get back converted volatility structure and test result on pillar points
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();
    Volatility targetVol = 0.0;
    Volatility outVol = 0.0;
    Real dummyStrike = 0.0;
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            Period optionTenor = vars.atmVols.optionTenors[i];
            Period swapTenor = vars.atmVols.swapTenors[j];
            targetVol = vars.atmVols.nVols[i][j];
            outVol = convertedsvs->volatility(optionTenor, swapTenor, dummyStrike);
            BOOST_CHECK_SMALL(outVol - targetVol, tolerance);
        }
    }
}

BOOST_AUTO_TEST_CASE(testFailureImplyingVol) {
    BOOST_TEST_MESSAGE("Testing failure to imply lognormal vol from normal vol...");

    CommonVars vars;

    // Normal volatility matrix where we cannot imply lognormal vol at 3M x 1Y point
    vector<Period> optionTenors = list_of(Period(3, Months))(Period(1, Years));
    vector<Period> swapTenors = list_of(Period(1, Years))(Period(5, Years));
    Matrix normalVols(2, 2);
    normalVols[0][0] = 0.003340;
    normalVols[0][1] = 0.004973;
    normalVols[1][0] = 0.003543;
    normalVols[1][1] = 0.005270;

    boost::shared_ptr<SwaptionVolatilityStructure> volMatrix = boost::make_shared<SwaptionVolatilityMatrix>(
        vars.referenceDate, vars.conventions.fixedCalendar, vars.conventions.fixedConvention, optionTenors, swapTenors,
        normalVols, Actual365Fixed(), true, Normal);

    // Set up the converter (Normal -> Lognormal)
    SwaptionVolatilityConverter converter(vars.referenceDate, volMatrix, vars.yieldCurves.discountEonia,
                                          vars.yieldCurves.discountEonia, vars.swapConventions, vars.swapConventions,
                                          1 * Years, 30 * Years, ShiftedLognormal);

    // We expect the conversion to fail
    BOOST_CHECK_THROW(converter.convert(), QuantLib::Error);
}

BOOST_AUTO_TEST_CASE(testNormalShiftsIgnored) {
    BOOST_TEST_MESSAGE("Testing shifts supplied to normal converter ignored...");

    CommonVars vars;

    // Tolerance used in boost check
    Real tolerance = 0.00001;

    // Set up the converter (Lognormal with no shifts -> Normal)
    // We supply target shifts but they are ignored since target type is Normal
    SwaptionVolatilityConverter converter(
        vars.referenceDate, vars.atmLogNormalVolMatrix, vars.yieldCurves.discountEonia, vars.yieldCurves.discountEonia,
        vars.swapConventions, vars.swapConventions, 1 * Years, 30 * Years, Normal, vars.atmVols.shifts_1);

    // Get back converted volatility structure and test result on pillar points
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();
    Volatility targetVol = 0.0;
    Volatility outVol = 0.0;
    Real dummyStrike = 0.0;
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            Period optionTenor = vars.atmVols.optionTenors[i];
            Period swapTenor = vars.atmVols.swapTenors[j];
            targetVol = vars.atmVols.nVols[i][j];
            outVol = convertedsvs->volatility(optionTenor, swapTenor, dummyStrike);
            BOOST_CHECK_SMALL(outVol - targetVol, tolerance);
        }
    }
}

BOOST_AUTO_TEST_CASE(testConstructionFromSwapIndex) {
    BOOST_TEST_MESSAGE("Testing construction of SwaptionVolatilityConverter from SwapIndex...");

    CommonVars vars;

    // Tolerance used in boost check
    Real tolerance = 0.00001;

    // Set up a SwapIndex
    boost::shared_ptr<SwapIndex> swapIndex =
        boost::make_shared<EuriborSwapIsdaFixA>(2 * Years, vars.yieldCurves.forward6M, vars.yieldCurves.discountEonia);

    // Set up the converter using swap index (Shifted Lognormal with shift set 2 -> Normal)
    SwaptionVolatilityConverter converter(vars.referenceDate, vars.atmShiftedLogNormalVolMatrix_2, swapIndex, swapIndex,
                                          Normal);

    // Test that the results are still ok
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();
    Volatility targetVol = 0.0;
    Volatility outVol = 0.0;
    Real dummyStrike = 0.0;
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            Period optionTenor = vars.atmVols.optionTenors[i];
            Period swapTenor = vars.atmVols.swapTenors[j];
            targetVol = vars.atmVols.nVols[i][j];
            outVol = convertedsvs->volatility(optionTenor, swapTenor, dummyStrike);
            BOOST_CHECK_SMALL(outVol - targetVol, tolerance);
        }
    }
}

BOOST_AUTO_TEST_CASE(testConstructionFromSwapIndexNoDiscount) {
    BOOST_TEST_MESSAGE("Testing construction from SwapIndex with no exogenous discount curve...");

    CommonVars vars;

    // Set up a SwapIndex
    boost::shared_ptr<SwapIndex> swapIndex =
        boost::make_shared<EuriborSwapIsdaFixA>(2 * Years, vars.yieldCurves.forward6M);

    // Set up the converter using swap index (Shifted Lognormal with shift set 2 -> Normal)
    SwaptionVolatilityConverter converter(vars.referenceDate, vars.atmShiftedLogNormalVolMatrix_2, swapIndex, swapIndex,
                                          Normal);

    // Test that calling convert() still works
    BOOST_CHECK_NO_THROW(converter.convert());
}

BOOST_AUTO_TEST_CASE(testCube) {
    BOOST_TEST_MESSAGE("Testing lognormal to normal conversion for cube...");

    CommonVars vars;

    // Set up Swap Indices
    boost::shared_ptr<SwapIndex> shortSwapIndex =
        boost::make_shared<EuriborSwapIsdaFixA>(1 * Years, vars.yieldCurves.forward3M, vars.yieldCurves.discountEonia);
    boost::shared_ptr<SwapIndex> swapIndex =
        boost::make_shared<EuriborSwapIsdaFixA>(30 * Years, vars.yieldCurves.forward6M, vars.yieldCurves.discountEonia);

    // Set up a lognormal cube
    boost::shared_ptr<SwaptionVolatilityCube> cube = boost::make_shared<QuantExt::SwaptionVolCube2>(
        Handle<SwaptionVolatilityStructure>(vars.atmLogNormalVolMatrix), vars.atmVols.optionTenors,
        vars.atmVols.swapTenors, vars.atmVols.strikeSpreads, vars.atmVols.lnVolSpreads, swapIndex, shortSwapIndex,
        false, true);

    // Convert the cube to normal
    SwaptionVolatilityConverter converter(vars.referenceDate, cube, swapIndex, shortSwapIndex, Normal);
    boost::shared_ptr<SwaptionVolatilityStructure> convertedsvs = converter.convert();

    // Price swaptions in the lognormal and normal cube and comare their premiums
    for (Size i = 0; i < vars.atmVols.optionTenors.size(); ++i) {
        for (Size j = 0; j < vars.atmVols.swapTenors.size(); ++j) {
            for (Size k = 0; k < vars.atmVols.strikeSpreads.size(); ++k) {
                Period optionTenor = vars.atmVols.optionTenors[i];
                Period swapTenor = vars.atmVols.swapTenors[j];
                Real atmStrike = cube->atmStrike(optionTenor, swapTenor);
                Real strikeSpread = vars.atmVols.strikeSpreads[k];
                Real strike = atmStrike + strikeSpread;
                if (strike > 0.0) {
                    Real inVol = cube->volatility(optionTenor, swapTenor, atmStrike + strikeSpread);
                    Real outVol = convertedsvs->volatility(optionTenor, swapTenor, atmStrike + strikeSpread);
                    Option::Type type = strikeSpread < 0.0 ? Option::Put : Option::Call;
                    Real tte = cube->optionTimes()[i];
                    Real inPrem = blackFormula(type, strike, atmStrike, inVol * std::sqrt(tte));
                    Real outPrem = bachelierBlackFormula(type, strike, atmStrike, outVol * std::sqrt(tte));
                    BOOST_CHECK_CLOSE(inPrem, outPrem, 0.01);
                }
            }
        }
    }
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()


#pragma once

#include <boost/container/static_vector.hpp>
#include <iostream>
#include <unordered_map>

#include "haar_basis.hpp"
#include "linear_operator.hpp"
#include "orthonormal_basis.hpp"
#include "three_point_basis.hpp"
namespace Time {

/**
 *  Implementations of LinearOperator.
 */
template <typename I, typename BasisIn, typename BasisOut>
SparseVector<BasisOut> LinearOperator<I, BasisIn, BasisOut>::MatVec(
    const SparseVector<BasisIn> &vec) const {
  SparseVector<BasisOut> result;
  result.reserve(vec.size() * 2);

  for (const auto [labda_in, coeff_in] : vec)
    for (const auto [labda_out, coeff_out] : I::Column(labda_in))
      result.emplace_back(labda_out, coeff_in * coeff_out);
  result.Compress();
  return result;
}

template <typename I, typename BasisIn, typename BasisOut>
SparseVector<BasisIn> LinearOperator<I, BasisIn, BasisOut>::RMatVec(
    const SparseVector<BasisOut> &vec) const {
  SparseVector<BasisIn> result;
  result.reserve(vec.size() * 2);

  for (const auto [labda_out, coeff_out] : vec)
    for (const auto [labda_in, coeff_in] : I::Row(labda_out))
      result.emplace_back(labda_in, coeff_out * coeff_in);
  result.Compress();
  return result;
}

template <typename I, typename BasisIn, typename BasisOut>
SparseVector<BasisOut> LinearOperator<I, BasisIn, BasisOut>::MatVec(
    const SparseVector<BasisIn> &vec,
    const SparseIndices<BasisOut> &indices_out) const {
  if (vec.empty() || indices_out.empty()) return {};
  assert(indices_out.IsUnique());
  SparseVector<BasisOut> result;
  result.reserve(indices_out.size());

  vec.StoreInTree();
  for (const auto labda_out : indices_out) {
    double val = 0;
    for (const auto [labda_in, coeff_in] : I::Row(labda_out))
      if (labda_in->has_data())
        val += coeff_in * (*labda_in->template data<double>());
    result.emplace_back(labda_out, val);
  }
  vec.RemoveFromTree();
  return result;
}

template <typename I, typename BasisIn, typename BasisOut>
SparseVector<BasisIn> LinearOperator<I, BasisIn, BasisOut>::RMatVec(
    const SparseVector<BasisOut> &vec,
    const SparseIndices<BasisIn> &indices_in) const {
  if (vec.empty() || indices_in.empty()) return {};
  assert(indices_in.IsUnique());
  SparseVector<BasisIn> result;
  result.reserve(indices_in.size());

  vec.StoreInTree();
  for (const auto labda_in : indices_in) {
    double val = 0;
    for (const auto [labda_out, coeff_out] : I::Column(labda_in))
      if (labda_out->has_data())
        val += coeff_out * (*labda_out->template data<double>());
    result.emplace_back(labda_in, val);
  }
  vec.RemoveFromTree();
  return result;
}

template <typename I, typename BasisIn, typename BasisOut>
SparseIndices<BasisOut> LinearOperator<I, BasisIn, BasisOut>::Range(
    const SparseIndices<BasisIn> &ind) const {
  SparseIndices<BasisOut> result;
  result.reserve(ind.size() * 2);

  for (const auto labda_in : ind)
    for (const auto [labda_out, _] : I::Column(labda_in))
      result.emplace_back(labda_out);
  result.Compress();
  return result;
}

template <typename I, typename BasisIn, typename BasisOut>
Eigen::MatrixXd LinearOperator<I, BasisIn, BasisOut>::ToMatrix(
    const SparseIndices<BasisIn> &indices_in,
    const SparseIndices<BasisOut> &indices_out) const {
  assert(indices_in.IsUnique() && indices_out.IsUnique());
  std::unordered_map<BasisIn *, int> indices_in_map;
  std::unordered_map<BasisOut *, int> indices_out_map;
  Eigen::MatrixXd A =
      Eigen::MatrixXd::Zero(indices_out.size(), indices_in.size());

  for (int i = 0; i < indices_in.size(); ++i) {
    assert(!indices_in_map.count(indices_in[i]));
    indices_in_map[indices_in[i]] = i;
  }
  for (int i = 0; i < indices_out.size(); ++i) {
    assert(!indices_out_map.count(indices_out[i]));
    indices_out_map[indices_out[i]] = i;
  }

  // Create A.
  for (int i = 0; i < indices_in.size(); ++i) {
    SparseVector<BasisIn> vec{{{indices_in[i], 1.0}}};
    auto op_vec = MatVec(vec);
    for (auto [fn, coeff] : op_vec) {
      A(indices_out_map[fn], i) = coeff;
    }
  }

  return A;
}

/**
 *  Below implementations have various return types based. The aliases
 *  below are some handy containers with fixed memory size.
 */
template <typename Basis, size_t N>
using StaticSparseVector =
    boost::container::static_vector<std::pair<Basis *, double>, N>;
template <typename Basis, size_t N>
using ArraySparseVector = std::array<std::pair<Basis *, double>, N>;

/**
 *  Implementations of Prolongate.
 */
template <>
inline auto Prolongate<ContLinearScalingFn>::Column(
    ContLinearScalingFn *phi_in) {
  StaticSparseVector<ContLinearScalingFn, 3> result;

  auto [l, n] = phi_in->labda();
  result.emplace_back(phi_in->RefineMiddle(), 1.0);
  if (n > 0) result.emplace_back(phi_in->RefineLeft(), 0.5);
  if (n < (1LL << l)) result.emplace_back(phi_in->RefineRight(), 0.5);
  return result;
}

template <>
inline auto Prolongate<ContLinearScalingFn>::Row(ContLinearScalingFn *phi_in) {
  StaticSparseVector<ContLinearScalingFn, 2> result;
  const auto &parents = phi_in->parents();
  if (parents.size() == 1)
    result = {{{parents[0], 1.0}}};
  else if (parents.size() == 2)
    result = {{{parents[0], 0.5}, {parents[1], 0.5}}};
  else
    assert(false);
  return result;
}

template <>
inline auto Prolongate<DiscLinearScalingFn>::Column(
    DiscLinearScalingFn *phi_in) {
  StaticSparseVector<DiscLinearScalingFn, 4> result;
  auto [l, n] = phi_in->labda();
  phi_in->Refine();
  const auto &children = phi_in->children();
  if (phi_in->pw_constant()) {
    result.emplace_back(children[0], 1);
    result.emplace_back(children[2], 1);
  } else {
    result.emplace_back(children[0], -sqrt(3) / 2);
    result.emplace_back(children[1], 0.5);
    result.emplace_back(children[2], sqrt(3) / 2);
    result.emplace_back(children[3], 0.5);
  }
  return result;
}

template <>
inline auto Prolongate<DiscLinearScalingFn>::Row(DiscLinearScalingFn *phi_in) {
  StaticSparseVector<DiscLinearScalingFn, 2> result;
  auto [l, n] = phi_in->labda();
  const auto &parents = phi_in->parents();
  switch (n % 4) {
    case 0:
      result = {{{parents[0], 1.0}, {parents[1], -sqrt(3) / 2}}};
      return result;
    case 1:
      result = {{{parents[1], 0.5}}};
      return result;
    case 2:
      result = {{{parents[0], 1.0}, {parents[1], sqrt(3) / 2}}};
      return result;
    case 3:
    default:
      result = {{{parents[1], 0.5}}};
      return result;
  }
}

/**
 *  Implementations of the Mass operator.
 */
template <>
inline auto MassOperator<ContLinearScalingFn, ContLinearScalingFn>::Column(
    ContLinearScalingFn *phi_in) {
  StaticSparseVector<ContLinearScalingFn, 3> result;

  auto [l, n] = phi_in->labda();
  double self_ip = 0;
  if (n > 0) {
    auto elem = phi_in->support()[0];
    result.emplace_back(elem->RefineContLinear()[0], 1. / ((1LL << l) * 6.0));
    self_ip += 1. / ((1LL << l) * 3.0);
  }
  if (n < (1LL << l)) {
    auto elem = phi_in->support().back();
    result.emplace_back(elem->RefineContLinear()[1], 1. / ((1LL << l) * 6.0));
    self_ip += 1. / ((1LL << l) * 3.0);
  }
  result.emplace_back(phi_in, self_ip);
  return result;
}

template <>
inline auto MassOperator<ContLinearScalingFn, ContLinearScalingFn>::Row(
    ContLinearScalingFn *phi_out) {
  // This MassOperator is symmetric.
  return Column(phi_out);
}

template <>
inline auto MassOperator<DiscConstantScalingFn, DiscConstantScalingFn>::Column(
    DiscConstantScalingFn *phi_in) {
  return ArraySparseVector<DiscConstantScalingFn, 1>{
      {{phi_in, 1. / (1LL << phi_in->level())}}};
}

template <>
inline auto MassOperator<DiscConstantScalingFn, DiscConstantScalingFn>::Row(
    DiscConstantScalingFn *phi_out) {
  // This MassOperator is symmetric.
  return Column(phi_out);
}

template <>
inline auto MassOperator<DiscLinearScalingFn, DiscLinearScalingFn>::Column(
    DiscLinearScalingFn *phi_in) {
  return ArraySparseVector<DiscLinearScalingFn, 1>{
      {{phi_in, 1. / (1LL << phi_in->level())}}};
}

template <>
inline auto MassOperator<DiscLinearScalingFn, DiscLinearScalingFn>::Row(
    DiscLinearScalingFn *phi_out) {
  // This MassOperator is symmetric.
  return Column(phi_out);
}

namespace Mass {
inline auto ThreeInOrthoOut(ContLinearScalingFn *phi_in) {
  StaticSparseVector<DiscLinearScalingFn, 4> result;

  auto [l, n] = phi_in->labda();
  const double s = 1. / (1LL << (l + 1));  // s = pow(2, -(l+1)).
  if (n > 0) {
    const auto &[pdl0, pdl1] = phi_in->support().front()->PhiDiscLinear();
    assert(pdl0 != nullptr && pdl1 != nullptr);
    result.emplace_back(pdl0, s);
    result.emplace_back(pdl1, s / sqrt(3));
  }
  if (n < (1LL << l)) {
    const auto &[pdl0, pdl1] = phi_in->support().back()->PhiDiscLinear();
    assert(pdl0 != nullptr && pdl1 != nullptr);
    result.emplace_back(pdl0, s);
    result.emplace_back(pdl1, -s / sqrt(3));
  }
  return result;
}

inline auto OrthoInThreeOut(DiscLinearScalingFn *phi_in) {
  ArraySparseVector<ContLinearScalingFn, 2> result;
  auto [l, n] = phi_in->labda();
  const auto &[pcl0, pcl1] = phi_in->support()[0]->RefineContLinear();
  assert(pcl0 != nullptr && pcl1 != nullptr);
  const double s = 1. / (1LL << (l + 1));  // s = pow(2, -(l+1)).
  if (phi_in->pw_constant())
    result = {{{pcl0, s}, {pcl1, s}}};
  else
    result = {{{pcl0, -s / sqrt(3)}, {pcl1, s / sqrt(3)}}};
  return result;
}
};  // namespace Mass

template <>
inline auto MassOperator<ContLinearScalingFn, DiscLinearScalingFn>::Column(
    ContLinearScalingFn *phi_in) {
  return Mass::ThreeInOrthoOut(phi_in);
}

template <>
inline auto MassOperator<ContLinearScalingFn, DiscLinearScalingFn>::Row(
    DiscLinearScalingFn *phi_out) {
  return Mass::OrthoInThreeOut(phi_out);
}

template <>
inline auto MassOperator<DiscLinearScalingFn, ContLinearScalingFn>::Column(
    DiscLinearScalingFn *phi_in) {
  return Mass::OrthoInThreeOut(phi_in);
}

template <>
inline auto MassOperator<DiscLinearScalingFn, ContLinearScalingFn>::Row(
    ContLinearScalingFn *phi_out) {
  return Mass::ThreeInOrthoOut(phi_out);
}

template <>
inline auto ZeroEvalOperator<ContLinearScalingFn, ContLinearScalingFn>::Column(
    ContLinearScalingFn *phi_in) {
  StaticSparseVector<ContLinearScalingFn, 1> result;
  auto [l, n] = phi_in->labda();
  if (n == 0) result = {{{phi_in, 1.0}}};
  return result;
}

template <>
inline auto ZeroEvalOperator<ContLinearScalingFn, ContLinearScalingFn>::Row(
    ContLinearScalingFn *phi_out) {
  return Column(phi_out);
}

template <>
inline auto ZeroEvalOperator<DiscLinearScalingFn, DiscLinearScalingFn>::Column(
    DiscLinearScalingFn *phi_in) {
  StaticSparseVector<DiscLinearScalingFn, 2> result;
  auto [l, n] = phi_in->labda();
  if (n <= 1) {
    const auto &[pdl0, pdl1] = phi_in->support().front()->PhiDiscLinear();
    assert(pdl0 != nullptr && pdl1 != nullptr);
    if (phi_in->pw_constant())
      result = {{{pdl0, 1.0}, {pdl1, -sqrt(3)}}};
    else
      result = {{{pdl0, -sqrt(3)}, {pdl1, 3.0}}};
  }
  return result;
}

template <>
inline auto ZeroEvalOperator<DiscLinearScalingFn, DiscLinearScalingFn>::Row(
    DiscLinearScalingFn *phi_out) {
  return Column(phi_out);
}

namespace ZeroEval {
inline auto ThreeInOrthoOut(ContLinearScalingFn *phi_in) {
  StaticSparseVector<DiscLinearScalingFn, 2> result;
  auto [l, n] = phi_in->labda();
  if (n == 0) {
    const auto &[pdl0, pdl1] = phi_in->support().front()->PhiDiscLinear();
    assert(pdl0 != nullptr && pdl1 != nullptr);
    result = {{{pdl0, 1.0}, {pdl1, -sqrt(3)}}};
  }
  return result;
}

inline auto OrthoInThreeOut(DiscLinearScalingFn *phi_in) {
  StaticSparseVector<ContLinearScalingFn, 1> result;
  auto [l, n] = phi_in->labda();
  if (n <= 1) {
    const auto &pcl0 = phi_in->support().front()->RefineContLinear()[0];
    assert(pcl0 != nullptr);
    if (phi_in->pw_constant())
      result = {{{pcl0, 1.0}}};
    else
      result = {{{pcl0, -sqrt(3)}}};
  }
  return result;
}
};  // namespace ZeroEval

template <>
inline auto ZeroEvalOperator<ContLinearScalingFn, DiscLinearScalingFn>::Column(
    ContLinearScalingFn *phi_in) {
  return ZeroEval::ThreeInOrthoOut(phi_in);
}

template <>
inline auto ZeroEvalOperator<ContLinearScalingFn, DiscLinearScalingFn>::Row(
    DiscLinearScalingFn *phi_out) {
  return ZeroEval::OrthoInThreeOut(phi_out);
}

template <>
inline auto ZeroEvalOperator<DiscLinearScalingFn, ContLinearScalingFn>::Column(
    DiscLinearScalingFn *phi_in) {
  return ZeroEval::OrthoInThreeOut(phi_in);
}

template <>
inline auto ZeroEvalOperator<DiscLinearScalingFn, ContLinearScalingFn>::Row(
    ContLinearScalingFn *phi_out) {
  return ZeroEval::ThreeInOrthoOut(phi_out);
}

namespace Transport {
inline auto ThreeInOrthoOut(ContLinearScalingFn *phi_in) {
  StaticSparseVector<DiscLinearScalingFn, 2> result;

  auto [l, n] = phi_in->labda();
  if (n > 0) {
    const auto &pdl0 = phi_in->support().front()->PhiDiscLinear()[0];
    assert(pdl0 != nullptr);
    result.emplace_back(pdl0, 1.0);
  }
  if (n < (1LL << l)) {
    const auto &pdl0 = phi_in->support().back()->PhiDiscLinear()[0];
    assert(pdl0 != nullptr);
    result.emplace_back(pdl0, -1.0);
  }
  return result;
}
inline auto OrthoInThreeOut(DiscLinearScalingFn *phi_in) {
  StaticSparseVector<ContLinearScalingFn, 2> result;
  auto [l, n] = phi_in->labda();
  const auto &[pcl0, pcl1] = phi_in->support().front()->RefineContLinear();
  assert(pcl0 != nullptr && pcl1 != nullptr);
  if (phi_in->pw_constant()) result = {{{pcl0, -1.0}, {pcl1, 1.0}}};
  return result;
}
}  // namespace Transport

template <>
inline auto TransportOperator<ContLinearScalingFn, DiscLinearScalingFn>::Column(
    ContLinearScalingFn *phi_in) {
  return Transport::ThreeInOrthoOut(phi_in);
}

template <>
inline auto TransportOperator<ContLinearScalingFn, DiscLinearScalingFn>::Row(
    DiscLinearScalingFn *phi_out) {
  return Transport::OrthoInThreeOut(phi_out);
}

template <>
inline auto TransportOperator<DiscLinearScalingFn, ContLinearScalingFn>::Column(
    DiscLinearScalingFn *phi_in) {
  return Transport::OrthoInThreeOut(phi_in);
}

template <>
inline auto TransportOperator<DiscLinearScalingFn, ContLinearScalingFn>::Row(
    ContLinearScalingFn *phi_out) {
  return Transport::ThreeInOrthoOut(phi_out);
}

}  // namespace Time