#ifndef KA_PARSE_HPP
#define KA_PARSE_HPP
#pragma once

#include "macroregular.hpp"
#include "utility.hpp"
#include "typetraits.hpp"
#include "indexed.hpp"
#include "opt.hpp"
#include "functional.hpp"
#include "zero.hpp"
#include "functor.hpp"

#include <boost/variant/variant.hpp>
#include <boost/variant/get.hpp>
#include <boost/variant/apply_visitor.hpp>

#include <vector>
#include <utility>
#include <initializer_list>

namespace ka {
namespace parse {
/// Semantics is provided through 'meaning' expressions, of the form
/// 'meaning(<C++ construct>) = <denotation>'. A denotation is an entity that is
/// the *interpretation* of a C++ construct.
///
/// See formal specification `spec:/sbre/framework/2020/d`.

/// The result of some parsing operation.
///
/// It is an optional parsed element (if parsing succeeded), paired with the
/// string that remains to parse. Two sets are therefore involved: the
/// "parsed set" and the "symbol set" (aka "character set").
///
/// meaning(std::iterator_traits<I>::value_type) = C
///   C is the 'character' or 'symbol' set, whom strings are parsed.
///
/// meaning(pair<res_t<A, I>, I>) = R(A, C)
///   `res_t` contains only the iterator until the parser succeeded to parse.
///   Therefore, the *string* C^* that remains to parse is represented by the
///   bounded range whose `res_t`'s iterator is the beginning and the end
///   iterator is known from the context.
///
/// ForwardIterator I
template<typename A, typename I>
struct res_t {
  using value_type = A;
  using iterator_type = I;

  opt_t<A> value;
  I iter;

// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_2(res_t, value, iter)

// Readable:
  auto operator*() const& noexcept -> decltype(*value) {
    return *value;
  }

// Mutable:
  auto operator*() & noexcept -> decltype(*value) {
    return *value;
  }
  auto operator*() && noexcept -> decltype(*mv(value)) {
    return *mv(value);
  }

// EmptyMutable:
  constexpr
  auto empty() const noexcept -> bool {
    return value.empty();
  }

  template<typename F, typename V>
  using ValueOfResultOfFmap = typename Decay<CodomainFor<fmap_fn_t, F, V>>::value_type;

// Functor:
  /// Function<U (A)> F
  template<typename F>
  auto fmap(F&& f) const&
    -> res_t<ValueOfResultOfFmap<F, const opt_t<A>&>, I> {
    return { ka::fmap(fwd<F>(f), value), iter };
  }

  /// Function<U (A)> F
  template<typename F>
  auto fmap(F&& f) &&
    -> res_t<ValueOfResultOfFmap<F, opt_t<A>&&>, I> {
    return { ka::fmap(fwd<F>(f), mv(value)), mv(iter) };
  }
};

/// Convenience function to construct a result in success, deducing argument types.
///
/// meaning(ok(a, i)) = (a, s), where s = meaning(pair{i, end})
struct ok_fn_t {
// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_0(ok_fn_t)
// Function:
  /// ForwardIterator I
  template<typename A, typename I> constexpr
  auto operator()(A&& a, I&& i) const noexcept -> res_t<Decay<A>, Decay<I>> {
    return {ka::opt(fwd<A>(a)), fwd<I>(i)};
  }
};

static constexpr auto const& ok = static_const_t<ok_fn_t>::value;

/// Convenience function to construct a result in error, deducing argument types.
///
/// meaning(err(t, i)) = (none, s), where s = meaning(pair{i, end})
///
/// ForwardIterator I
struct err_fn_t {
// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_0(err_fn_t)
// Function:
  template<typename A, typename I> constexpr
  auto operator()(type_t<A>, I i) const noexcept -> res_t<A, I> {
    return {opt_t<A>{}, mv(i)};
  }
};

static constexpr auto const& err = static_const_t<err_fn_t>::value;

/// Convenience function to construct a result, deducing argument types.
///
/// meaning(res(a, i)) = (a, s), where s = meaning(pair{i, end})
///
/// ForwardIterator I
template<typename A, typename I> constexpr
auto res(opt_t<A> opt_val, I i) noexcept -> res_t<A, I> {
  return {mv(opt_val), mv(i)};
}

/// The result iterator up to which the parsing was successful.
///
/// meaning(iter(res(a, i))) = s, where s = meaning(pair{i, end})
///
/// ForwardIterator I
template<typename A, typename I> constexpr
auto iter(res_t<A, I> const& r) noexcept -> I const& {
  return r.iter;
}

/// Overload for non-const references.
template<typename A, typename I> constexpr
auto iter(res_t<A, I>& r) noexcept -> I& {
  return r.iter;
}

/// Overload for rvalue-references.
template<typename A, typename I> constexpr
auto iter(res_t<A, I>&& r) noexcept -> I {
  return mv(r.iter);
}

/// The optional value of the result.
///
/// invariant: ka::empty(r) == ka::empty(to_opt(r))
///         && ka::src(r) == ka::src(to_opt(r))
///
/// meaning(to_opt)(a, s) = a
///
/// ForwardIterator I
template<typename A, typename I> constexpr
auto to_opt(res_t<A, I> const& r) noexcept -> ka::opt_t<A> const& {
  return r.value;
}

/// Overload for non-const references.
template<typename A, typename I> constexpr
auto to_opt(res_t<A, I>& r) noexcept -> ka::opt_t<A>& {
  return r.value;
}

/// Overload for rvalue-references.
template<typename A, typename I> constexpr
auto to_opt(res_t<A, I>&& r) noexcept -> ka::opt_t<A> {
  return mv(r.value);
}

/// Derives the implementation of the functor part of a parser type.
#define KA_PARSER_DERIVE_FUNCTOR()                                 \
  /* Procedure<B (A)> PDFF */                                      \
  template<typename PDFF> constexpr                                \
  auto fmap(PDFF&& f) const & noexcept                             \
    -> ka::parse::fmapped_t<Decay<PDFF>, Decay<decltype(*this)>> { \
    return { fwd<PDFF>(f), *this };                                \
  }                                                                \
  template<typename PDFF>                                          \
  auto fmap(PDFF&& f) && noexcept                                  \
    -> ka::parse::fmapped_t<Decay<PDFF>, Decay<decltype(*this)>> { \
    return { fwd<PDFF>(f), std::move(*this) };                     \
  }

/// meaning(ResultOf<PA, I>) = R(A, C^*), where C = meaning(I::value_type)
///
/// Parser<A> PA
/// ForwardIterator I
template<typename PA, typename I>
using ResultOf = Decay<CodomainFor<PA, I, I>>;

/// meaning(ValueOf<PA, I>) = A
///
/// Parser<A> PA
/// ForwardIterator I
template<typename PA, typename I>
using ValueOf = typename ResultOf<PA, I>::value_type;

/// Mapping of a procedure through the parser functor.
///
/// I.e. new parser whose result is obtained by mapping the function to the
/// original parser's result.
///
/// meaning(fmap) = P: B^A → (PB)^(PA) // function-part of the functor
///
/// Procedure<B (A)> F
/// Parser PA
template<typename F, typename PA>
struct fmapped_t {
  F f;
  PA pa;

// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_2(fmapped_t, f, pa)

// Functor (up to isomorphism):
  KA_PARSER_DERIVE_FUNCTOR()

// Parser<A>:
  /// @pre readable_bounded_range(b, e)
  template<typename I> constexpr
  auto operator()(I b, I e) const -> decltype(pa(b, e).fmap(f)) {
    // Workaround MSVC:
    // Using `ka::fmap()` does not instantiate the right `fmap_dispatch` function.
    // This seems to be a compiler bug.
    return pa(b, e).fmap(f);
  }
};

/// Parser of a single element.
///
/// meaning(symbol_t<I>) = PC, where C = meaning(I::value_type)
/// (C is the 'character' or 'symbol' set)
struct symbol_t {
  template<typename I>
  using Value = typename std::iterator_traits<I>::value_type;

// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_0(symbol_t)

// Functor (up to isomorphism):
  KA_PARSER_DERIVE_FUNCTOR()

// Parser<C>:
  /// @pre readable_bounded_range(b, e)
  template<typename I> constexpr
  auto operator()(I b, I e) const -> res_t<Value<I>, I> {
    return (b == e) ? err(type_t<Value<I>>{}, b) : ok(*b, std::next(b));
  }
};

static constexpr auto const& symbol = static_const_t<symbol_t>::value;

/// Combines multiple parsers into one, which returns the product of all results.
/// Succeeds if all parsers succeed.
///
/// meaning(product_t) = ⊗
///
/// Parser<A>... PA
template<typename... PA>
struct product_t {
  ka::product_t<PA...> pa;

  template<typename I>
  using Value = ka::product_t<ValueOf<PA, I>...>;

// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_1(product_t, pa)

// Functor (up to isomorphism):
  KA_PARSER_DERIVE_FUNCTOR()

// Parser<ka::product_t<A...>>:
  /// @pre readable_bounded_range(b, e)
  template<typename I>
  auto operator()(I b, I e) const -> res_t<Value<I>, I> {
    return impl(index_sequence_for<PA...>{}, b, e);
  }

private:
  template<std::size_t Idx, typename I>
  auto do_one_at(ka::product_t<opt_t<ResultOf<PA, I>>...>& res, I b, I e) const
    -> std::pair<bool, I> {
    auto& opt_res_at = std::get<Idx>(res);
    opt_res_at.emplace_front(std::get<Idx>(pa)(b, e));
    auto& res_at = opt_res_at.front();
    return { !res_at.empty(), iter(res_at) };
  }

  template<std::size_t... Idx, typename I>
  auto impl(index_sequence<Idx...>, I b, I e) const -> res_t<Value<I>, I> {
    auto success = true;
    auto it = b;
    auto opt_res = ka::product_t<opt_t<ResultOf<PA, I>>...>{};
    // The following use of `std::initializer_list` is a C++11 hack to
    // repeatedly apply a function on arguments of heterogeneous types, avoiding
    // function recursion.
    // TODO: Factorize in a named function, and replace in C++17 or above.

    // Ignore unused variables (happens with product_t<>).
    (void) e; (void) opt_res;
    (void) std::initializer_list<bool>{ (
      success = success && (
          std::tie(success, it) = do_one_at<Idx>(opt_res, it, e),
          success
        )
      )... };

    if (!success) {
      return err(type_t<Value<I>>{}, b);
    }

    auto values = ka::product(mv(**std::get<Idx>(opt_res))...);
    return ok(mv(values), it);
  }
};

/// Constructs a product of parsers, performing argument type deduction.
///
/// Parser<A>... PA
struct product_fn_t {
  template<typename... PA> constexpr
  auto operator()(PA&&... pa) const noexcept -> product_t<Decay<PA>...> {
    return { ka::product(fwd<PA>(pa)...) };
  }
};
static constexpr auto const& product = static_const_t<product_fn_t>::value;

/// Parser that always succeeds, returning unit and the string as-is.
///
/// meaning(unit_t) = 1
using unit_t = product_t<>;
static constexpr auto const& unit = static_const_t<unit_t>::value;

namespace detail {
  // `product_t` is `parse::product_t`, by opposition to `ka::product_t` (tuple
  // construction).
  template<typename PA> constexpr
  auto unwrap_product(PA&& pa) noexcept -> ka::product_t<PA&&> {
    return ka::product_t<PA&&>{ fwd<PA>(pa) };
  }

  template<typename... PA> constexpr
  auto unwrap_product(product_t<PA...> const& prod) noexcept ->
  ka::product_t<PA...> const& {
    return prod.pa;
  }

  template<typename... PA> constexpr
  auto unwrap_product(product_t<PA...>&& prod) noexcept -> ka::product_t<PA...>&& {
    return mv(prod.pa);
  }

  constexpr
  auto unwrap_product(product_t<> prod) noexcept -> ka::product_t<> {
    return {};
  }
} // namespace detail

/// Constructs a product of parsers, flattening any product of product in the
/// result and performing argument type deduction.
///
/// Parser<A>... PA
template<typename... PA> constexpr
auto flat_product(PA&&... pa) noexcept
  -> decltype(apply(product, std::tuple_cat(detail::unwrap_product(fwd<PA>(pa))...))) {
  return apply(product, std::tuple_cat(detail::unwrap_product(fwd<PA>(pa))...));
}

/// Convenience type for the value type of the result of a sum of parsers.
template<typename... A>
using SumValue = ApplyIndexed<boost::variant, A...>;

/// Combines multiple parsers into one, which returns the result of the first
/// parser that succeeds, in the order of declaration. Succeeds if any parser
/// succeeds.
///
/// meaning(sum_t) = ⊕
///
/// Parser<A>... PA
template<typename... PA>
struct sum_t {
  ka::product_t<PA...> pa;

  template<typename I>
  using Value = SumValue<ValueOf<PA, I>...>;

// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_1(sum_t, pa)

// Functor (up to isomorphism):
  KA_PARSER_DERIVE_FUNCTOR()

// Parser<SumValue<A>>:
  /// @pre readable_bounded_range(b, e)
  template<typename I>
  auto operator()(I b, I e) const -> res_t<Value<I>, I> {
    return impl(index_sequence_for<PA...>{}, b, e);
  }

private:
  template<std::size_t Idx, typename I>
  auto do_one_at(opt_t<Value<I>>& opt_val, I b, I e) const -> std::pair<bool, I> {
    auto res = std::get<Idx>(pa)(b, e);
    auto const ok = !res.empty();
    if (ok) {
      opt_val.emplace_front(indexed<Idx>(*mv(res)));
    }
    // Ok to use `res` here even after move, since we're not accessing the moved
    // part.
    return { ok, iter(res) };
  }

  template<std::size_t... Idx, typename I>
  auto impl(index_sequence<Idx...>, I b, I e) const -> res_t<Value<I>, I> {
    auto success = false;
    auto it = b;
    auto opt_val = opt_t<Value<I>>{};

    // The following use of `std::initializer_list` is a C++11 hack to
    // repeatedly apply a function on arguments of heterogeneous types, avoiding
    // function recursion.
    // TODO: Factorize in a named function, and replace in C++17 or above.
    (void) std::initializer_list<bool>{(
      success = success || (
          std::tie(success, it) = do_one_at<Idx>(opt_val, it, e),
          success
        )
      )...};

    if (!success) {
      return err(type_t<Value<I>>{}, b);
    }
    assert(!opt_val.empty());
    return ok(*mv(opt_val), it);
  }
};

/// Parser that always fails to return an element of the empty set 0 (since
/// there's none).
///
/// meaning(sum_t<>) = 0
// TODO: Remove this specialization when having a `ka::sum_t` type that can be empty.
template<>
struct sum_t<> {
  using Value = ka::zero_t;
  ka::product_t<> pa;

// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_0(sum_t)

// Functor (up to isomorphism):
  KA_PARSER_DERIVE_FUNCTOR()

// Parser<>:
  /// @pre readable_bounded_range(b, e)
  template<typename I> constexpr
  auto operator()(I b, I) const noexcept -> res_t<Value, I> {
    return err(type_t<Value>{}, b);
  }
};
using zero_t = sum_t<>;
static constexpr auto const& zero = static_const_t<zero_t>::value;

/// Constructs a sum of parsers, performing argument type deduction.
///
/// Parser<A>... PA
struct sum_fn_t {
  template<typename... PA> constexpr
  auto operator()(PA&&... pa) const noexcept -> sum_t<Decay<PA>...> {
    return { ka::product(fwd<PA>(pa)...) };
  }
};
static constexpr auto const& sum = static_const_t<sum_fn_t>::value;

namespace detail {
  template<typename PA> constexpr
  auto unwrap_sum(PA&& pa) noexcept -> ka::product_t<PA&&> {
    return ka::product_t<PA&&>{ fwd<PA>(pa) };
  }

  template<typename... PA> constexpr
  auto unwrap_sum(sum_t<PA...> const& s) noexcept -> ka::product_t<PA...> const& {
    return s.pa;
  }

  template<typename... PA> constexpr
  auto unwrap_sum(sum_t<PA...>&& s) noexcept -> ka::product_t<PA...>&& {
    return mv(s.pa);
  }
} // namespace detail

/// Constructs a sum of parsers, flattening any sum of sum in the result and
/// performing argument type deduction.
///
/// Parser<A>... PA
template<typename... PA> constexpr
auto flat_sum(PA&&... pa) noexcept
  -> decltype(apply(sum, std::tuple_cat(detail::unwrap_sum(fwd<PA>(pa))...))) {
  return apply(sum, std::tuple_cat(detail::unwrap_sum(fwd<PA>(pa))...));
}

namespace detail {
  /// Visitor for `boost::variant` that contains either `indexed_t<T>` or
  /// indexed_t<ka::unit_t>` and converts it into an `opt_t<T>`.
  struct to_opt_t {
    template<typename V, typename... A>
    struct visitor_t : boost::static_visitor<opt_t<V>> {
      template<std::size_t I, typename U>
      auto operator()(indexed_t<I, U> const& u) const -> ka::opt_t<V> {
        return ka::opt(V(src(u)));
      }
      template<std::size_t I>
      auto operator()(indexed_t<I, ka::unit_t>) const -> ka::opt_t<V> {
        return {};
      }
    };

    template<typename A> constexpr
    auto operator()(boost::variant<indexed_t<0, A>, indexed_t<1, ka::unit_t>> const& v) const
      -> opt_t<A> {
      return boost::apply_visitor(visitor_t<A>{}, v);
    }
  };
} // namespace detail

/// Convenience function to construct a parser of optional that always succeeds,
/// producing an optional value.
///
/// meaning(opt(pa)) = pa ⊕ 1
///
/// Parser<A> PA
template<typename PA> constexpr
auto opt(PA&& pa) noexcept
  -> decltype(sum(pa, unit).fmap(detail::to_opt_t{})) {
  return sum(pa, unit).fmap(detail::to_opt_t{});
}

/// Parser that applies another one between min and max times (included). Its
/// result is a list of `A` containing between min and max (included) elements.
///
/// meaning(quantify_t) = quantify
///
/// Parser<A> PA
template<typename PA>
struct quantify_t {
  PA pa;
  std::size_t min;
  ka::opt_t<std::size_t> max; // Precondition: max.empty() || min <= src(max)

  template<typename I>
  using Value = std::vector<ValueOf<PA, I>>;

// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_3(quantify_t, pa, min, max)

// Functor (up to isomorphism):
  KA_PARSER_DERIVE_FUNCTOR()

// Parser<std::vector<A>>:
  /// @pre readable_bounded_range(b, e)
  template<typename I>
  auto operator()(I b, I e) const -> res_t<Value<I>, I> {
    auto b0 = b;
    auto values = Value<I>{};
    if (max.empty()) {
      while (parse_one(pa, b, e, values)); // unbounded
    } else {
      auto const m = src(max);
      for (std::size_t i = 0; i != m && parse_one(pa, b, e, values); ++i);
    }
    return min <= values.size()
      ? ok(mv(values), mv(b))
      : err(type_t<Value<I>>{}, mv(b0));
  }
private:
  template<typename I, typename V> static
  auto parse_one(PA const& pa, I& b, I const& e, V& values) -> bool {
    auto res = pa(b, e);
    if (res.empty()) return false;
    b = iter(res);
    values.push_back(*mv(res));
    return true;
  }
};

/// Constructs a quantified parser, performing argument type deduction.
/// Omitting or passing an empty maximum means there is no upper bound.
struct quantify_fn_t {
// Regular:
  KA_GENERATE_FRIEND_REGULAR_OPS_0(quantify_fn_t)
// Function:
  /// Precondition: max.empty() || min <= src(max)
  /// Parser<A> PA
  template<typename PA>
  auto operator()(PA&& pa, std::size_t min, ka::opt_t<std::size_t> max) const noexcept -> quantify_t<Decay<PA>> {
    return {fwd<PA>(pa), min, mv(max)};
  }
  /// Precondition: min <= max
  /// Parser<A> PA
  template<typename PA>
  auto operator()(PA&& pa, std::size_t min, std::size_t max) const noexcept -> quantify_t<Decay<PA>> {
    return {fwd<PA>(pa), min, ka::opt(max)};
  }
  /// Parser<A> PA
  template<typename PA>
  auto operator()(PA&& pa, std::size_t min) const noexcept -> quantify_t<Decay<PA>> {
    return {fwd<PA>(pa), min, ka::opt_t<std::size_t>{}};
  }
};

static constexpr auto const& quantify = static_const_t<quantify_fn_t>::value;

/// Parser that applies another one as much as possible and always succeeds. Its
/// result is a list of `A`.
///
/// meaning(list) = ^* = quantify(_, 0, ∞)
///
/// Parser<A> PA
template<typename PA> constexpr
auto list(PA&& pa) noexcept -> quantify_t<Decay<PA>> {
  return quantify(fwd<PA>(pa), 0);
}

namespace detail {
  template<typename PA> constexpr
  auto unwrap_quantify(PA&& pa) noexcept -> PA&& {
    return fwd<PA>(pa);
  }

  template<typename PA> constexpr
  auto unwrap_quantify(quantify_t<PA> const& l) noexcept -> PA const& {
    return l.pa;
  }

  template<typename PA> constexpr
  auto unwrap_quantify(quantify_t<PA>&& l) noexcept -> PA&& {
    return mv(l.pa);
  }
} // namespace detail

/// Constructs a list of a parser, flattening the result if the argument is
/// already a list and performing argument type deduction.
///
/// Parser<A> PA
template<typename PA> constexpr
auto flat_list(PA&& pa) noexcept
  -> decltype(list(detail::unwrap_quantify(fwd<PA>(pa)))) {
  return list(detail::unwrap_quantify(fwd<PA>(pa)));
}

// TODO: Complete list monoid with concatenation and empty list.
// TODO: Add literal, filter and bounded quantification parsers (see
//  `spec:/sbre/framework/2020/d`)

namespace ops {
  /// Operator for flat product of parsers.
  ///
  /// Parser<A> PA
  /// Parser<B> PB
  template<typename PA, typename PB> constexpr
  auto operator*(PA&& pa, PB&& pb) noexcept
    -> decltype(flat_product(fwd<PA>(pa), fwd<PB>(pb))) {
    return flat_product(fwd<PA>(pa), fwd<PB>(pb));
  }

  /// Operator for flat sum of parsers.
  ///
  /// Parser<A> PA
  /// Parser<B> PB
  template<typename PA, typename PB> constexpr
  auto operator+(PA&& pa, PB&& pb) noexcept
    -> decltype(flat_sum(fwd<PA>(pa), fwd<PB>(pb))) {
    return flat_sum(fwd<PA>(pa), fwd<PB>(pb));
  }

  /// Operator for flat list (repetition) of parsers.
  ///
  /// Parser<A> PA
  /// Parser<B> PB
  template<typename PA> constexpr
  auto operator*(PA&& pa) noexcept
    -> decltype(flat_list(fwd<PA>(pa))) {
    return flat_list(fwd<PA>(pa));
  }
} // namespace ops

} // namespace parse
} // namespace ka

#endif // KA_PARSE_HPP


#include "Quad.h"
#include <memory>
#include <cassert>
#include <iostream>
#include "ofVboMesh.h"
#include <exception>
#include <boost/algorithm/string.hpp>

using namespace std;
using namespace ofx;

// Convert two vertex indices into edge key by returning pair of
// vertex indices where first index in pair is greater than second.
EdgeKey makeEdgeKey(VertexID v0, VertexID v1)
{
    if (v0 == -1 || v1 == -1) {
        return {-1, -1};
    }

    if (v0 > v1) {
        auto temp = v0;
        v0 = v1;
        v1 = temp;
    }

    return {v0, v1};
}

Quad::Quad() : _redrawMesh(true)
{

}

Quad::Quad(string objFilename) : _redrawMesh(true)
{
    load(objFilename);
}

// Load mesh data from OBJ file.
// Mesh in OBJ file is restricted to 4 sided polygons.
void Quad::load(std::string objFilename)
{
    ifstream input("./bin/" + objFilename);

    vector<VertexID> vertexIDs;

    string line;
    vector<string> tokens;
    while (getline(input, line)) {
        tokens.clear();
        boost::split(tokens, line, boost::is_any_of(" "));
        if (tokens.size() == 4 && tokens[0] == "v") {
            auto vID = addVertex({stof(tokens[1]), stof(tokens[2]), stof(tokens[3])});
            vertexIDs.push_back(vID);
        }
        else if (tokens.size() == 5 && tokens[0] == "f") {
            auto v0 = vertexIDs[stoi(tokens[1]) - 1];
            auto v1 = vertexIDs[stoi(tokens[2]) - 1];
            auto v2 = vertexIDs[stoi(tokens[3]) - 1];
            auto v3 = vertexIDs[stoi(tokens[4]) - 1];
            addFace(v0, v1, v2, v3);
        }
    }

    _redrawMesh = true;
}

// Given vertex position, add new vertex to mesh
// and return vertex ID of new vertex
VertexID Quad::addVertex(ofVec3f vertex)
{
    // Initialize normal of vertex to all zeros. This will be calculated later.
    // Initialize new vertex ID to -1. This is used internally when doing
    // subdivision surface calculations.
    _vertices.push_back({vertex, {0.0, 0.0, 0.0}, -1});
    _redrawMesh = true;
    return _vertices.size() - 1;
}

// Given four valid vertex IDs, add new face to mesh
// and return face ID of new face
FaceID Quad::addFace(VertexID v0, VertexID v1, VertexID v2, VertexID v3)
{
    _edges.push_back({v0, -1, -1, -1, -1});
    EdgeID edge0 = _edges.size() - 1;

    _edges.push_back({v1, -1, -1, -1, -1});
    EdgeID edge1 = _edges.size() - 1;

    _edges.push_back({v2, -1, -1, -1, -1});
    EdgeID edge2 = _edges.size() - 1;

    _edges.push_back({v3, -1, -1, -1, -1});
    EdgeID edge3 = _edges.size() - 1;

    _edges[edge0].next = edge1;
    _edges[edge1].next = edge2;
    _edges[edge2].next = edge3;
    _edges[edge3].next = edge0;

    // function for attaching an edge to an adjacent edge, if exists.
    // Given two vertex IDs, search for existing edge with those vertices
    // as endpoints.
    auto attachEdge = [this](EdgeID edge, VertexID a, VertexID b) {
        auto neighborEdge = findEdge(a, b);
        if (neighborEdge == -1) {
            _edgeMap[makeEdgeKey(a, b)] = edge;
        }
        else {
            _edges[neighborEdge].opposite = edge;
            _edges[edge].opposite = neighborEdge;
        }
    };

    // Attach each edge to an adjacent edge with same endpoints, if it exists
    attachEdge(edge0, v0, v1);
    attachEdge(edge1, v1, v2);
    attachEdge(edge2, v2, v3);
    attachEdge(edge3, v3, v0);

    _faces.push_back({edge0, edge1, edge2, edge3, -1});
    FaceID face = _faces.size() - 1;

    _edges[edge0].face = face;
    _edges[edge1].face = face;
    _edges[edge2].face = face;
    _edges[edge3].face = face;

    _redrawMesh = true;
    return face;
}

// Draw mesh using either flat shading or smooth shading
void Quad::draw(bool smoothShading)
{
    // If mesh has changed, build OpenFrameworks tri mesh out of quad mesh
    // and draw tri mesh
    if (_redrawMesh) {
        _mesh.clear();
        calculateNormals();

        for (auto &f: _faces) {
            auto v0 = _vertices[_edges[f.edges[0]].vertex];
            auto v1 = _vertices[_edges[f.edges[1]].vertex];
            auto v2 = _vertices[_edges[f.edges[2]].vertex];
            auto v3 = _vertices[_edges[f.edges[3]].vertex];

            _mesh.addVertex(v0.position);
            _mesh.addVertex(v1.position);
            _mesh.addVertex(v2.position);

            _mesh.addVertex(v0.position);
            _mesh.addVertex(v2.position);
            _mesh.addVertex(v3.position);

            if (smoothShading) {
                _mesh.addNormal(v0.normal);
                _mesh.addNormal(v1.normal);
                _mesh.addNormal(v2.normal);
                _mesh.addNormal(v0.normal);
                _mesh.addNormal(v2.normal);
                _mesh.addNormal(v3.normal);
            } else {
                _mesh.addNormal(f.normal);
                _mesh.addNormal(f.normal);
                _mesh.addNormal(f.normal);
                _mesh.addNormal(f.normal);
                _mesh.addNormal(f.normal);
                _mesh.addNormal(f.normal);
            }
        }

        _redrawMesh = false;
    }
    _mesh.draw();
}

void Quad::drawWireframe()
{
    for (auto &f: _faces) {
        auto v0 = _vertices[_edges[f.edges[0]].vertex];
        auto v1 = _vertices[_edges[f.edges[1]].vertex];
        auto v2 = _vertices[_edges[f.edges[2]].vertex];
        auto v3 = _vertices[_edges[f.edges[3]].vertex];
        ofLine(v0.position, v1.position);
        ofLine(v1.position, v2.position);
        ofLine(v2.position, v3.position);
        ofLine(v3.position, v0.position);
    }
}

EdgeID Quad::findEdge(VertexID v0, VertexID v1)
{
    auto key = makeEdgeKey(v0, v1);
    auto result = _edgeMap.find(key);
    if (result == _edgeMap.end()) {
        return -1;
    }
    return result->second;
}

// Catmull-Clark subdivision surface algorithm
Quad Quad::subdivide(int level)
{
    if (level == 0) {
        return *this;
    }

    for (auto &v: _vertices) {
        v.newVertex = -1;
    }

    vector<ofVec3f> newVertices;

    // Divide existing face into four new faces, using the four existing face
    // vertices and a new vertex at the center of existing face. Calculate new
    // vertex in center of existing face by averaging four corner vertices
    // of face.
    for (auto &f: _faces) {
        newVertices.push_back((_vertices[_edges[f.edges[0]].vertex].position +
                               _vertices[_edges[f.edges[1]].vertex].position +
                               _vertices[_edges[f.edges[2]].vertex].position +
                               _vertices[_edges[f.edges[3]].vertex].position) / 4.0f);
        f.center = newVertices.size() - 1;
    }

    // Divide existing edge into two new edges, using edge endpoints and new
    // vertex around the midpoint of edge. Calculate new vertex on existing
    // edge by averaging the endpoints of edge and the centers of the two
    // adjacent faces.
    auto newMidpoint = [this, &newVertices](EdgeID edge, VertexID a, VertexID b) {
        // Check to see if new midpoint has already been calculated for
        // opposite edge
        if (_edges[_edges[edge].opposite].midpoint == -1) {
            auto newVertex = (_vertices[a].position +
                              _vertices[b].position +
                              newVertices[_faces[_edges[edge].face].center] +
                              newVertices[_faces[_edges[_edges[edge].opposite].face].center]) / 4.0f;
            newVertices.push_back(newVertex);
            _edges[edge].midpoint = newVertices.size() - 1;
            _edges[_edges[edge].opposite].midpoint = newVertices.size() - 1;
        }
        else {
            // We've already calculated the new midpoint vertex for the
            // opposite edge; no need to calculate again.
            _edges[edge].midpoint = _edges[_edges[edge].opposite].midpoint;
        }
    };

    for (auto &f: _faces) {
        newMidpoint(f.edges[0], _edges[f.edges[0]].vertex, _edges[f.edges[1]].vertex);
        newMidpoint(f.edges[1], _edges[f.edges[1]].vertex, _edges[f.edges[2]].vertex);
        newMidpoint(f.edges[2], _edges[f.edges[2]].vertex, _edges[f.edges[3]].vertex);
        newMidpoint(f.edges[3], _edges[f.edges[3]].vertex, _edges[f.edges[0]].vertex);
    }

    // Calculate new position of each existing vertex, using the midpoints of
    // connected edges, centers of connected faces, and current position.
    // Valence value is the number of connected edges.
    auto getNewVertex = [this, &newVertices](EdgeID edge) {
        int valence = 0;
        ofVec3f  sumMidpoints = {0.0, 0.0, 0.0};
        ofVec3f sumCenters = {0.0, 0.0, 0.0};

        // Loop through each connected edge of vertex
        auto e = _edges[edge].opposite;
        do {
            sumMidpoints += (_vertices[_edges[e].vertex].position +
                             _vertices[_edges[_edges[e].opposite].vertex].position) / 2.0f;
            sumCenters += newVertices[_faces[_edges[e].face].center];
            valence++;
            e = _edges[_edges[e].next].opposite;
        } while (_edges[e].opposite != edge);

        // Formulate for calculating new vertex position
        auto newVertex = ((sumCenters / valence) +
                          ((sumMidpoints / valence) * 2) +
                          (_vertices[_edges[edge].vertex].position * (valence - 3))) / valence;

        newVertices.push_back(newVertex);
        return newVertices.size() - 1;
    };

    for (auto &f: _faces) {
        auto v0 = _edges[f.edges[0]].vertex;
        auto v1 = _edges[f.edges[1]].vertex;
        auto v2 = _edges[f.edges[2]].vertex;
        auto v3 = _edges[f.edges[3]].vertex;
        _vertices[v0].newVertex = getNewVertex(f.edges[0]);
        _vertices[v1].newVertex = getNewVertex(f.edges[1]);
        _vertices[v2].newVertex = getNewVertex(f.edges[2]);
        _vertices[v3].newVertex = getNewVertex(f.edges[3]);
    }

    // Build new quad
    Quad newQuad;
    for (auto &v: newVertices) {
        newQuad.addVertex(v);
    }

    for (auto &f: _faces) {
        auto e0 = _edges[f.edges[0]];
        auto e1 = _edges[f.edges[1]];
        auto e2 = _edges[f.edges[2]];
        auto e3 = _edges[f.edges[3]];

        auto v0 = _vertices[e0.vertex].newVertex;
        auto v1 = _vertices[e1.vertex].newVertex;
        auto v2 = _vertices[e2.vertex].newVertex;
        auto v3 = _vertices[e3.vertex].newVertex;

        auto mp0 = e0.midpoint;
        auto mp1 = e1.midpoint;
        auto mp2 = e2.midpoint;
        auto mp3 = e3.midpoint;

        auto center = f.center;

        newQuad.addFace(v0, mp0, center, mp3);
        newQuad.addFace(v1, mp1, center, mp0);
        newQuad.addFace(v2, mp2, center, mp1);
        newQuad.addFace(v3, mp3, center, mp2);
    }

    if (level > 1) {
        return newQuad.subdivide(level - 1);
    }
    return newQuad;
}

// Calculate smooth normals for each vertex by averaging normal of each face
// that vertex is a part of
void Quad::calculateNormals()
{
    // Calculate normal for each pair of edges in face, and average together
    // to get face normal. I ~think~ this should work for slightly coplanar
    // faces.
    for (auto &f: _faces) {
        auto v0 = &_vertices[_edges[f.edges[0]].vertex];
        auto v1 = &_vertices[_edges[f.edges[1]].vertex];
        auto v2 = &_vertices[_edges[f.edges[2]].vertex];
        auto v3 = &_vertices[_edges[f.edges[3]].vertex];
        auto n0 = ((v0->position - v1->position).getCrossed(v0->position - v3->position)).getNormalized();
        auto n1 = ((v2->position - v3->position).getCrossed(v2->position - v1->position)).getNormalized();
        f.normal = (n0 + n1) / 2.0;
    }

    // Calculate smoothed normal for each vertex
    for (auto &edge: _edges) {
        int numNormals = 0;
        ofVec3f sumNormals = {0.0, 0.0, 0.0};
        auto e = edge;
        do {
            sumNormals += _faces[e.face].normal;
            numNormals++;
            e = _edges[_edges[e.opposite].next];
        } while (e.face != edge.face);
        _vertices[edge.vertex].normal = sumNormals / numNormals;
    }
}


/*=========================================================================
 *
 *  Copyright 2011-2013 The University of North Carolina at Chapel Hill
 *  All rights reserved.
 *
 *  Licensed under the MADAI Software License. You may obtain a copy of
 *  this license at
 *
 *         https://madai-public.cs.unc.edu/visualization/software-license/
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *=========================================================================*/

#include <cassert>
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <boost/iostreams/filtering_streambuf.hpp>
#include <boost/iostreams/filter/gzip.hpp>

#include "ApplicationUtilities.h"
#include "Defaults.h"
#include "ExternalModel.h"
#include "MetropolisHastingsSampler.h"
#include "GaussianProcessEmulator.h"
#include "GaussianProcessEmulatorDirectoryFormatIO.h"
#include "GaussianProcessEmulatedModel.h"
#include "Paths.h"
#include "PercentileGridSampler.h"
#include "RuntimeParameterFileReader.h"
#include "SamplerCSVWriter.h"

#include "madaisys/SystemTools.hxx"

int main(int argc, char ** argv) {

  if (argc < 3) {
    std::cerr
      << "Usage:\n"
      << "    " << argv[0] << " <StatisticsDirectory> <OutputFileName>\n"
      << "\n"
      << "This program produces a Markov Chain Monte Carlo trace by either \n"
      << "evaluating a model defined in an external process or evaluating a \n"
      << "trained emulator. The program madai_pca_decompose must have been \n"
      << "run on <StatisticsDirectory> prior to running this program and if \n"
      << "no EXTERNAL_MODEL_EXECUTABLE is specified in the settings file, \n"
      << "madai_train_emulator must have been run as well.\n"
      << "\n"
      << "<StatisticsDirectory> is the directory in which all \n"
      << "statistics data are stored. It contains the parameter file "
      << madai::Paths::RUNTIME_PARAMETER_FILE << "\n"
      << "\n"
      << "<OutputFileName> is the name of the comma-separated value-format \n"
      << "file in which the trace will be written. This file will be \n"
      << "written in the directory <StatisticsDirectory>/trace/.\n"
      << "\n"
      << "Format of entries in " << madai::Paths::RUNTIME_PARAMETER_FILE
      << ":\n\n"
      << "MODEL_OUTPUT_DIRECTORY <value> (default: "
      << madai::Defaults::MODEL_OUTPUT_DIRECTORY << ")\n"
      << "EXPERIMENTAL_RESULTS_FILE <value> (default: "
      << madai::Defaults::EXPERIMENTAL_RESULTS_FILE << ")\n"
      << "SAMPLER <value> (default: "
      << madai::Defaults::SAMPLER << ")\n"
      << "SAMPLER_NUMBER_OF_SAMPLES <value> (default: "
      << madai::Defaults::SAMPLER_NUMBER_OF_SAMPLES << ")\n"
      << "SAMPLER_INACTIVE_PARAMETERS_FILE <value> (default: "
      << madai::Defaults::SAMPLER_INACTIVE_PARAMETERS_FILE << ")\n"
      << "MCMC_NUMBER_OF_BURN_IN_SAMPLES <value> (default: "
      << madai::Defaults::MCMC_NUMBER_OF_BURN_IN_SAMPLES << ")\n"
      << "MCMC_USE_MODEL_ERROR <value> (default: "
      << madai::Defaults::MCMC_USE_MODEL_ERROR << ")\n"
      << "MCMC_STEP_SIZE <value> (default: "
      << madai::Defaults::MCMC_STEP_SIZE << ")\n"
      << "EXTERNAL_MODEL_EXECUTABLE <value> (default: \""
      << madai::Defaults::EXTERNAL_MODEL_EXECUTABLE << "\")\n"
      << "EXTERNAL_MODEL_ARGUMENTS <Argument1> <Argument2> ... <LastArgument>\n"
      << "VERBOSE <value> (default: "
      << madai::Defaults::VERBOSE << ")\n";

    return EXIT_FAILURE;
  }
  std::string statisticsDirectory( argv[1] );
  madai::EnsurePathSeparatorAtEnd( statisticsDirectory );

  madai::RuntimeParameterFileReader settings;
  std::string settingsFile = statisticsDirectory + madai::Paths::RUNTIME_PARAMETER_FILE;
  if ( !settings.ParseFile( settingsFile ) ) {
    std::cerr << "Could not open runtime parameter file '" << settingsFile << "'\n";
    return EXIT_FAILURE;
  }

  std::string modelOutputDirectory =
    madai::GetModelOutputDirectory( statisticsDirectory, settings );
  std::string experimentalResultsFile =
    madai::GetExperimentalResultsFile( statisticsDirectory, settings );

  std::string samplerType = settings.GetOption( "SAMPLER", madai::Defaults::SAMPLER );

  int numberOfSamples = settings.GetOptionAsInt(
      "SAMPLER_NUMBER_OF_SAMPLES",
      madai::Defaults::SAMPLER_NUMBER_OF_SAMPLES);

  int numberOfBurnInSamples = settings.GetOptionAsInt(
      "MCMC_NUMBER_OF_BURN_IN_SAMPLES",
      madai::Defaults::MCMC_NUMBER_OF_BURN_IN_SAMPLES);

  bool useModelError = settings.GetOptionAsBool(
      "MCMC_USE_MODEL_ERROR",
      madai::Defaults::MCMC_USE_MODEL_ERROR);

  double stepSize = settings.GetOptionAsDouble(
      "MCMC_STEP_SIZE", madai::Defaults::MCMC_STEP_SIZE);

  std::string executable = settings.GetOption(
      "EXTERNAL_MODEL_EXECUTABLE",
      madai::Defaults::EXTERNAL_MODEL_EXECUTABLE);

  bool verbose = settings.GetOptionAsBool( "VERBOSE", madai::Defaults::VERBOSE );

  bool writeLogLikelihoodGradients = settings.GetOptionAsBool(
      "WRITE_LOGLIKELIHOOD_GRADIENTS",
      madai::Defaults::WRITE_LOGLIKELIHOOD_GRADIENTS );

  bool compressed = settings.GetOptionAsBool( "COMPRESS_TRACE", madai::Defaults::COMPRESS_TRACE );

  madai::ExternalModel externalModel;
  madai::GaussianProcessEmulatedModel gpem;

  madai::Model * model;
  if ( executable == "" ) { // Use emulator
    bool useModelError = settings.GetOptionAsBool(
        "PCA_USE_MODEL_ERROR", madai::Defaults::PCA_USE_MODEL_ERROR );
    madai::GaussianProcessEmulator gpe(useModelError);
    madai::GaussianProcessEmulatorDirectoryFormatIO directoryReader;
    if ( !directoryReader.LoadTrainingData( &gpe,
                                            modelOutputDirectory,
                                            statisticsDirectory,
                                            experimentalResultsFile ) ) {
      std::cerr << "Error loading training data from the directory structure.\n";
      return EXIT_FAILURE;
    }
    if ( !directoryReader.LoadPCA( &gpe, statisticsDirectory ) ) {
      std::cerr << "Error loading the PCA decomposition data. Did you "
                << "run madai_pca_decompose?\n";
      return EXIT_FAILURE;
    }
    if ( !directoryReader.LoadEmulator( &gpe, statisticsDirectory ) ) {
      std::cerr << "Error loading emulator data. Did you run "
                << "madai_train_emulator?\n";
      return EXIT_FAILURE;
    }

    gpem.SetGaussianProcessEmulator( gpe );

    model = &gpem;

    if ( verbose ) {
      std::cout << "Using emulator to generate trace.\n";
    }
  } else { // Use external model

    // Split arguments into vector of strings
    std::vector< std::string > arguments;
    if ( settings.HasOption( "EXTERNAL_MODEL_ARGUMENTS" ) ) {
      std::string argumentsString =
        settings.GetOption( "EXTERNAL_MODEL_ARGUMENTS" );
      arguments = madai::SplitString( argumentsString, ' ' );
    }

    if ( verbose ) {
      std::cout << "Using external model executable '" << executable << "'.\n";
    }

    externalModel.StartProcess( executable, arguments );
    if (! externalModel.IsReady()) {
      std::cerr << "Something is wrong with the external model\n";
      return EXIT_FAILURE;
    }

    model = &externalModel;
  }

  std::ifstream experimentalResults(experimentalResultsFile.c_str());
  if ( madai::Model::NO_ERROR !=
       madai::LoadObservations( model, experimentalResults ) ) {
    std::cerr << "Error loading observations.\n";
    externalModel.StopProcess();
    return EXIT_FAILURE;
  }
  experimentalResults.close();

  madai::Sampler * sampler;

  madai::PercentileGridSampler pgs;
  madai::MetropolisHastingsSampler mhs;
  if ( samplerType == "PercentileGrid" ) {
    pgs.SetModel( model );

    // Burn-in samples don't exist for a percentile grid sampling
    numberOfBurnInSamples = 0;

    sampler = &pgs;

    if ( verbose ) {
      std::cout << "Using PercentileGridSampler for sampling\n";
    }
  } else { // Default to Metropolis Hastings
    mhs.SetModel( model );
    mhs.SetStepSize( stepSize );

    sampler = &mhs;

    if ( verbose ) {
      std::cout << "Using MetropolisHastingsSampler for sampling\n";
    }
  }

  // Potentially set some parameters to inactive
  std::string samplerInactiveParametersFile =
    madai::GetInactiveParametersFile( statisticsDirectory, settings );
  if ( samplerInactiveParametersFile != "" ) {
    if ( ! madai::SetInactiveParameters( samplerInactiveParametersFile,
                                         *sampler, verbose ) ) {
      std::cerr << "Error when setting inactive parameters from file '"
                << samplerInactiveParametersFile << "'.\n";
      return EXIT_FAILURE;
    }
  }

  if ( samplerType == "PercentileGrid" ) {
    assert( pgs.GetModel() != NULL );
    pgs.SetNumberOfSamples(numberOfSamples);
    numberOfSamples = pgs.GetNumberOfSamples();
    if ( verbose ) {
      std::cout << "Number of grid samples: " << numberOfSamples << "\n";
    }
  }

  std::string outputFilePath( argv[2] );
  std::ofstream outFile(outputFilePath.c_str(), std::ios_base::out | std::ios_base::binary);
  if ( !outFile.good() ) {
    std::cerr << "Could not open trace file '" << outputFilePath << "' for writing.\n";
    return EXIT_FAILURE;
  }
  boost::iostreams::filtering_streambuf<boost::iostreams::output> outbuf;
  if( compressed ) {
      outbuf.push(boost::iostreams::gzip_compressor());
  }
  outbuf.push(outFile);
  std::ostream outFileStream(&outbuf);

  std::ostream * progressStream = verbose ? (& std::cerr) : NULL;
  int returnCode = madai::SamplerCSVWriter::GenerateSamplesAndSaveToFile(
      *sampler,
      *model,
      outFileStream,
      numberOfSamples,
      numberOfBurnInSamples,
      useModelError,
      writeLogLikelihoodGradients,
      progressStream);

  if ( verbose ) {
    if ( returnCode == EXIT_SUCCESS ) {
      std::cout << "Succeeded writing trace file '" << outputFilePath << "'.\n";
    } else {
      std::cerr << "Could not write trace file '" << outputFilePath << "'.\n";
    }
  }

  return returnCode;
}


/*ckwg +29
 * Copyright 2014-2016 by Kitware, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  * 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.
 *
 *  * Neither name of Kitware, Inc. nor the names of any contributors may be used
 *    to endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * 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 AUTHORS 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.
 */

/**
 * \file
 * \brief Implementation of kwiver::arrows::transform functions to apply
 * similarity transformations
 */

#include "transform.h"
#include <Eigen/Geometry>
#include <Eigen/SVD>

namespace kwiver {
namespace arrows {

/// Transform the camera by applying a similarity transformation in place
void
transform_inplace(const vital::similarity_d& xform,
                  vital::simple_camera_perspective& cam)
{
  cam.set_center( xform * cam.get_center() );
  cam.set_rotation( cam.get_rotation() * xform.rotation().inverse() );
  cam.set_center_covar( transform(cam.get_center_covar(), xform) );
}

/// Transform the landmark by applying a similarity transformation in place
template <typename T>
void
transform_inplace(const vital::similarity_<T>& xform,
                  vital::landmark_<T>& lm)
{
  lm.set_loc( xform * lm.get_loc() );
  lm.set_scale( lm.get_scale() * xform.scale() );
  lm.set_covar( transform(lm.get_covar(), xform) );
}

/// Transform a 3D covariance matrix with a similarity transformation
template <typename T>
vital::covariance_<3,T> transform(const vital::covariance_<3,T>& covar,
                                  const vital::similarity_<T>& xform)
{
  // TODO trasform covariance parameters directly
  // instead of converting to matrix form and back
  Eigen::Matrix<T,3,3> C(covar.matrix());
  Eigen::Matrix<T,3,3> sR(xform.rotation().matrix());
  sR /= xform.scale();
  C = sR * C * sR.transpose();
  return vital::covariance_<3,T>(C);
}

/// construct a transformed camera by applying a similarity transformation
vital::camera_perspective_sptr transform(vital::camera_perspective_sptr cam,
                                         const vital::similarity_d& xform)
{
  cam = std::dynamic_pointer_cast<vital::camera_perspective>(cam->clone());
  if( vital::simple_camera_perspective* vcam =
      dynamic_cast<vital::simple_camera_perspective*>(cam.get()) )
  {
    transform_inplace(xform, *vcam);
  }
  else
  {
    vital::simple_camera_perspective* new_cam =
        new vital::simple_camera_perspective( xform * cam->center(),
                                  cam->rotation() * xform.rotation().inverse(),
                                  cam->intrinsics() );
    new_cam->set_center_covar( transform(cam->center_covar(), xform) );
    cam = vital::camera_perspective_sptr( new_cam );
  }
  return cam;
}

/// construct a transformed map of cameras by applying a similarity transformation
vital::camera_map_sptr transform(vital::camera_map_sptr cameras,
                                 const vital::similarity_d& xform)
{
  vital::camera_map::map_camera_t cam_map = cameras->cameras();
  for(vital::camera_map::map_camera_t::value_type& p : cam_map)
  {
    auto cam_ptr = std::dynamic_pointer_cast<vital::camera_perspective>(p.second);
    p.second = transform(cam_ptr, xform);
  }
  return vital::camera_map_sptr(new vital::simple_camera_map(cam_map));
}

/// construct a transformed landmark by applying a similarity transformation
vital::landmark_sptr transform(vital::landmark_sptr lm,
                               const vital::similarity_d& xform)
{
  if (!lm)
  {
    return vital::landmark_sptr();
  }
  lm = lm->clone();
  if( vital::landmark_d* vlm = dynamic_cast<vital::landmark_d*>(lm.get()) )
  {
    transform_inplace(xform, *vlm);
  }
  else if( vital::landmark_f* vlm = dynamic_cast<vital::landmark_f*>(lm.get()) )
  {
    transform_inplace(vital::similarity_f(xform), *vlm);
  }
  else
  {
    auto new_lm = std::make_shared<vital::landmark_d>( *lm );
    new_lm->set_loc( xform * lm->loc() );
    new_lm->set_scale( lm->scale() * xform.scale() );
    new_lm->set_covar( transform(lm->covar(), xform) );
    lm = new_lm;
  }
  return lm;
}

/// construct a transformed map of landmarks by applying a similarity transformation
vital::landmark_map_sptr transform(vital::landmark_map_sptr landmarks,
                                   const vital::similarity_d& xform)
{
  vital::landmark_map::map_landmark_t lm_map = landmarks->landmarks();
  for(vital::landmark_map::map_landmark_t::value_type& p : lm_map)
  {
    p.second = transform(p.second, xform);
  }
  return vital::landmark_map_sptr(new vital::simple_landmark_map(lm_map));
}

/// Compute an approximate Necker reversal of cameras and landmarks
void
necker_reverse(vital::camera_map_sptr& cameras,
               vital::landmark_map_sptr& landmarks)
{
  typedef vital::landmark_map::map_landmark_t lm_map_t;
  typedef vital::camera_map::map_camera_t cam_map_t;

  cam_map_t cams = cameras->cameras();
  lm_map_t lms = landmarks->landmarks();

  // compute the landmark location mean and covariance
  vital::vector_3d lc(0.0, 0.0, 0.0);
  vital::matrix_3x3d covar = vital::matrix_3x3d::Zero();
  for(const lm_map_t::value_type& p : lms)
  {
    vital::vector_3d pt = p.second->loc();
    lc += pt;
    covar += pt * pt.transpose();
  }
  const double num_lm = static_cast<double>(lms.size());
  lc /= num_lm;
  covar /= num_lm;
  covar -= lc * lc.transpose();

  // the mirroring plane will pass through the landmark centeroid (lc)
  // and have a normal vector aligned with the smallest eigenvector of covar
  Eigen::JacobiSVD<vital::matrix_3x3d> svd(covar, Eigen::ComputeFullV);
  vital::vector_3d axis = svd.matrixV().col(2);

  // flip cameras around
  vital::rotation_d Ra180(vital::vector_4d(axis.x(), axis.y(), axis.z(), 0.0));
  vital::rotation_d Rz180(vital::vector_4d(0.0, 0.0, 1.0, 0.0));
  for(cam_map_t::value_type& p : cams)
  {
    auto flipped = std::make_shared<vital::simple_camera_perspective>(
      dynamic_cast<vital::simple_camera_perspective&>(*p.second));
    // extract the camera center
    const vital::vector_3d cc = flipped->center();
    // extract the camera principal axis
    vital::vector_3d pa = flipped->rotation().matrix().row(2);
    // compute the distance from cc along pa until intersection with
    // the mirroring plane of the points
    const double dist = (lc - cc).dot(axis) / pa.dot(axis);
    // compute the ground point where the principal axis
    // intersects the mirroring plane
    vital::vector_3d gp = cc + dist * pa;
    // rotate the camera center 180 degrees about the mirroring plane normal
    // axis centered at gp, also rotate the camera 180 about its principal axis
    flipped->set_center(Ra180 * (flipped->center() - gp) + gp);
    flipped->set_rotation(Rz180 * flipped->rotation() * Ra180);
    p.second = vital::camera_perspective_sptr(flipped);
  }

  // mirror landmark locations about the mirroring plane
  for(lm_map_t::value_type& p : lms)
  {
    vital::vector_3d v = p.second->loc();
    v -= 2.0 * (v - lc).dot(axis) * axis;
    auto new_lm = std::make_shared<vital::landmark_d>(*p.second);
    new_lm->set_loc(v);
    p.second = new_lm;
  }

  cameras = vital::camera_map_sptr(new vital::simple_camera_map(cams));
  landmarks = vital::landmark_map_sptr(new vital::simple_landmark_map(lms));
}

/// \cond DoxygenSuppress
#define INSTANTIATE_TRANSFORM(T) \
template KWIVER_ALGO_CORE_EXPORT vital::covariance_<3,T> \
transform(const vital::covariance_<3,T>& covar, \
          const vital::similarity_<T>& xform); \
template KWIVER_ALGO_CORE_EXPORT void \
transform_inplace(const vital::similarity_<T>& xform, \
                  vital::landmark_<T>& cam);

INSTANTIATE_TRANSFORM(double);
INSTANTIATE_TRANSFORM(float);

#undef INSTANTIATE_TRANSFORM
/// \endcond

} // end namespace arrows
} // end namespace kwiver


/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2020 NKI/AVL, Netherlands Cancer Institute
 *
 * 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 OWNER 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.
 */

// Calculate DSSP-like secondary structure information

#include <numeric>
#include <iomanip>
#include <thread>

#include <boost/algorithm/string.hpp>

#include "cif++/Structure.hpp"
#include "cif++/Secondary.hpp"

namespace ba = boost::algorithm;

// --------------------------------------------------------------------

namespace mmcif
{

struct Res;

enum ResidueType
{
	kUnknownResidue,

	//
	kAlanine,				// A	ala
	kArginine,				// R	arg
	kAsparagine,			// N	asn
	kAsparticAcid,			// D	asp
	kCysteine,				// C	cys
	kGlutamicAcid,			// E	glu
	kGlutamine,				// Q	gln
	kGlycine,				// G	gly
	kHistidine,				// H	his
	kIsoleucine,			// I	ile
	kLeucine,				// L	leu
	kLysine,				// K	lys
	kMethionine,			// M	met
	kPhenylalanine,			// F	phe
	kProline,				// P	pro
	kSerine,				// S	ser
	kThreonine,				// T	thr
	kTryptophan,			// W	trp
	kTyrosine,				// Y	tyr
	kValine,				// V	val

	kResidueTypeCount
};

struct ResidueInfo
{
	ResidueType		type;
	char			code;
	char			name[4];
};

const ResidueInfo kResidueInfo[] = {
	{ kUnknownResidue,	'X', "UNK" },
	{ kAlanine,			'A', "ALA" },
	{ kArginine,		'R', "ARG" },
	{ kAsparagine,		'N', "ASN" },
	{ kAsparticAcid,	'D', "ASP" },
	{ kCysteine,		'C', "CYS" },
	{ kGlutamicAcid,	'E', "GLU" },
	{ kGlutamine,		'Q', "GLN" },
	{ kGlycine,			'G', "GLY" },
	{ kHistidine,		'H', "HIS" },
	{ kIsoleucine,		'I', "ILE" },
	{ kLeucine,			'L', "LEU" },
	{ kLysine,			'K', "LYS" },
	{ kMethionine,		'M', "MET" },
	{ kPhenylalanine,	'F', "PHE" },
	{ kProline,			'P', "PRO" },
	{ kSerine,			'S', "SER" },
	{ kThreonine,		'T', "THR" },
	{ kTryptophan,		'W', "TRP" },
	{ kTyrosine,		'Y', "TYR" },
	{ kValine,			'V', "VAL" }
};

ResidueType MapResidue(std::string inName)
{
	ba::trim(inName);

	ResidueType result = kUnknownResidue;

	for (auto& ri: kResidueInfo)
	{
		if (inName == ri.name)
		{
			result = ri.type;
			break;
		}
	}

	return result;
}

struct HBond
{
	Res*	residue;
	double	energy;
};

enum BridgeType
{
	btNoBridge, btParallel, btAntiParallel
};

struct Bridge
{
	BridgeType				type;
	uint32_t				sheet, ladder;
	std::set<Bridge*>		link;
	std::deque<uint32_t>	i, j;
	std::string				chainI, chainJ;

	bool			operator<(const Bridge& b) const		{ return chainI < b.chainI or (chainI == b.chainI and i.front() < b.i.front()); }
};

struct BridgeParner
{
	Res*		residue;
	uint32_t	ladder;
	bool		parallel;
};

// --------------------------------------------------------------------

const float
	// kSSBridgeDistance = 3.0f,
	kMinimalDistance = 0.5f,
	kMinimalCADistance = 9.0f,
	kMinHBondEnergy = -9.9f,
	kMaxHBondEnergy = -0.5f,
	kCouplingConstant = -27.888f,	//	= -332 * 0.42 * 0.2
	kMaxPeptideBondLength = 2.5f;

const float
	kRadiusN = 1.65f,
	kRadiusCA = 1.87f,
	kRadiusC = 1.76f,
	kRadiusO = 1.4f,
	kRadiusSideAtom = 1.8f,
	kRadiusWater = 1.4f;

struct Res
{
	Res(const Monomer& m, int nr, ChainBreak brk)
		: mM(m), mNumber(nr)
		, mType(MapResidue(m.compoundID()))
		, mChainBreak(brk)
	{
		// update the box containing all atoms
		mBox[0].mX = mBox[0].mY = mBox[0].mZ =  std::numeric_limits<float>::max();
		mBox[1].mX = mBox[1].mY = mBox[1].mZ = -std::numeric_limits<float>::max();

		mH = mmcif::Point{ std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max() };

		for (auto& a: mM.unique_atoms())
		{
			if (a.labelAtomID() == "CA")
			{
				mCAlpha = a.location();
				ExtendBox(mCAlpha, kRadiusCA + 2 * kRadiusWater);
			}
			else if (a.labelAtomID() == "C")
			{
				mC = a.location();
				ExtendBox(mC, kRadiusC + 2 * kRadiusWater);
			}
			else if (a.labelAtomID() == "N")
			{
				mH = mN = a.location();
				ExtendBox(mN, kRadiusN + 2 * kRadiusWater);
			}
			else if (a.labelAtomID() == "O")
			{
				mO = a.location();
				ExtendBox(mO, kRadiusO + 2 * kRadiusWater);
			}
			else if (a.type() != AtomType::H)
			{
				mSideChain.push_back(a.location());
				ExtendBox(a.location(), kRadiusSideAtom + 2 * kRadiusWater);
			}
		}

		mRadius = mBox[1].mX - mBox[0].mX;
		if (mRadius < mBox[1].mY - mBox[0].mY)
			mRadius = mBox[1].mY - mBox[0].mY;
		if (mRadius < mBox[1].mZ - mBox[0].mZ)
			mRadius = mBox[1].mZ - mBox[0].mZ;

		mCenter.mX = (mBox[0].mX + mBox[1].mX) / 2;
		mCenter.mY = (mBox[0].mY + mBox[1].mY) / 2;
		mCenter.mZ = (mBox[0].mZ + mBox[1].mZ) / 2;
	}

	void assignHydrogen()
	{
		// assign the Hydrogen
		mH = mN;

		if (mType != kProline and mPrev != nullptr)
		{
			auto pc = mPrev->mC;
			auto po = mPrev->mO;

			float CODistance = static_cast<float>(Distance(pc, po));

			mH.mX += (pc.mX - po.mX) / CODistance;
			mH.mY += (pc.mY - po.mY) / CODistance;
			mH.mZ += (pc.mZ - po.mZ) / CODistance;
		}
	}

	void SetSecondaryStructure(SecondaryStructureType inSS)	{ mSecondaryStructure = inSS; }
	SecondaryStructureType GetSecondaryStructure() const	{ return mSecondaryStructure; }

	void SetBetaPartner(uint32_t n, Res& inResidue, uint32_t inLadder, bool inParallel)
	{
		assert(n == 0 or n == 1);

		mBetaPartner[n].residue = &inResidue;
		mBetaPartner[n].ladder = inLadder;
		mBetaPartner[n].parallel = inParallel;
	}

	BridgeParner GetBetaPartner(uint32_t n) const
	{
		assert(n == 0 or n == 1);
		return mBetaPartner[n];
	}

	void SetSheet(uint32_t inSheet)						{ mSheet = inSheet; }
	uint32_t GetSheet() const							{ return mSheet; }

	bool IsBend() const									{ return mBend; }
	void SetBend(bool inBend)							{ mBend = inBend; }

	Helix GetHelixFlag(HelixType helixType) const
	{
		size_t stride = static_cast<size_t>(helixType);
		assert(stride < 4);
		return mHelixFlags[stride];
	}

	bool IsHelixStart(HelixType helixType) const
	{
		size_t stride = static_cast<size_t>(helixType);
		assert(stride < 4);
		return mHelixFlags[stride] == Helix::Start or mHelixFlags[stride] == Helix::StartAndEnd;
	}

	void SetHelixFlag(HelixType helixType, Helix inHelixFlag)
	{
		size_t stride = static_cast<size_t>(helixType);
		assert(stride < 4);
		mHelixFlags[stride] = inHelixFlag;
	}

	void SetSSBridgeNr(uint8_t inBridgeNr)
	{
		if (mType != kCysteine)
			throw std::runtime_error("Only cysteine residues can form sulphur bridges");
		mSSBridgeNr = inBridgeNr;
	}

	uint8_t GetSSBridgeNr() const
	{
		if (mType != kCysteine)
			throw std::runtime_error("Only cysteine residues can form sulphur bridges");
		return mSSBridgeNr;
	}

	double CalculateSurface(const std::vector<Res>& inResidues);
	double CalculateSurface(const Point& inAtom, float inRadius, const std::vector<Res*>& inNeighbours);

	bool AtomIntersectsBox(const Point& atom, float inRadius) const
	{
		return
			atom.mX + inRadius >= mBox[0].mX and
			atom.mX - inRadius <= mBox[1].mX and
			atom.mY + inRadius >= mBox[0].mY and
			atom.mY - inRadius <= mBox[1].mY and
			atom.mZ + inRadius >= mBox[0].mZ and
			atom.mZ - inRadius <= mBox[1].mZ;
	}

	void ExtendBox(const Point& atom, float inRadius)
	{
		if (mBox[0].mX > atom.mX - inRadius)
			mBox[0].mX = atom.mX - inRadius;
		if (mBox[0].mY > atom.mY - inRadius)
			mBox[0].mY = atom.mY - inRadius;
		if (mBox[0].mZ > atom.mZ - inRadius)
			mBox[0].mZ = atom.mZ - inRadius;
		if (mBox[1].mX < atom.mX + inRadius)
			mBox[1].mX = atom.mX + inRadius;
		if (mBox[1].mY < atom.mY + inRadius)
			mBox[1].mY = atom.mY + inRadius;
		if (mBox[1].mZ < atom.mZ + inRadius)
			mBox[1].mZ = atom.mZ + inRadius;
	}

	Res* mNext = nullptr;
	Res* mPrev = nullptr;

	const Monomer& mM;
	std::string mAltID;

	int mNumber;

	Point mCAlpha, mC, mN, mO, mH;
	Point mBox[2] = {};
	float mRadius;
	Point mCenter;
	std::vector<Point> mSideChain;
	double mAccessibility = 0;

	ResidueType mType;
	uint8_t mSSBridgeNr = 0;
	SecondaryStructureType mSecondaryStructure = ssLoop;
	HBond mHBondDonor[2] = {}, mHBondAcceptor[2] = {};
	BridgeParner mBetaPartner[2] = {};
	uint32_t mSheet = 0;
	Helix mHelixFlags[4] = { Helix::None, Helix::None, Helix::None, Helix::None };	//
	bool mBend = false;
	ChainBreak mChainBreak = ChainBreak::None;
};

// --------------------------------------------------------------------

class Accumulator
{
	public:

	struct candidate
	{
		Point	location;
		double	radius;
		double	distance;

		bool operator<(const candidate& rhs) const
				{ return distance < rhs.distance; }
	};

	void operator()(const Point& a, const Point& b, double d, double r)
	{
		double distance = DistanceSquared(a, b);

		d += kRadiusWater;
		r += kRadiusWater;

		double test = d + r;
		test *= test;

		if (distance < test and distance > 0.0001)
		{
			candidate c = { b - a, r * r, distance };

			m_x.push_back(c);
			push_heap(m_x.begin(), m_x.end());
		}
	}

	void sort()
	{
		sort_heap(m_x.begin(), m_x.end());
	}

	std::vector<candidate>  m_x;
};

// we use a fibonacci sphere to calculate the even distribution of the dots
class MSurfaceDots
{
  public:

	static MSurfaceDots&  Instance();

	size_t size() const							{ return mPoints.size(); }
	const Point& operator[](size_t inIx) const	{ return mPoints[inIx]; }
	double weight() const						{ return mWeight; }

  private:
	MSurfaceDots(int32_t inN);

	std::vector<Point> mPoints;
	double mWeight;
};

MSurfaceDots& MSurfaceDots::Instance()
{
	const int32_t kN = 200;

	static MSurfaceDots sInstance(kN);
	return sInstance;
}

MSurfaceDots::MSurfaceDots(int32_t N)
{
	auto P = 2 * N + 1;

	const float kGoldenRatio = (1 + std::sqrt(5.0f)) / 2;

	mWeight = (4 * kPI) / P;

	for (auto i = -N; i <= N; ++i)
	{
		float lat = std::asin((2.0f * i) / P);
		float lon = static_cast<float>(std::fmod(i, kGoldenRatio) * 2 * kPI / kGoldenRatio);

		mPoints.emplace_back(std::sin(lon) * std::cos(lat), std::cos(lon) * std::cos(lat), std::sin(lat));
	}
}

double Res::CalculateSurface(const Point& inAtom, float inRadius, const std::vector<Res*>& inNeighbours)
{
	Accumulator accumulate;

	for (auto r: inNeighbours)
	{
		if (r->AtomIntersectsBox(inAtom, inRadius))
		{
			accumulate(inAtom, r->mN, inRadius, kRadiusN);
			accumulate(inAtom, r->mCAlpha, inRadius, kRadiusCA);
			accumulate(inAtom, r->mC, inRadius, kRadiusC);
			accumulate(inAtom, r->mO, inRadius, kRadiusO);

			for (auto& atom: r->mSideChain)
				accumulate(inAtom, atom, inRadius, kRadiusSideAtom);
		}
	}

	accumulate.sort();

	float radius = inRadius + kRadiusWater;
	double surface = 0;

	MSurfaceDots& surfaceDots = MSurfaceDots::Instance();

	for (size_t i = 0; i < surfaceDots.size(); ++i)
	{
		Point xx = surfaceDots[i] * radius;

		bool free = true;
		for (size_t k = 0; free and k < accumulate.m_x.size(); ++k)
			free = accumulate.m_x[k].radius < DistanceSquared(xx, accumulate.m_x[k].location);

		if (free)
			surface += surfaceDots.weight();
	}

	return surface * radius * radius;
}

double Res::CalculateSurface(const std::vector<Res>& inResidues)
{
	std::vector<Res*> neighbours;

	for (auto& r: inResidues)
	{
		Point center = r.mCenter;
		double radius = r.mRadius;

		if (Distance(mCenter, center) < mRadius + radius)
			neighbours.push_back(const_cast<Res*>(&r));
	}

	mAccessibility = CalculateSurface(mN, kRadiusN, neighbours) +
					 CalculateSurface(mCAlpha, kRadiusCA, neighbours) +
					 CalculateSurface(mC, kRadiusC, neighbours) +
					 CalculateSurface(mO, kRadiusO, neighbours);

	for (auto& atom: mSideChain)
		mAccessibility += CalculateSurface(atom, kRadiusSideAtom, neighbours);

	return mAccessibility;
}

void CalculateAccessibilities(std::vector<Res>& inResidues, DSSP_Statistics& stats)
{
	stats.accessibleSurface = 0;
	for (auto& residue: inResidues)
		stats.accessibleSurface += residue.CalculateSurface(inResidues);
}

// --------------------------------------------------------------------
// TODO: use the angle to improve bond energy calculation.

double CalculateHBondEnergy(Res& inDonor, Res& inAcceptor)
{
	double result = 0;

	if (inDonor.mType != kProline)
	{
		double distanceHO = Distance(inDonor.mH, inAcceptor.mO);
		double distanceHC = Distance(inDonor.mH, inAcceptor.mC);
		double distanceNC = Distance(inDonor.mN, inAcceptor.mC);
		double distanceNO = Distance(inDonor.mN, inAcceptor.mO);

		if (distanceHO < kMinimalDistance or distanceHC < kMinimalDistance or distanceNC < kMinimalDistance or distanceNO < kMinimalDistance)
			result = kMinHBondEnergy;
		else
			result = kCouplingConstant / distanceHO - kCouplingConstant / distanceHC + kCouplingConstant / distanceNC - kCouplingConstant / distanceNO;

		// DSSP compatibility mode:
		result = round(result * 1000) / 1000;

		if (result < kMinHBondEnergy)
			result = kMinHBondEnergy;
	}

	// update donor
	if (result < inDonor.mHBondAcceptor[0].energy)
	{
		inDonor.mHBondAcceptor[1] = inDonor.mHBondAcceptor[0];
		inDonor.mHBondAcceptor[0].residue = &inAcceptor;
		inDonor.mHBondAcceptor[0].energy = result;
	}
	else if (result < inDonor.mHBondAcceptor[1].energy)
	{
		inDonor.mHBondAcceptor[1].residue = &inAcceptor;
		inDonor.mHBondAcceptor[1].energy = result;
	}

	// and acceptor
	if (result < inAcceptor.mHBondDonor[0].energy)
	{
		inAcceptor.mHBondDonor[1] = inAcceptor.mHBondDonor[0];
		inAcceptor.mHBondDonor[0].residue = &inDonor;
		inAcceptor.mHBondDonor[0].energy = result;
	}
	else if (result < inAcceptor.mHBondDonor[1].energy)
	{
		inAcceptor.mHBondDonor[1].residue = &inDonor;
		inAcceptor.mHBondDonor[1].energy = result;
	}

	return result;
}

// --------------------------------------------------------------------

void CalculateHBondEnergies(std::vector<Res>& inResidues)
{
	// Calculate the HBond energies
	for (uint32_t i = 0; i + 1 < inResidues.size(); ++i)
	{
		auto& ri = inResidues[i];

		for (uint32_t j = i + 1; j < inResidues.size(); ++j)
		{
			auto& rj = inResidues[j];

			if (Distance(ri.mCAlpha, rj.mCAlpha) < kMinimalCADistance)
			{
				CalculateHBondEnergy(ri, rj);
				if (j != i + 1)
					CalculateHBondEnergy(rj, ri);
			}
		}
	}
}

// --------------------------------------------------------------------

bool NoChainBreak(const Res* a, const Res* b)
{
	bool result = a->mM.asymID() == b->mM.asymID();
	for (auto r = a; result and r != b; r = r->mNext)
	{
		auto next = r->mNext;
		if (next == nullptr)
			result = false;
		else
			result = next->mNumber == r->mNumber + 1;
	}
	return result;
}

bool NoChainBreak(const Res& a, const Res& b)
{
	return NoChainBreak(&a, &b);
}

// --------------------------------------------------------------------

bool TestBond(const Res* a, const Res* b)
{
	return
		(a->mHBondAcceptor[0].residue == b and a->mHBondAcceptor[0].energy < kMaxHBondEnergy) or
		(a->mHBondAcceptor[1].residue == b and a->mHBondAcceptor[1].energy < kMaxHBondEnergy);
}

// --------------------------------------------------------------------

BridgeType TestBridge(const Res& r1, const Res& r2)
{										// I.	a	d	II.	a	d		parallel
	auto a = r1.mPrev;					//		  \			  /
	auto b = &r1;						//		b	e		b	e
	auto c = r1.mNext;					// 		  /			  \                      ..
	auto d = r2.mPrev;					//		c	f		c	f
	auto e = &r2;						//
	auto f = r2.mNext;					// III.	a <- f	IV. a	  f		antiparallel
										//
	BridgeType result = btNoBridge;		//		b	 e      b <-> e
										//
										//		c -> d		c     d

	if (a and c and NoChainBreak(a, c) and d and f and NoChainBreak(d, f))
	{
		if ((TestBond(c, e) and TestBond(e, a)) or (TestBond(f, b) and TestBond(b, d)))
			result = btParallel;
		else if ((TestBond(c, d) and TestBond(f, a)) or (TestBond(e, b) and TestBond(b, e)))
			result = btAntiParallel;
	}

	return result;
}

// --------------------------------------------------------------------
// return true if any of the residues in bridge a is identical to any of the residues in bridge b
bool Linked(const Bridge& a, const Bridge& b)
{
	return
		find_first_of(a.i.begin(), a.i.end(), b.i.begin(), b.i.end()) != a.i.end() or
		find_first_of(a.i.begin(), a.i.end(), b.j.begin(), b.j.end()) != a.i.end() or
		find_first_of(a.j.begin(), a.j.end(), b.i.begin(), b.i.end()) != a.j.end() or
		find_first_of(a.j.begin(), a.j.end(), b.j.begin(), b.j.end()) != a.j.end();
}

// --------------------------------------------------------------------

void CalculateBetaSheets(std::vector<Res>& inResidues, DSSP_Statistics& stats)
{
	// Calculate Bridges
	std::vector<Bridge> bridges;
	if (inResidues.size() > 4)
	{
		for (uint32_t i = 1; i + 4 < inResidues.size(); ++i)
		{
			auto& ri = inResidues[i];

			for (uint32_t j = i + 3; j + 1 < inResidues.size(); ++j)
			{
				auto& rj = inResidues[j];

				BridgeType type = TestBridge(ri, rj);
				if (type == btNoBridge)
					continue;

				bool found = false;
				for (Bridge& bridge : bridges)
				{
					if (type != bridge.type or i != bridge.i.back() + 1)
						continue;

					if (type == btParallel and bridge.j.back() + 1 == j)
					{
						bridge.i.push_back(i);
						bridge.j.push_back(j);
						found = true;
						break;
					}

					if (type == btAntiParallel and bridge.j.front() - 1 == j)
					{
						bridge.i.push_back(i);
						bridge.j.push_front(j);
						found = true;
						break;
					}
				}

				if (not found)
				{
					Bridge bridge = {};

					bridge.type = type;
					bridge.i.push_back(i);
					bridge.chainI = ri.mM.asymID();
					bridge.j.push_back(j);
					bridge.chainJ = rj.mM.asymID();

					bridges.push_back(bridge);
				}
			}
		}
	}

	// extend ladders
	std::sort(bridges.begin(), bridges.end());

	for (uint32_t i = 0; i < bridges.size(); ++i)
	{
		for (uint32_t j = i + 1; j < bridges.size(); ++j)
		{
			uint32_t ibi = bridges[i].i.front();
			uint32_t iei = bridges[i].i.back();
			uint32_t jbi = bridges[i].j.front();
			uint32_t jei = bridges[i].j.back();
			uint32_t ibj = bridges[j].i.front();
			uint32_t iej = bridges[j].i.back();
			uint32_t jbj = bridges[j].j.front();
			uint32_t jej = bridges[j].j.back();

			if (bridges[i].type != bridges[j].type or
				NoChainBreak(inResidues[std::min(ibi, ibj)], inResidues[std::max(iei, iej)]) == false or
				NoChainBreak(inResidues[std::min(jbi, jbj)], inResidues[std::max(jei, jej)]) == false or
				ibj - iei >= 6 or
				(iei >= ibj and ibi <= iej))
			{
				continue;
			}

			bool bulge;
			if (bridges[i].type == btParallel)
				bulge = ((jbj - jei < 6 and ibj - iei < 3) or (jbj - jei < 3));
			else
				bulge = ((jbi - jej < 6 and ibj - iei < 3) or (jbi - jej < 3));

			if (bulge)
			{
				bridges[i].i.insert(bridges[i].i.end(), bridges[j].i.begin(), bridges[j].i.end());
				if (bridges[i].type == btParallel)
					bridges[i].j.insert(bridges[i].j.end(), bridges[j].j.begin(), bridges[j].j.end());
				else
					bridges[i].j.insert(bridges[i].j.begin(), bridges[j].j.begin(), bridges[j].j.end());
				bridges.erase(bridges.begin() + j);
				--j;
			}
		}
	}

	// Sheet
	std::set<Bridge*> ladderset;
	for (Bridge& bridge : bridges)
	{
		ladderset.insert(&bridge);

		size_t n = bridge.i.size();
		if (n > kHistogramSize)
			n = kHistogramSize;

		if (bridge.type == btParallel)
			stats.parallelBridgesPerLadderHistogram[n - 1] += 1;
		else
			stats.antiparallelBridgesPerLadderHistogram[n - 1] += 1;
	}

	uint32_t sheet = 1, ladder = 0;
	while (not ladderset.empty())
	{
		std::set<Bridge*> sheetset;
		sheetset.insert(*ladderset.begin());
		ladderset.erase(ladderset.begin());

		bool done = false;
		while (not done)
		{
			done = true;
			for (Bridge* a : sheetset)
			{
				for (Bridge* b : ladderset)
				{
					if (Linked(*a, *b))
					{
						sheetset.insert(b);
						ladderset.erase(b);
						done = false;
						break;
					}
				}
				if (not done)
					break;
			}
		}

		for (Bridge* bridge : sheetset)
		{
			bridge->ladder = ladder;
			bridge->sheet = sheet;
			bridge->link = sheetset;

			++ladder;
		}

		size_t nrOfLaddersPerSheet = sheetset.size();
		if (nrOfLaddersPerSheet > kHistogramSize)
			nrOfLaddersPerSheet = kHistogramSize;
		if (nrOfLaddersPerSheet == 1 and (*sheetset.begin())->i.size() > 1)
			stats.laddersPerSheetHistogram[0] += 1;
		else if (nrOfLaddersPerSheet > 1)
			stats.laddersPerSheetHistogram[nrOfLaddersPerSheet - 1] += 1;

		++sheet;
	}

	for (Bridge& bridge : bridges)
	{
		// find out if any of the i and j set members already have
		// a bridge assigned, if so, we're assigning bridge 2

		uint32_t betai = 0, betaj = 0;

		for (uint32_t l : bridge.i)
		{
			if (inResidues[l].GetBetaPartner(0).residue != nullptr)
			{
				betai = 1;
				break;
			}
		}

		for (uint32_t l : bridge.j)
		{
			if (inResidues[l].GetBetaPartner(0).residue != nullptr)
			{
				betaj = 1;
				break;
			}
		}

		SecondaryStructureType ss = ssBetabridge;
		if (bridge.i.size() > 1)
			ss = ssStrand;

		if (bridge.type == btParallel)
		{
			stats.nrOfHBondsInParallelBridges += bridge.i.back() - bridge.i.front() + 2;

			std::deque<uint32_t>::iterator j = bridge.j.begin();
			for (uint32_t i : bridge.i)
				inResidues[i].SetBetaPartner(betai, inResidues[*j++], bridge.ladder, true);

			j = bridge.i.begin();
			for (uint32_t i : bridge.j)
				inResidues[i].SetBetaPartner(betaj, inResidues[*j++], bridge.ladder, true);
		}
		else
		{
			stats.nrOfHBondsInAntiparallelBridges += bridge.i.back() - bridge.i.front() + 2;

			std::deque<uint32_t>::reverse_iterator j = bridge.j.rbegin();
			for (uint32_t i : bridge.i)
				inResidues[i].SetBetaPartner(betai, inResidues[*j++], bridge.ladder, false);

			j = bridge.i.rbegin();
			for (uint32_t i : bridge.j)
				inResidues[i].SetBetaPartner(betaj, inResidues[*j++], bridge.ladder, false);
		}

		for (uint32_t i = bridge.i.front(); i <= bridge.i.back(); ++i)
		{
			if (inResidues[i].GetSecondaryStructure() != ssStrand)
				inResidues[i].SetSecondaryStructure(ss);
			inResidues[i].SetSheet(bridge.sheet);
		}

		for (uint32_t i = bridge.j.front(); i <= bridge.j.back(); ++i)
		{
			if (inResidues[i].GetSecondaryStructure() != ssStrand)
				inResidues[i].SetSecondaryStructure(ss);
			inResidues[i].SetSheet(bridge.sheet);
		}
	}
}

// --------------------------------------------------------------------
// TODO: improve alpha helix calculation by better recognizing pi-helices

void CalculateAlphaHelices(std::vector<Res>& inResidues, DSSP_Statistics& stats, bool inPreferPiHelices = true)
{
	// Helix and Turn
	for (HelixType helixType: { HelixType::rh_3_10, HelixType::rh_alpha, HelixType::rh_pi })
	{
		uint32_t stride = static_cast<uint32_t>(helixType) + 3;

		for (uint32_t i = 0; i + stride < inResidues.size(); ++i)
		{
			if (NoChainBreak(inResidues[i], inResidues[i + stride]) and TestBond(&inResidues[i + stride], &inResidues[i]))
			{
				inResidues[i + stride].SetHelixFlag(helixType, Helix::End);
				for (uint32_t j = i + 1; j < i + stride; ++j)
				{
					if (inResidues[j].GetHelixFlag(helixType) == Helix::None)
						inResidues[j].SetHelixFlag(helixType, Helix::Middle);
				}

				if (inResidues[i].GetHelixFlag(helixType) == Helix::End)
					inResidues[i].SetHelixFlag(helixType, Helix::StartAndEnd);
				else
					inResidues[i].SetHelixFlag(helixType, Helix::Start);
			}
		}
	}

	for (auto& r : inResidues)
	{
		double kappa = r.mM.kappa();
		r.SetBend(kappa != 360 and kappa > 70);
	}

	for (uint32_t i = 1; i + 4 < inResidues.size(); ++i)
	{
		if (inResidues[i].IsHelixStart(HelixType::rh_alpha) and inResidues[i - 1].IsHelixStart(HelixType::rh_alpha))
		{
			for (uint32_t j = i; j <= i + 3; ++j)
				inResidues[j].SetSecondaryStructure(ssAlphahelix);
		}
	}

	for (uint32_t i = 1; i + 3 < inResidues.size(); ++i)
	{
		if (inResidues[i].IsHelixStart(HelixType::rh_3_10) and inResidues[i - 1].IsHelixStart(HelixType::rh_3_10))
		{
			bool empty = true;
			for (uint32_t j = i; empty and j <= i + 2; ++j)
				empty = inResidues[j].GetSecondaryStructure() == ssLoop or inResidues[j].GetSecondaryStructure() == ssHelix_3;
			if (empty)
			{
				for (uint32_t j = i; j <= i + 2; ++j)
					inResidues[j].SetSecondaryStructure(ssHelix_3);
			}
		}
	}

	for (uint32_t i = 1; i + 5 < inResidues.size(); ++i)
	{
		if (inResidues[i].IsHelixStart(HelixType::rh_pi) and inResidues[i - 1].IsHelixStart(HelixType::rh_pi))
		{
			bool empty = true;
			for (uint32_t j = i; empty and j <= i + 4; ++j)
				empty = inResidues[j].GetSecondaryStructure() == ssLoop or inResidues[j].GetSecondaryStructure() == ssHelix_5 or
							(inPreferPiHelices and inResidues[j].GetSecondaryStructure() == ssAlphahelix);
			if (empty)
			{
				for (uint32_t j = i; j <= i + 4; ++j)
					inResidues[j].SetSecondaryStructure(ssHelix_5);
			}
		}
	}

	for (uint32_t i = 1; i + 1 < inResidues.size(); ++i)
	{
		if (inResidues[i].GetSecondaryStructure() == ssLoop)
		{
			bool isTurn = false;
			for (HelixType helixType: { HelixType::rh_3_10, HelixType::rh_alpha, HelixType::rh_pi })
			{
				uint32_t stride = 3 + static_cast<uint32_t>(helixType);
				for (uint32_t k = 1; k < stride and not isTurn; ++k)
					isTurn = (i >= k) and inResidues[i - k].IsHelixStart(helixType);
			}

			if (isTurn)
				inResidues[i].SetSecondaryStructure(ssTurn);
			else if (inResidues[i].IsBend())
				inResidues[i].SetSecondaryStructure(ssBend);
		}
	}

	std::string asym;
	size_t helixLength = 0;
	for (auto r: inResidues)
	{
		if (r.mM.asymID() != asym)
		{
			helixLength = 0;
			asym = r.mM.asymID();
		}

		if (r.GetSecondaryStructure() == ssAlphahelix)
			++helixLength;
		else if (helixLength > 0)
		{
			if (helixLength > kHistogramSize)
				helixLength = kHistogramSize;

			stats.residuesPerAlphaHelixHistogram[helixLength - 1] += 1;
			helixLength = 0;
		}
	}
}

// --------------------------------------------------------------------

void CalculatePPHelices(std::vector<Res>& inResidues, DSSP_Statistics& stats, int stretch_length)
{
	size_t N = inResidues.size();

	const float epsilon = 29;
	const float phi_min = -75 - epsilon;
	const float phi_max = -75 + epsilon;
	const float psi_min = 145 - epsilon;
	const float psi_max = 145 + epsilon;

	std::vector<float> phi(N), psi(N);

	for (uint32_t i = 1; i + 1 < inResidues.size(); ++i)
	{
		phi[i] = inResidues[i].mM.phi();
		psi[i] = inResidues[i].mM.psi();
	}

	for (uint32_t i = 1; i + 3 < inResidues.size(); ++i)
	{
		switch (stretch_length)
		{
			case 2:
			{
				if (phi_min > phi[i + 0] or phi[i + 0] > phi_max or
					phi_min > phi[i + 1] or phi[i + 1] > phi_max)
					continue;

				if (psi_min > psi[i + 0] or psi[i + 0] > psi_max or
					psi_min > psi[i + 1] or psi[i + 1] > psi_max)
					continue;

				// auto phi_avg = (phi[i + 0] + phi[i + 1]) / 2;
				// auto phi_sq = (phi[i + 0] - phi_avg) * (phi[i + 0] - phi_avg) +
				// 			  (phi[i + 1] - phi_avg) * (phi[i + 1] - phi_avg);

				// if (phi_sq >= 200)
				// 	continue;

				// auto psi_avg = (psi[i + 0] + psi[i + 1]) / 2;
				// auto psi_sq = (psi[i + 0] - psi_avg) * (psi[i + 0] - psi_avg) +
				// 			  (psi[i + 1] - psi_avg) * (psi[i + 1] - psi_avg);

				// if (psi_sq >= 200)
				// 	continue;

				switch (inResidues[i].GetHelixFlag(HelixType::rh_pp))
				{
					case Helix::None:
						inResidues[i].SetHelixFlag(HelixType::rh_pp, Helix::Start);
						break;

					case Helix::End:
						inResidues[i].SetHelixFlag(HelixType::rh_pp, Helix::Middle);
						break;

					default:
						break;
				}

				inResidues[i + 1].SetHelixFlag(HelixType::rh_pp, Helix::End);

				if (inResidues[i].GetSecondaryStructure() == SecondaryStructureType::ssLoop)
					inResidues[i].SetSecondaryStructure(SecondaryStructureType::ssHelix_PPII);
				if (inResidues[i + 1].GetSecondaryStructure() == SecondaryStructureType::ssLoop)
					inResidues[i + 1].SetSecondaryStructure(SecondaryStructureType::ssHelix_PPII);
			}
			break;

			case 3:
			{
				if (phi_min > phi[i + 0] or phi[i + 0] > phi_max or
					phi_min > phi[i + 1] or phi[i + 1] > phi_max or
					phi_min > phi[i + 2] or phi[i + 2] > phi_max)
					continue;

				if (psi_min > psi[i + 0] or psi[i + 0] > psi_max or
					psi_min > psi[i + 1] or psi[i + 1] > psi_max or
					psi_min > psi[i + 2] or psi[i + 2] > psi_max)
					continue;

				// auto phi_avg = (phi[i + 0] + phi[i + 1] + phi[i + 2]) / 3;
				// auto phi_sq = (phi[i + 0] - phi_avg) * (phi[i + 0] - phi_avg) +
				// 			  (phi[i + 1] - phi_avg) * (phi[i + 1] - phi_avg) +
				// 			  (phi[i + 2] - phi_avg) * (phi[i + 2] - phi_avg);

				// if (phi_sq >= 300)
				// 	continue;

				// auto psi_avg = (psi[i + 0] + psi[i + 1] + psi[i + 2]) / 3;
				// auto psi_sq = (psi[i + 0] - psi_avg) * (psi[i + 0] - psi_avg) +
				// 			  (psi[i + 1] - psi_avg) * (psi[i + 1] - psi_avg) +
				// 			  (psi[i + 2] - psi_avg) * (psi[i + 2] - psi_avg);

				// if (psi_sq >= 300)
				// 	continue;

				switch (inResidues[i].GetHelixFlag(HelixType::rh_pp))
				{
					case Helix::None:
						inResidues[i].SetHelixFlag(HelixType::rh_pp, Helix::Start);
						break;

					case Helix::End:
						inResidues[i].SetHelixFlag(HelixType::rh_pp, Helix::StartAndEnd);
						break;

					default:
						break;
				}

				inResidues[i + 1].SetHelixFlag(HelixType::rh_pp, Helix::Middle);
				inResidues[i + 2].SetHelixFlag(HelixType::rh_pp, Helix::End);

				if (inResidues[i + 0].GetSecondaryStructure() == SecondaryStructureType::ssLoop)
					inResidues[i + 0].SetSecondaryStructure(SecondaryStructureType::ssHelix_PPII);

				if (inResidues[i + 1].GetSecondaryStructure() == SecondaryStructureType::ssLoop)
					inResidues[i + 1].SetSecondaryStructure(SecondaryStructureType::ssHelix_PPII);

				if (inResidues[i + 2].GetSecondaryStructure() == SecondaryStructureType::ssLoop)
					inResidues[i + 2].SetSecondaryStructure(SecondaryStructureType::ssHelix_PPII);

				break;
			}

			default:
				throw std::runtime_error("Unsupported stretch length");
		}
	}
}

// --------------------------------------------------------------------

struct DSSPImpl
{
	DSSPImpl(const Structure& s, int min_poly_proline_stretch_length);

	const Structure&			mStructure;
	const std::list<Polymer>&	mPolymers;
	std::vector<Res>			mResidues;
	std::vector<std::pair<Res*,Res*>> mSSBonds;

	int m_min_poly_proline_stretch_length;

	auto findRes(const std::string& asymID, int seqID)
	{
		return std::find_if(mResidues.begin(), mResidues.end(), [&](auto& r) { return r.mM.asymID() == asymID and r.mM.seqID() == seqID; });
	}

	void calculateSurface();
	void calculateSecondaryStructure();

	DSSP_Statistics				mStats = {};
};

// --------------------------------------------------------------------

DSSPImpl::DSSPImpl(const Structure& s, int min_poly_proline_stretch_length)
	: mStructure(s)
	, mPolymers(mStructure.polymers())
	, m_min_poly_proline_stretch_length(min_poly_proline_stretch_length)
{
	size_t nRes = accumulate(mPolymers.begin(), mPolymers.end(),
		0ULL, [](size_t s, auto& p) { return s + p.size(); });

	mStats.nrOfChains = static_cast<uint32_t>(mPolymers.size());

	mResidues.reserve(nRes);
	int resNumber = 0;

	for (auto& p: mPolymers)
	{
		ChainBreak brk = ChainBreak::NewChain;

		for (auto& m: p)
		{
			if (not m.isComplete())
				continue;

			++resNumber;

			if (not mResidues.empty() and
				Distance(mResidues.back().mC, m.atomByID("N").location()) > kMaxPeptideBondLength)
			{
				if (mResidues.back().mM.asymID() == m.asymID())
				{
					++mStats.nrOfChains;
					brk = ChainBreak::Gap;
				}
				++resNumber;
			}

			mResidues.emplace_back(m, resNumber, brk);

			brk = ChainBreak::None;
		}
	}

	mStats.nrOfResidues = static_cast<uint32_t>(mResidues.size());

	for (size_t i = 0; i + 1 < mResidues.size(); ++i)
	{
		mResidues[i].mNext = &mResidues[i + 1];
		mResidues[i + 1].mPrev = &mResidues[i];

		mResidues[i + 1].assignHydrogen();
	}
}

void DSSPImpl::calculateSecondaryStructure()
{
	auto& db = mStructure.getFile().data();
	for (auto r: db["struct_conn"].find(cif::Key("conn_type_id") == "disulf"))
	{
		std::string asym1, asym2;
		int seq1, seq2;
		cif::tie(asym1, seq1, asym2, seq2) = r.get("ptnr1_label_asym_id", "ptnr1_label_seq_id", "ptnr2_label_asym_id", "ptnr2_label_seq_id");

		auto r1 = findRes(asym1, seq1);
		if (r1 == mResidues.end())
		{
			if (cif::VERBOSE)
				std::cerr << "Missing (incomplete?) residue for SS bond when trying to find " << asym1 << '/' << seq1 << std::endl;
			continue;
			// throw std::runtime_error("Invalid file, missing residue for SS bond");
		}

		auto r2 = findRes(asym2, seq2);
		if (r2 == mResidues.end())
		{
			if (cif::VERBOSE)
				std::cerr << "Missing (incomplete?) residue for SS bond when trying to find " << asym2 << '/' << seq2 << std::endl;
			continue;
			// throw std::runtime_error("Invalid file, missing residue for SS bond");
		}

		mSSBonds.emplace_back(&*r1, &*r2);
	}

	CalculateHBondEnergies(mResidues);
	CalculateBetaSheets(mResidues, mStats);
	CalculateAlphaHelices(mResidues, mStats);
	CalculatePPHelices(mResidues, mStats, m_min_poly_proline_stretch_length);

	if (cif::VERBOSE > 1)
	{
		for (auto& r: mResidues)
		{
			auto& m = r.mM;

			char helix[5] = { };
			for (HelixType helixType: { HelixType::rh_3_10, HelixType::rh_alpha, HelixType::rh_pi, HelixType::rh_pp	})
			{
				switch (r.GetHelixFlag(helixType))
				{
					case Helix::Start:			helix[static_cast<int>(helixType)] = '>'; break;
					case Helix::Middle:			helix[static_cast<int>(helixType)] = helixType == HelixType::rh_pp ? 'P' : '3' + static_cast<char>(helixType); break;
					case Helix::StartAndEnd:	helix[static_cast<int>(helixType)] = 'X'; break;
					case Helix::End:			helix[static_cast<int>(helixType)] = '<'; break;
					case Helix::None:			helix[static_cast<int>(helixType)] = ' '; break;
				}
			}

			auto id = m.asymID() + ':' + std::to_string(m.seqID()) + '/' + m.compoundID();

			std::cerr << id << std::string(12 - id.length(), ' ')
					<< char(r.mSecondaryStructure) << ' '
					<< helix
					<< std::endl;
		}
	}

	// finish statistics
	mStats.nrOfSSBridges = static_cast<uint32_t>(mSSBonds.size());

	mStats.nrOfIntraChainSSBridges = 0;
	uint8_t ssBondNr = 0;
	for (const auto& [a, b]: mSSBonds)
	{
		if (a == b)
		{
			if (cif::VERBOSE)
				std::cerr << "In the SS bonds list, the residue " << a->mM << " is bonded to itself" << std::endl;
			continue;
		}

		if (a->mM.asymID() == b->mM.asymID() and NoChainBreak(a, b))
			++mStats.nrOfIntraChainSSBridges;

		a->mSSBridgeNr = b->mSSBridgeNr = ++ssBondNr;
	}

	mStats.nrOfHBonds = 0;
	for (auto& r: mResidues)
	{
		auto donor = r.mHBondDonor;

		for (int i = 0; i < 2; ++i)
		{
			if (donor[i].residue != nullptr and donor[i].energy < kMaxHBondEnergy)
			{
				++mStats.nrOfHBonds;
				auto k = donor[i].residue->mNumber - r.mNumber;
				if (k >= -5 and k <= 5)
					mStats.nrOfHBondsPerDistance[k + 5] += 1;
			}
		}
	}
}

void DSSPImpl::calculateSurface()
{
	CalculateAccessibilities(mResidues, mStats);
}

// --------------------------------------------------------------------

const Monomer& DSSP::ResidueInfo::residue() const
{
	return mImpl->mM;
}

std::string DSSP::ResidueInfo::alt_id() const
{
	return mImpl->mAltID;
}

ChainBreak DSSP::ResidueInfo::chainBreak() const
{
	return mImpl->mChainBreak;
}

int DSSP::ResidueInfo::nr() const
{
	return mImpl->mNumber;
}

SecondaryStructureType DSSP::ResidueInfo::ss() const
{
	return mImpl->mSecondaryStructure;
}

int DSSP::ResidueInfo::ssBridgeNr() const
{
	return mImpl->mSSBridgeNr;
}

Helix DSSP::ResidueInfo::helix(HelixType helixType) const
{
	return mImpl->GetHelixFlag(helixType);
}

bool DSSP::ResidueInfo::bend() const
{
	return mImpl->IsBend();
}

double DSSP::ResidueInfo::accessibility() const
{
	return mImpl->mAccessibility;
}

std::tuple<DSSP::ResidueInfo,int,bool> DSSP::ResidueInfo::bridgePartner(int i) const
{
	auto bp = mImpl->GetBetaPartner(i);

	ResidueInfo ri(bp.residue);

	return std::make_tuple(std::move(ri), bp.ladder, bp.parallel);
}

int DSSP::ResidueInfo::sheet() const
{
	return mImpl->GetSheet();
}

std::tuple<DSSP::ResidueInfo,double> DSSP::ResidueInfo::acceptor(int i) const
{
	auto& a = mImpl->mHBondAcceptor[i];
	return { ResidueInfo(a.residue), a.energy };
}

std::tuple<DSSP::ResidueInfo,double> DSSP::ResidueInfo::donor(int i) const
{
	auto& d = mImpl->mHBondDonor[i];
	return { ResidueInfo(d.residue), d.energy };
}

// --------------------------------------------------------------------

DSSP::iterator::iterator(Res* res)
	: mCurrent(res)
{
}

DSSP::iterator::iterator(const iterator& i)
	: mCurrent(i.mCurrent)
{
}

DSSP::iterator& DSSP::iterator::operator=(const iterator& i)
{
	mCurrent = i.mCurrent;
	return *this;
}

DSSP::iterator& DSSP::iterator::operator++()
{
	++mCurrent.mImpl;
	return *this;
}

// --------------------------------------------------------------------

DSSP::DSSP(const Structure& s, int min_poly_proline_stretch, bool calculateSurfaceAccessibility)
	: mImpl(new DSSPImpl(s, min_poly_proline_stretch))
{
	if (calculateSurfaceAccessibility)
	{
		std::thread t(std::bind(&DSSPImpl::calculateSurface, mImpl));
		mImpl->calculateSecondaryStructure();
		t.join();
	}
	else
		mImpl->calculateSecondaryStructure();
}

DSSP::~DSSP()
{
	delete mImpl;
}

DSSP::iterator DSSP::begin() const
{
	return iterator(mImpl->mResidues.empty() ? nullptr : mImpl->mResidues.data());
}

DSSP::iterator DSSP::end() const
{
	// careful now, MSVC is picky when it comes to dereferencing iterators that are at the end.
	Res* res = nullptr;
	if (not mImpl->mResidues.empty())
	{
		res = mImpl->mResidues.data();
		res += mImpl->mResidues.size();
	}

	return iterator(res);
}

SecondaryStructureType DSSP::operator()(const std::string& inAsymID, int inSeqID) const
{
	SecondaryStructureType result = ssLoop;
	auto i = find_if(mImpl->mResidues.begin(), mImpl->mResidues.end(),
		[&](auto& r) { return r.mM.asymID() == inAsymID and r.mM.seqID() == inSeqID; });
	if (i != mImpl->mResidues.end())
		result = i->mSecondaryStructure;
	else if (cif::VERBOSE)
		std::cerr << "Could not find secondary structure for " << inAsymID << ':' << inSeqID << std::endl;
	return result;
}

SecondaryStructureType DSSP::operator()(const Monomer& m) const
{
	return operator()(m.asymID(), m.seqID());
}

double DSSP::accessibility(const std::string& inAsymID, int inSeqID) const
{
	SecondaryStructureType result = ssLoop;
	auto i = find_if(mImpl->mResidues.begin(), mImpl->mResidues.end(),
		[&](auto& r) { return r.mM.asymID() == inAsymID and r.mM.seqID() == inSeqID; });
	if (i != mImpl->mResidues.end())
		result = i->mSecondaryStructure;
	else if (cif::VERBOSE)
		std::cerr << "Could not find secondary structure for " << inAsymID << ':' << inSeqID << std::endl;
	return result;
}

double DSSP::accessibility(const Monomer& m) const
{
	return accessibility(m.asymID(), m.seqID());
}

bool DSSP::isAlphaHelixEndBeforeStart(const Monomer& m) const
{
	return isAlphaHelixEndBeforeStart(m.asymID(), m.seqID());
}

bool DSSP::isAlphaHelixEndBeforeStart(const std::string& inAsymID, int inSeqID) const
{
	auto i = find_if(mImpl->mResidues.begin(), mImpl->mResidues.end(),
		[&](auto& r) { return r.mM.asymID() == inAsymID and r.mM.seqID() == inSeqID; });

	bool result = false;

	if (i != mImpl->mResidues.end() and i + 1 != mImpl->mResidues.end())
		result = i->GetHelixFlag(HelixType::rh_alpha) == Helix::End and (i + 1)->GetHelixFlag(HelixType::rh_alpha) == Helix::Start;
	else if (cif::VERBOSE)
		std::cerr << "Could not find secondary structure for " << inAsymID << ':' << inSeqID << std::endl;

	return result;
}

DSSP_Statistics DSSP::GetStatistics() const
{
	return mImpl->mStats;
}

}