/**
 * @brief Functions and classes for manipulating the primitive cell
 *
 * @author Junyi Wang (jywang)
 *
 * @copyright Copyright (c) 2024, TBPLaS develop team.
 */

#ifndef TBPLAS_BUILDER_PRIMITIVE_H
#define TBPLAS_BUILDER_PRIMITIVE_H

#include <cassert>
#include <fstream>
#include <iomanip>
#include <list>
#include <tuple>
#include <vector>

#include <eigen3/Eigen/Dense>

#include "base.h"
#include "base/consts.h"
#include "base/funcs.h"
#include "base/lattice.h"
#include "hopping.h"

namespace tbplas::builder {

/// @brief Class representing an orbital in the primitive cell.
class Orbital {
public:
    /// @brief fractional coordinate of orbital along a, b and c directions
    double fa = 0.0, fb = 0.0, fc = 0.0;

    /// @brief on-site energy of orbital in eV
    double energy = 0.0;

    /// @brief user-defined meta-info of the label, e.g., atom type, spin, etc.
    int64_t label = 0;

    Orbital() = default;

    /// @brief Default constructor of Orbital
    /// @param[in] fa_ fractional coordinate along a
    /// @param[in] fb_ fractional coordinate along b
    /// @param[in] fc_ fractional coordinate along c
    /// @param[in] energy_ on-site energy in eV
    /// @param[in] label_ meta-info of the label
    Orbital(const double& fa_, const double& fb_, const double& fc_,
        const double& energy_ = 0.0, const int64_t& label_ = 0);

    /// @brief Get fractional coordinate as a vector
    /// @return vector of fractional coordinate
    [[nodiscard]] Eigen::Vector3d get_position() const;

    /// @brief Set fractional coordinate from vector
    /// @param[in] new_pos new fractional coordinate
    void set_position(const Eigen::Vector3d& new_pos);
};

using orbital_container_t = std::vector<Orbital>;

/// @brief Class for representing the primitive cell
/// @tparam hop_t datatype of hopping terms
template <typename hop_t>
class PrimitiveCell {

private:
    /// @brief Cartesian coordinates of lattice vectors in NM
    /// @note Each ROW corresponds to one lattice vector.
    Eigen::Matrix3d lattice_ = Eigen::MatrixXd::Identity(3, 3);

    /// @brief Cartesian coordinate of lattice origin in NM
    Eigen::Vector3d origin_ = Eigen::VectorXd::Zero(3);

    /// @brief Container of orbitals
    orbital_container_t orbitals_;

    /// @brief Container of hopping terms
    /// @note  Only half of the hopping terms are stored. Conjugate terms are added
    /// automatically when constructing the Hamiltonian.
    IntraHopping<hop_t> hopping_;

    /// @brief Check the sanity of hopping term
    /// @param[in] ra cell index along a-direction
    /// @param[in] rb cell index along b-direction
    /// @param[in] rc cell index along c-direction
    /// @param[in] orb_i orbital index in cell (0, 0, 0)
    /// @param[in] orb_j orbital index in cell (ra, rb, rc)
    /// @return 0: OK, 1: orb_i out of range, 2: orb_j out of range, 3: diagonal term
    /// @note In most cases orbital indices will be positive, but they may be larger
    /// than the number of orbitals-1. So we check the upper bound first to make use of
    /// short-circuit logic.
    [[nodiscard]] int check_hop_index(const cell_index_t& ra, const cell_index_t& rb,
        const cell_index_t& rc, const orbital_index_t& orb_i,
        const orbital_index_t& orb_j) const
    {
        if ((orb_i >= orbitals_.size()) || (orb_i < 0)) {
            return -1;
        }
        if ((orb_j >= orbitals_.size()) || (orb_j < 0)) {
            return -2;
        }
        if ((ra == 0) && (rb == 0) && (rc == 0) && (orb_i == orb_j)) {
            return -3;
        }
        return 0;
    }

    /**
     * @brief Build the 'indptr' attribute of sparse Hamiltonian and hopping distances
     * in CSR format.
     *
     * @tparam csr_idx_t datatype of CSR matrix indices
     * @param[out] indptr the 'indptr' attribute
     *
     * @note The FEAST library requires the indices to be int32_t, while orbital_index_t
     *   defaults to uint64_t. So we make it a template argument.
     */
    template <typename csr_idx_t>
    void build_indptr(std::vector<csr_idx_t>& indptr) const
    {
        orbital_index_t num_orb = get_num_orbitals();
        indptr = std::vector<csr_idx_t>(num_orb + 1, 0);

        // On-site terms
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            indptr[i + 1] += 1;
        }

        // Hopping terms
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            auto iter_ij = ((*iter_rn).second).begin();
            for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                const auto& pair = (*iter_ij).first;
                orbital_index_t orb_i, orb_j;
                std::tie(orb_i, orb_j) = pair;
                indptr[orb_i + 1] += 1;
                indptr[orb_j + 1] += 1;
            }
        }

        // Sum up
        for (orbital_index_t i = 1; i <= num_orb; ++i) {
            indptr[i] += indptr[i - 1];
        }
    }

public:
    /// @brief Default constructor
    /// @param[in] lattice Cartesian coordinates of lattice vectors in unit
    /// @param[in] origin Cartesian coordinate of lattice origin in unit
    /// @param[in] unit Scaling factor from unit to NM
    /// @note Each ROW of lattice correspond to a lattice vector.
    explicit PrimitiveCell(const Eigen::Matrix3d& lattice = Eigen::MatrixXd::Identity(3, 3),
        const Eigen::Vector3d& origin = Eigen::VectorXd::Zero(3), double unit = tbplas::base::ANG2NM)
        : lattice_(lattice * unit)
        , origin_(origin * unit)
    {
        assert(tbplas::base::get_lattice_volume(lattice_) > tbplas::base::INFINITESIMAL);
    }

    /// @brief Explicit constructor without implicit datatype conversion
    /// @param[in] num_orb number of orbitals
    /// @param[in] lattice Cartesian coordinates of lattice vectors in unit
    /// @param[in] origin Cartesian coordinate of lattice origin in unit
    /// @param[in] unit Scaling factor from unit to NM
    /// @note Each ROW of lattice correspond to a lattice vector.
    explicit PrimitiveCell(orbital_index_t num_orb,
        const Eigen::Matrix3d& lattice = Eigen::MatrixXd::Identity(3, 3),
        const Eigen::Vector3d& origin = Eigen::VectorXd::Zero(3), double unit = tbplas::base::ANG2NM)
        : lattice_(lattice * unit)
        , origin_(origin * unit)
        , orbitals_(num_orb)
    {
        assert(tbplas::base::get_lattice_volume(lattice_) > tbplas::base::INFINITESIMAL);
    }

    PrimitiveCell(const PrimitiveCell& other)
        : lattice_(other.lattice_)
        , origin_(other.origin_)
        , orbitals_(other.orbitals_)
        , hopping_(other.hopping_) {};
    PrimitiveCell(PrimitiveCell&& other) noexcept
        : lattice_(std::move(other.lattice_))
        , origin_(std::move(other.origin_))
        , orbitals_(std::move(other.orbitals_))
        , hopping_(std::move(other.hopping_)) {};
    PrimitiveCell& operator=(const PrimitiveCell& other)
    {
        if (this != &other) {
            lattice_ = other.lattice_;
            origin_ = other.origin_;
            orbitals_ = other.orbitals_;
            hopping_ = other.hopping_;
        }
        return *this;
    }
    PrimitiveCell& operator=(PrimitiveCell&& other) noexcept
    {
        if (this != &other) {
            lattice_ = std::move(other.lattice_);
            origin_ = std::move(other.origin_);
            orbitals_ = std::move(other.orbitals_);
            hopping_ = std::move(other.hopping_);
        }
        return *this;
    }
    ~PrimitiveCell() = default;

    /// @brief Add a new orbital to the model
    /// @param[in] fa fractional coordinate of orbital along a-direction
    /// @param[in] fb fractional coordinate of orbital along b-direction
    /// @param[in] fc fractional coordinate of orbital along c-direction
    /// @param[in] energy on-site energy of the orbital in eV
    /// @param[in] label label of the orbital
    void add_orbital(const double& fa, const double& fb, const double& fc,
        const double& energy = 0.0, const int64_t& label = 0)
    {
        orbitals_.emplace_back(fa, fb, fc, energy, label);
    }

    /// @brief Add a new orbital to the model
    /// @param[in] orbital orbital to be added
    void add_orbital(const Orbital& orbital)
    {
        orbitals_.push_back(orbital);
    }

    /// @brief Add a list of new orbitals to the model
    /// @param[in] new_orbitals the list of new orbitals
    void add_orbitals(const std::list<Orbital>& new_orbitals)
    {
        assert(!new_orbitals.empty());
        orbitals_.insert(orbitals_.end(), new_orbitals.begin(), new_orbitals.end());
    }

    /// @brief Set the attributes of given orbital
    /// @param[in] orb_i index of the orbital to manipulate
    /// @param[in] fa new fractional coordinate of orbital along a-direction
    /// @param[in] fb new fractional coordinate of orbital along b-direction
    /// @param[in] fc new fractional coordinate of orbital along c-direction
    /// @param[in] energy new on-site energy
    /// @param[in] label new label
    /// @warning In the Python version we can set the optional arguments, typically energy and label,
    /// to None to skip these attributes. Unfortunately, we are unable to do that in C++. So, DO NOT
    /// ASSIGN ANY DEFAULT VALUES TO THE ARGUMENTS!
    void set_orbital(const orbital_index_t& orb_i, const double& fa, const double& fb, const double& fc,
        const double& energy, const int64_t& label)
    {
        assert(orbitals_.size() > orb_i);
        assert(orb_i >= 0);
        Orbital& orbital = orbitals_[orb_i];
        orbital.fa = fa;
        orbital.fb = fb;
        orbital.fc = fc;
        orbital.energy = energy;
        orbital.label = label;
    }

    /// @brief Set the position of given orbital
    /// @param[in] orb_i index of the orbital to manipulate
    /// @param[in] fa new fractional coordinate of orbital along a-direction
    /// @param[in] fb new fractional coordinate of orbital along b-direction
    /// @param[in] fc new fractional coordinate of orbital along c-direction
    void set_orbital_position(const orbital_index_t& orb_i, const double& fa, const double& fb, const double& fc)
    {
        assert(orbitals_.size() > orb_i);
        assert(orb_i >= 0);
        Orbital& orbital = orbitals_[orb_i];
        orbital.fa = fa;
        orbital.fb = fb;
        orbital.fc = fc;
    }

    /// @brief Set the energy of given orbital
    /// @param[in] orb_i index of the orbital to manipulate
    /// @param[in] energy new on-site energy in eV
    void set_orbital_energy(const orbital_index_t& orb_i, const double& energy)
    {
        assert(orbitals_.size() > orb_i);
        assert(orb_i >= 0);
        Orbital& orbital = orbitals_[orb_i];
        orbital.energy = energy;
    }

    /// @brief Set the label of given orbital
    /// @param[in] orb_i index of the orbital to manipulate
    /// @param[in] label new label
    void set_orbital_label(const orbital_index_t& orb_i, const int64_t& label)
    {
        assert(orbitals_.size() > orb_i);
        assert(orb_i >= 0);
        Orbital& orbital = orbitals_[orb_i];
        orbital.label = label;
    }

    /// @brief Set the attributes of given orbital
    /// @param[in] orb_i index of the orbital to manipulate
    /// @param[in] orbital new orbital from which attributes will be copied
    void set_orbital(const orbital_index_t& orb_i, const Orbital& orbital)
    {
        assert(orbitals_.size() > orb_i);
        assert(orb_i >= 0);
        orbitals_[orb_i] = orbital;
    }

    /// @brief Reset the vector of orbitals
    /// @param[in] new_orbitals new orbitals to reset the existing orbital vector
    void set_all_orbitals(const std::list<Orbital>& new_orbitals)
    {
        assert(!new_orbitals.empty());
        orbitals_ = orbital_container_t(new_orbitals.size());
        orbitals_.assign(new_orbitals.begin(), new_orbitals.end());
    }

    /// @brief Get the given orbital
    /// @param[in] orb_i index of the orbital to fetch
    /// @return orbital specified by the orbital index
    [[nodiscard]] const Orbital& get_orbital(const orbital_index_t& orb_i) const
    {
        assert(orbitals_.size() > orb_i);
        assert(orb_i >= 0);
        return orbitals_[orb_i];
    }

    /// @brief Get reference to all orbitals
    /// @return reference to this->orbital_
    [[nodiscard]] const orbital_container_t& get_all_orbitals() const
    {
        return orbitals_;
    }

    /// @brief Remove given set of orbitals according to their indices
    /// @param[in] orbital_indices indices of orbitals to remove
    /// @note Even removing a single orbital will trigger the updating of hopping terms, which is
    /// expensive. So we prefer to remove multiple orbitals at once.
    void remove_orbitals(const std::set<orbital_index_t>& orbital_indices)
    {
        assert(!orbital_indices.empty());
        orbital_index_t num_orb = orbitals_.size();
        for (const auto& orb_i : orbital_indices) {
            assert(orb_i < num_orb);
            assert(0 <= orb_i);
        }
        std::vector<Orbital> new_orbitals(num_orb - orbital_indices.size());
        orbital_index_t counter = 0;
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            if (orbital_indices.count(i) == 0) {
                new_orbitals[counter] = orbitals_[i];
                ++counter;
            }
        }
        orbitals_ = std::move(new_orbitals);
        hopping_.remove_orbitals(orbital_indices);
    }

    /// @brief Add a new hopping term or update existing term
    /// @param[in] ra cell index along a-direction
    /// @param[in] rb cell index along b-direction
    /// @param[in] rc cell index along c-direction
    /// @param[in] orb_i orbital index in cell (0, 0, 0)
    /// @param[in] orb_j orbital index in cell (ra, rb, rc)
    /// @param[in] energy hopping energy in eV
    void add_hopping(const cell_index_t& ra, const cell_index_t& rb,
        const cell_index_t& rc, const orbital_index_t& orb_i,
        const orbital_index_t& orb_j, const hop_t& energy)
    {
        assert(check_hop_index(ra, rb, rc, orb_i, orb_j) == 0);
        hopping_.add_hopping(ra, rb, rc, orb_i, orb_j, energy);
    }

    /// @brief Get an existing hopping term
    /// @param[in] ra cell index along a-direction
    /// @param[in] rb cell index along b-direction
    /// @param[in] rc cell index along c-direction
    /// @param[in] orb_i orbital index in cell (0, 0, 0)
    /// @param[in] orb_j orbital index in cell (ra, rb, rc)
    /// @param[out] energy hopping energy in eV
    /// @return the flag of whether specified hopping item is found. The result is stored in energy.
    [[nodiscard]] bool get_hopping(const cell_index_t& ra, const cell_index_t& rb,
        const cell_index_t& rc,
        const orbital_index_t& orb_i,
        const orbital_index_t& orb_j,
        hop_t& energy) const
    {
        assert(check_hop_index(ra, rb, rc, orb_i, orb_j) == 0);
        bool status = hopping_.get_hopping(ra, rb, rc, orb_i, orb_j, energy);
        return status;
    }

    /// @brief Get reference to all hopping terms
    [[nodiscard]] const IntraHopping<hop_t>& get_all_hopping() const
    {
        return hopping_;
    }

    /// @brief Remove an existing hopping term
    /// @param[in] ra cell index along a-direction
    /// @param[in] rb cell index along b-direction
    /// @param[in] rc cell index along c-direction
    /// @param[in] orb_i orbital index in cell (0, 0, 0)
    /// @param[in] orb_j orbital index in cell (ra, rb, rc)
    void remove_hopping(const cell_index_t& ra,
        const cell_index_t& rb,
        const cell_index_t& rc,
        const orbital_index_t& orb_i,
        const orbital_index_t& orb_j)
    {
        assert(check_hop_index(ra, rb, rc, orb_i, orb_j) == 0);
        bool status = hopping_.remove_hopping(ra, rb, rc, orb_i, orb_j);
        assert(status);
    }

    /// @brief Get the number of orbitals
    /// @return number of orbitals
    [[nodiscard]] orbital_index_t get_num_orbitals() const
    {
        return orbitals_.size();
    }

    /// @brief Get the number of hopping terms
    /// @return number of hopping terms
    [[nodiscard]] orbital_index_t get_num_hopping() const
    {
        return hopping_.get_num_hopping();
    }

    /// @brief Reset lattice vectors and origin
    /// @param[in] lattice Cartesian coordinates of new lattice vectors in unit
    /// @param[in] origin Cartesian coordinate of new origin in unit
    /// @param[in] unit conversion coefficient from unit to NM
    /// @param[in] fix_orb whether to keep Cartesian coordinates of orbitals
    /// unchanged after resetting lattice
    /// @note Each ROW of lattice is a lattice vector.
    void reset_lattice(const Eigen::Matrix3d& lattice, const Eigen::Vector3d& origin,
        double unit = tbplas::base::ANG2NM, bool fix_orb = true)
    {
        Eigen::Matrix3d new_lattice = lattice * unit;
        Eigen::Vector3d new_origin = origin * unit;

        // Update orbital positions if needed
        orbital_index_t num_orb = orbitals_.size();
        if (fix_orb && num_orb > 0) {
            Eigen::Matrix3Xd cart_coord = get_orbital_positions_nm();
            Eigen::Matrix3Xd frac_coord = tbplas::base::cart2frac(new_lattice, cart_coord, new_origin);
            for (orbital_index_t i = 0; i < num_orb; ++i) {
                orbitals_[i].set_position(frac_coord.col(i));
            }
        }

        // Finally update lattice vectors and origin
        lattice_ = new_lattice;
        origin_ = new_origin;
    }

    /// @brief Get lattice vectors
    /// @return Cartesian coordinates of lattice vectors in NM
    /// @note Each ROW of lattice is a lattice vector.
    [[nodiscard]] const Eigen::Matrix3d& get_lattice() const
    {
        return lattice_;
    }

    /// @brief Get lattice origin
    /// @return Cartesian coordinate of origin in NM
    [[nodiscard]] const Eigen::Vector3d& get_origin() const
    {
        return origin_;
    }

    /// @brief Get the fractional coordinates of orbitals as a matrix
    /// @return fractional coordinates of orbitals with each COLUMN begin a coordinate
    [[nodiscard]] Eigen::Matrix3Xd get_orbital_positions() const
    {
        orbital_index_t num_orb = orbitals_.size();
        assert(num_orb > 0);
        Eigen::Matrix3Xd orb_pos(3, num_orb);
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            orb_pos.col(i) = orbitals_[i].get_position();
        }
        return orb_pos;
    }

    /// @brief Get the Cartesian coordinates of orbitals in NM as a matrix
    /// @return Cartesian coordinates of orbitals in NM with each COLUMN begin a coordinate
    [[nodiscard]] Eigen::Matrix3Xd get_orbital_positions_nm() const
    {
        Eigen::Matrix3Xd frac_coord = get_orbital_positions();
        Eigen::Matrix3Xd cart_coord = tbplas::base::frac2cart(lattice_, frac_coord, origin_);
        return cart_coord;
    }

    /// @brief Get lattice area in nm^2
    /// @param[in] direction direction along which the area is evaluated, should be in ("a", "b", "c")
    [[nodiscard]] double get_lattice_area(const std::string& direction = "c") const
    {
        return tbplas::base::get_lattice_area(lattice_, direction);
    }

    /// @brief Get lattice volume in nm^3
    /// @return lattice volume in nm^3
    [[nodiscard]] double get_lattice_volume() const
    {
        return tbplas::base::get_lattice_volume(lattice_);
    }

    /// @brief Get reciprocal lattice vectors
    /// @return Cartesian coordinates of reciprocal lattice vectors in 1/NM.
    [[nodiscard]] Eigen::Matrix3d get_reciprocal_lattice() const
    {
        return tbplas::base::gen_reciprocal_vectors(lattice_);
    }

    /// @brief Apply periodic boundary conditions by removing hopping terms between
    /// cells along non-periodic direction.
    /// @param[in] pbc whether pbc is enabled along 3 directions
    void apply_pbc(const std::tuple<bool, bool, bool>& pbc = { true, true, true })
    {
        const auto& hop_data = hopping_.get_data();
        std::list<rn_t> rn_remove;
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            bool to_keep = true;
            if ((!std::get<0>(pbc)) && (std::get<0>(rn) != 0)) {
                to_keep = false;
            }
            if ((!std::get<1>(pbc)) && (std::get<1>(rn) != 0)) {
                to_keep = false;
            }
            if ((!std::get<2>(pbc)) && (std::get<2>(rn) != 0)) {
                to_keep = false;
            }
            if (!to_keep) {
                rn_remove.push_back(rn);
            }
        }
        for (const auto& rn : rn_remove) {
            bool status = hopping_.remove_rn(std::get<0>(rn), std::get<1>(rn), std::get<2>(rn));
            assert(status);
        }
    }

    /**
     * @brief Rotate and shift primitive cell with respect to z-axis.
     *
     * @param[in] angle twisting angle in RADIANs, NOT degrees
     * @param[in] center Cartesian coordinates of the rotation center in NANOMETER
     * @param[in] shift  distance of shift in NANOMETER
     */
    void rotate(const double& angle = 0.0,
        const Eigen::Vector3d& center = Eigen::Vector3d::Zero(),
        const double& shift = 0.0)
    {
        // Get rotated lattice vectors and origin
        Eigen::MatrixX3d end_points = Eigen::MatrixX3d::Zero(4, 3);
        end_points.block(1, 0, 3, 3) = lattice_;
        end_points = end_points.rowwise() + origin_.transpose();
        end_points = tbplas::base::rotate_coord(end_points.transpose(), angle, "z", center).transpose();
        Eigen::Matrix3d new_lattice = end_points.block(1, 0, 3, 3).rowwise() - end_points.row(0);
        Eigen::Vector3d new_origin = end_points.row(0);

        // Reset lattice vectors and origin without fixing the orbitals
        reset_lattice(new_lattice, new_origin, tbplas::base::NM, false);

        // Shift orbitals along z-axis
        orbital_index_t num_orb = orbitals_.size();
        if (num_orb > 0) {
            Eigen::Matrix3Xd orb_pos = get_orbital_positions_nm();
            Eigen::Vector3d shift_vec(0.0, 0.0, shift);
            orb_pos = orb_pos.colwise() + shift_vec;
            orb_pos = tbplas::base::cart2frac(lattice_, orb_pos, origin_);
            for (orbital_index_t i = 0; i < num_orb; ++i) {
                orbitals_[i].set_position(orb_pos.col(i));
            }
        }
    }

    /// @brief Trim dangling orbitals and associated hopping terms.
    void trim()
    {
        // Count the number of hopping terms of each orbital
        std::vector<orbital_index_t> hop_count(orbitals_.size());
        for (auto& d : hop_count) {
            d = 0;
        }
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            auto iter_ij = ((*iter_rn).second).begin();
            for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                const auto& pair = (*iter_ij).first;
                hop_count[std::get<0>(pair)] += 1;
                hop_count[std::get<1>(pair)] += 1;
            }
        }

        // Get indices of orbitals to remove
        std::set<orbital_index_t> orb_id_trim;
        for (orbital_index_t i = 0; i < hop_count.size(); ++i) {
            if (hop_count[i] <= 1) {
                orb_id_trim.insert(i);
            }
        }

        // Remove orbitals and hopping terms
        if (!orb_id_trim.empty()) {
            remove_orbitals(orb_id_trim);
        }
    }

    /**
     * @brief Apply magnetic field perpendicular to xOy-plane to -z direction via Peierls substitution.
     *
     * @param[in] intensity magnetic B field in Tesla
     * @param[in] gauge gauge of vector potential of the magnetic field to -z\n
     * 0 for (By, 0, 0), 1 for (0, -Bx, 0), 2 for (0.5By, -0.5Bx, 0)
     */
    void apply_magnetic_field(double intensity, int gauge = 0)
    {
        assert((gauge == 0) || (gauge == 1) || (gauge == 2));
        Eigen::Matrix3Xd orb_pos_nm = get_orbital_positions_nm();
        double factor = tbplas::base::PEIERLS_FACTOR;
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            cell_index_t ra, rb, rc;
            std::tie(ra, rb, rc) = rn;
            Eigen::Vector3d rn_cart = (rn2vec(rn).transpose() * lattice_).transpose();
            auto iter_ij = ((*iter_rn).second).begin();
            for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                const auto& pair = (*iter_ij).first;
                const auto& t = (*iter_ij).second;
                orbital_index_t orb_i, orb_j;
                std::tie(orb_i, orb_j) = pair;
                Eigen::Vector3d dr_cart = rn_cart + orb_pos_nm.col(orb_j) - orb_pos_nm.col(orb_i);
                double dx = dr_cart(0);
                double dy = dr_cart(1);
                double sx = orb_pos_nm(0, orb_j) + orb_pos_nm(0, orb_i);
                double sy = orb_pos_nm(1, orb_j) + orb_pos_nm(1, orb_i);
                double phase;
                if (gauge == 0) {
                    phase = factor * intensity * dx * sy;
                } else if (gauge == 1) {
                    phase = -factor * intensity * dy * sx;
                } else {
                    phase = factor * 0.5 * intensity * (dx * sx - dy * sy);
                }
                hopping_.add_hopping(ra, rb, rc, orb_i, orb_j, t * base::exp_it(phase));
            }
        }
    }

    /**
     * @brief Checks if a 1x1x1 supercell can be built from this primitive cell by
     * searching for duplicate hopping terms.
     *
     * @note We require that the hopping terms should not contain diagonal terms (i==j)
     * since they are redundant with the on-site terms in the sparse Hamiltonian and hopping
     * distances. Yet, the off-diagonal terms should appear only once. Although these
     * requirements are unphysical, they SIGNIFICANTLY simplify the functions for constructing
     * the sparse matrices. For large models, these requirements are typically satisfied.
     */
    void check_dim() const
    {
        auto pair_count = hopping_.count_pairs();
        // Add contributions from on-site terms
        orbital_index_t num_orb = get_num_orbitals();
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            pair_count[std::make_tuple(i, i)] += 1;
        }
        auto iter_pair = pair_count.begin();
        for (; iter_pair != pair_count.end(); iter_pair++) {
            assert((*iter_pair).second <= 1);
        }
    }

    /**
     * @brief Extract hopping data in COO format for Lindhard AC conductivity calculation
     *
     * @param[out] hop_i index i in orbital pairs, i.e, orbital in r0
     * @param[out] hop_j index j in orbital pairs, i.e, orbital in rn
     * @param[out] hop_v hopping energy in eV
     * @param[out] dr_nm hopping distances in nm in COLUMN-WISE order
     *
     * @note Conjugate terms are not included. The caller is responsible for restoring
     * them.
     */
    void build_ham_dr_coo(
        std::vector<orbital_index_t>& hop_i,
        std::vector<orbital_index_t>& hop_j,
        std::vector<complex_t>& hop_v,
        Eigen::Matrix3Xd& dr_nm) const
    {
        // Allocate arrays
        orbital_index_t num_hop = get_num_hopping();
        hop_i = std::vector<orbital_index_t>(num_hop);
        hop_j = std::vector<orbital_index_t>(num_hop);
        hop_v = std::vector<complex_t>(num_hop);
        dr_nm = Eigen::Matrix3Xd(3, num_hop);

        // Loop over rn
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        orbital_index_t counter = 0;
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            Eigen::Vector3d rn_cart = (rn2vec(rn).transpose() * lattice_).transpose();

            // Loop over pairs
            auto iter_ij = ((*iter_rn).second).begin();
            for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                const auto& pair = (*iter_ij).first;
                const auto& t = (*iter_ij).second;
                orbital_index_t orb_i, orb_j;
                std::tie(orb_i, orb_j) = pair;

                // Evaluate dr
                Eigen::Vector3d dr_frac = orbitals_[orb_j].get_position() - orbitals_[orb_i].get_position();
                Eigen::Vector3d dr_cart = (dr_frac.transpose() * lattice_).transpose() + rn_cart;

                // Fill arrays
                hop_i[counter] = orb_i;
                hop_j[counter] = orb_j;
                hop_v[counter] = t;
                dr_nm.col(counter) = dr_cart;
                ++counter;
            }
        }
    }

    /**
     * @brief Build the attributes of sparse Hamiltonian and current operator
     * in CSR format.
     *
     * @param[out] indptr the 'indptr' attribute (outerIndex of eigen)
     * @param[out] indices the 'indices' attribute (innerIndex of eigen)
     * @param[out] ham non-zero elements of Hamiltonian in eV
     * @param[out] curr_coeff_x coefficients of current operator along along x direction
     * @param[out] curr_coeff_y coefficients of current operator along along x direction
     *
     * @note See eqn. 54 of CPC paper for the formula of current operator.
     */
    void build_ham_curr_csr(std::vector<orbital_index_t>& indptr,
        std::vector<orbital_index_t>& indices,
        Eigen::VectorXcd& ham,
        Eigen::VectorXcd& curr_coeff_x,
        Eigen::VectorXcd& curr_coeff_y) const
    {
        // Allocate indices and data
        build_indptr(indptr);
        orbital_index_t num_orb = indptr.size() - 1;
        orbital_index_t num_hop = indptr.back();
        indices = std::vector<orbital_index_t>(num_hop);
        ham = Eigen::VectorXcd(num_hop);
        curr_coeff_x = Eigen::VectorXcd(num_hop);
        curr_coeff_y = Eigen::VectorXcd(num_hop);
        std::vector<orbital_index_t> count(num_orb, 0);

        // Add on-site terms
        // WARNING:
        // 1. IT MUST BE count[i]++, not ++count[i]!
        // 2. DO NOT forget to set curr_coeff_x and curr_coeff_y, otherwise the diagonal terms
        //    will be undefined. This bug has caused us hours to debug.
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            orbital_index_t ptr = indptr[i] + count[i]++;
            indices[ptr] = i;
            ham[ptr] = orbitals_[i].energy;
            curr_coeff_x[ptr] = 0.0;
            curr_coeff_y[ptr] = 0.0;
        }

        // Add hopping terms
        // In TBPM calculations we have limited memory, so we have to calculate hopping distances
        // on-the-fly rather than calling 'get_orbital_positions_nm' directly.
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            Eigen::Vector3d rn_cart = (rn2vec(rn).transpose() * lattice_).transpose();
            auto iter_ij = ((*iter_rn).second).begin();
            for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                const auto& pair = (*iter_ij).first;
                const auto& hij = (*iter_ij).second;

                // Evaluate hopping distance
                orbital_index_t idx_i, idx_j;
                std::tie(idx_i, idx_j) = pair;
                Eigen::Vector3d dr = ((orbitals_[idx_j].get_position() - orbitals_[idx_i].get_position()).transpose() * lattice_).transpose() + rn_cart;

                // Notes on the current operator:
                // The current operator takes the form of j_ab = -i*e/hbar * t_ab * dr_ab.
                // So we have
                //   j_ba = -i*e/hbar * t_ba * dr_ba
                //        = -i*e/hbar * conj(t_ab) * -dr_ab
                //        = i*e/hbar * conj(t_ab) * dr_ab
                // This can also be explained with conjugate relation:
                //   j_ba = conj(j_ab)
                //        = conj(-i*e/hbar) * conj(t_ab) * conj(dr_ab)
                //        = i*e/hbar * conj(t_ab) * dr_ab
                // since dr_ab is real.

                // Fill vectors
                // Original part
                orbital_index_t ptr_i = indptr[idx_i] + count[idx_i]++;
                orbital_index_t ptr_j = indptr[idx_j] + count[idx_j]++;
                indices[ptr_i] = idx_j;
                ham[ptr_i] = hij;
                complex_t factor = -base::UIMAG * hij;
                curr_coeff_x[ptr_i] = factor * dr[0];
                curr_coeff_y[ptr_i] = factor * dr[1];

                // Conjugate part
                indices[ptr_j] = idx_i;
                ham[ptr_j] = base::conj_t(hij);
                factor = base::UIMAG * base::conj_t(hij);
                curr_coeff_x[ptr_j] = factor * dr[0];
                curr_coeff_y[ptr_j] = factor * dr[1];
            }
        }
    }

    /**
     * @brief Build the attributes of sparse Hamiltonian for given k-point
     * in CSR format.
     *
     * @tparam csr_idx_t datatype of CSR matrix indices
     * @param[out] indptr the 'indptr' attribute (outerIndex of eigen)
     * @param[out] indices the 'indices' attribute (innerIndex of eigen)
     * @param[out] ham non-zero elements of Hamiltonian in eV
     * @param[in] k_point fractional coordinate of k-point
     * @param[in] convention convention of Hamiltonian
     *
     * @note
     * 1. In most cases we need only the Hamiltonian at Gamma point. So we make
     *    it that if convention != 1 or 2, k_point will be discarded and Gamma point
     *    is assumed.
     * 2. The FEAST library requires the indices to be int32_t, while orbital_index_t
     *    defaults to uint64_t. So we make it a template argument.
     */
    template <typename csr_idx_t>
    void build_ham_csr(
        std::vector<csr_idx_t>& indptr,
        std::vector<csr_idx_t>& indices,
        Eigen::VectorXcd& ham,
        const Eigen::Vector3d& k_point = Eigen::Vector3d::Zero(3),
        int convention = -1) const
    {
        // Allocate indices and data
        build_indptr(indptr);
        orbital_index_t num_orb = indptr.size() - 1;
        orbital_index_t num_hop = indptr.back();
        indices = std::vector<csr_idx_t>(num_hop);
        ham = Eigen::VectorXcd(num_hop);
        std::vector<orbital_index_t> count(num_orb, 0);

        // Add on-site terms
        // WARNING: IT MUST BE count[i]++, not ++count[i]!
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            orbital_index_t ptr = indptr[i] + count[i]++;
            indices[ptr] = i;
            ham[ptr] = orbitals_[i].energy;
        }

        // Add hopping terms
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            auto iter_ij = ((*iter_rn).second).begin();

            // Evaluate phase factor from rn for convention 1 and 2
            complex_t factor_rn = 1.0;
            if ((convention == 1) || (convention == 2)) {
                double k_dot_rn = k_point.dot(rn2vec(rn));
                double phase_rn = tbplas::base::TWOPI * k_dot_rn;
                factor_rn = base::exp_it(phase_rn);
            }

            for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                const auto& pair = (*iter_ij).first;
                const auto& t = (*iter_ij).second;
                orbital_index_t idx_i, idx_j;
                std::tie(idx_i, idx_j) = pair;
                complex_t hij = t * factor_rn;

                // Evaluate phase factor from \tau_j - \tau_i for convention 1
                if (convention == 1) {
                    Eigen::Vector3d tau = orbitals_[idx_j].get_position() - orbitals_[idx_i].get_position();
                    double k_dot_tau = k_point.dot(tau);
                    double phase_tau = tbplas::base::TWOPI * k_dot_tau;
                    hij *= base::exp_it(phase_tau);
                }

                // Fill vectors
                // Original part
                orbital_index_t ptr_i = indptr[idx_i] + count[idx_i]++;
                orbital_index_t ptr_j = indptr[idx_j] + count[idx_j]++;
                indices[ptr_i] = idx_j;
                ham[ptr_i] = hij;

                // Conjugate part
                indices[ptr_j] = idx_i;
                ham[ptr_j] = base::conj_t(hij);
            }
        }
    }

    /// @brief Set up dense Hamiltonian matrix for given k-point.
    /// @param[in] k_point fractional coordinate of k-point
    /// @param[out] ham incoming Hamiltonian matrix
    /// @param[in] convention convention of Hamiltonian
    void build_ham_dense(
        const Eigen::Vector3d& k_point,
        Eigen::MatrixXcd& ham,
        int convention = 1) const
    {
        assert((convention == 1) || (convention == 2));
        orbital_index_t num_orb = get_num_orbitals();
        assert((ham.rows() == num_orb) && (ham.cols() == num_orb));
        ham.setZero();

        // On-site energy
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            ham(i, i) = orbitals_[i].energy;
        }

        // Hopping terms
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            auto iter_ij = ((*iter_rn).second).begin();
            // Evaluate phase factor from rn
            double k_dot_rn = k_point.dot(rn2vec(rn));
            double phase_rn = tbplas::base::TWOPI * k_dot_rn;
            complex_t factor_rn = base::exp_it(phase_rn);

            // Evaluate phase factor from \tau_j - \tau_i if convention == 1
            if (convention == 1) {
                for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                    const auto& pair = (*iter_ij).first;
                    const auto& t = (*iter_ij).second;
                    orbital_index_t i, j;
                    std::tie(i, j) = pair;
                    const Orbital& orb_i = orbitals_[i];
                    const Orbital& orb_j = orbitals_[j];
                    Eigen::Vector3d tau = orb_j.get_position() - orb_i.get_position();
                    double k_dot_tau = k_point.dot(tau);
                    double phase_tau = tbplas::base::TWOPI * k_dot_tau;
                    complex_t factor_tau = base::exp_it(phase_tau);
                    complex_t hij = t * factor_rn * factor_tau;
                    ham(i, j) += hij;
                    ham(j, i) += std::conj(hij);
                }
            } else {
                for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                    const auto& pair = (*iter_ij).first;
                    const auto& t = (*iter_ij).second;
                    orbital_index_t i, j;
                    std::tie(i, j) = pair;
                    complex_t hij = t * factor_rn;
                    ham(i, j) += hij;
                    ham(j, i) += std::conj(hij);
                }
            }
        }
    }

    /**
     * @brief Set up derivatives of dense Hamiltonian matrix for given k-point.
     * @param[in] k_point fractional coordinate of k-point
     * @param[out] dh_dka dH/dka, not dH/dkx!
     * @param[out] dh_dkb dH/dkb, not dH/dky!
     * @param[out] dh_dkc dH/dkc, not dH/dkz!
     * @param[in] convention convention of Hamiltonian
     */
    void build_ham_der_dense(
        const Eigen::Vector3d& k_point,
        Eigen::MatrixXcd& dh_dka,
        Eigen::MatrixXcd& dh_dkb,
        Eigen::MatrixXcd& dh_dkc,
        int convention = 1) const
    {
        assert((convention == 1) || (convention == 2));
        orbital_index_t num_orb = get_num_orbitals();
        assert((dh_dka.rows() == num_orb) && (dh_dka.cols() == num_orb));
        assert((dh_dkb.rows() == num_orb) && (dh_dkb.cols() == num_orb));
        assert((dh_dkc.rows() == num_orb) && (dh_dkc.cols() == num_orb));
        dh_dka.setZero();
        dh_dkb.setZero();
        dh_dkc.setZero();
        complex_t i2pi = tbplas::base::UIMAG * tbplas::base::TWOPI;

        // No need for on-site terms since the hopping distance is zero.

        // Hopping terms
        const auto& hop_data = hopping_.get_data();
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            auto iter_ij = ((*iter_rn).second).begin();
            // Evaluate phase factor from rn
            double k_dot_rn = k_point.dot(rn2vec(rn));
            double phase_rn = tbplas::base::TWOPI * k_dot_rn;
            complex_t factor_rn = base::exp_it(phase_rn);

            // Evaluate phase factor from \tau_j - \tau_i if convention == 1
            if (convention == 1) {
                for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                    const auto& pair = (*iter_ij).first;
                    const auto& t = (*iter_ij).second;
                    orbital_index_t i, j;
                    std::tie(i, j) = pair;
                    const Orbital& orb_i = orbitals_[i];
                    const Orbital& orb_j = orbitals_[j];
                    Eigen::Vector3d tau = orb_j.get_position() - orb_i.get_position();
                    double k_dot_tau = k_point.dot(tau);
                    double phase_tau = tbplas::base::TWOPI * k_dot_tau;
                    complex_t factor_tau = base::exp_it(phase_tau);
                    complex_t hij = t * factor_rn * factor_tau;

                    // dH/dk is also Hermitian due to the I2PI factor
                    Eigen::Vector3cd i2pi_dr = i2pi * (rn2vec(rn) + tau);
                    dh_dka(i, j) += i2pi_dr[0] * hij;
                    dh_dkb(i, j) += i2pi_dr[1] * hij;
                    dh_dkc(i, j) += i2pi_dr[2] * hij;
                    dh_dka(j, i) -= i2pi_dr[0] * std::conj(hij);
                    dh_dkb(j, i) -= i2pi_dr[1] * std::conj(hij);
                    dh_dkc(j, i) -= i2pi_dr[2] * std::conj(hij);
                }
            } else {
                for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                    const auto& pair = (*iter_ij).first;
                    const auto& t = (*iter_ij).second;
                    orbital_index_t i, j;
                    std::tie(i, j) = pair;
                    complex_t hij = t * factor_rn;

                    // dH/dk is also Hermitian due to the I2PI factor
                    Eigen::Vector3cd i2pi_dr = i2pi * rn2vec(rn);
                    dh_dka(i, j) += i2pi_dr[0] * hij;
                    dh_dkb(i, j) += i2pi_dr[1] * hij;
                    dh_dkc(i, j) += i2pi_dr[2] * hij;
                    dh_dka(j, i) -= i2pi_dr[0] * std::conj(hij);
                    dh_dkb(j, i) -= i2pi_dr[1] * std::conj(hij);
                    dh_dkc(j, i) -= i2pi_dr[2] * std::conj(hij);
                }
            }
        }
    }

    /**
     * @brief Build density operator coefficients for given q-point
     *
     * @param[in] q_point FRACTIONAL coordinate of q-point
     * @param[out] coeff_q coefficients for q point
     * @param[out] coeff_mq coefficients for -q point
     *
     * @note The formula is eqn. 78 of CPC paper.
     */
    void build_density_coeff(
        const Eigen::Vector3d& q_point,
        Eigen::VectorXcd& coeff_q,
        Eigen::VectorXcd& coeff_mq) const
    {
        Eigen::Vector3d origin_frac = origin_.transpose() * lattice_.inverse();
        orbital_index_t num_orb = get_num_orbitals();
        coeff_q = Eigen::VectorXcd(num_orb);
        coeff_mq = Eigen::VectorXcd(num_orb);
        complex_t phase = 0.0;
#pragma omp parallel for private(phase)
        for (orbital_index_t i = 0; i < num_orb; ++i) {
            phase = base::exp_it(base::TWOPI * q_point.dot(orbitals_[i].get_position() + origin_frac));
            coeff_q[i] = phase;
            coeff_mq[i] = base::conj_t(phase);
        }
    }

    /// @brief Print attributes for debugging
    void print() const
    {
        std::cout << "Lattice vectors:\n";
        std::cout << lattice_;
        std::cout << "\n\n";
        std::cout << "Lattice origin:\n";
        std::cout << origin_.transpose();
        std::cout << "\n\n";
        std::cout << "Orbitals:\n";
        for (const auto& orb : orbitals_) {
            std::cout << orb.fa << " " << orb.fb << " " << orb.fc << " " << orb.energy << " " << orb.label << "\n";
        }
        std::cout << "\n";
        std::cout << "Hopping terms:\n";
        hopping_.print();
        std::cout << "\n";
    }

    /// @brief Save model to file
    void save(const std::string& file_name = "model") const
    {
        constexpr int prec = 16;
        constexpr int width = 22;
        constexpr int width_int = 6;
        std::ofstream outfile;
        outfile.open(file_name, std::ios::out);
        outfile.setf(std::ios::fixed);
        outfile.precision(prec);

        // lattice vectors
        outfile << "begin_lattice\n";
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                outfile << std::setw(width) << lattice_(i, j);
            }
            outfile << "\n";
        }
        outfile << "end_lattice\n";

        // lattice origin
        outfile << "begin_origin\n";
        for (int i = 0; i < 3; ++i) {
            outfile << std::setw(width) << origin_(i);
        }
        outfile << "\nend_origin\n";

        // orbitals
        outfile << "begin_orbitals\n";
        for (const auto& orb : orbitals_) {
            outfile << std::setw(width) << orb.fa
                    << std::setw(width) << orb.fb
                    << std::setw(width) << orb.fc
                    << std::setw(width) << orb.energy
                    << std::setw(width_int) << orb.label << "\n";
        }
        outfile << "end_orbitals\n";

        // hopping terms
        const auto& hop_data = hopping_.get_data();
        outfile << "begin_hopping\n";
        auto iter_rn = hop_data.begin();
        for (; iter_rn != hop_data.end(); iter_rn++) {
            const auto& rn = (*iter_rn).first;
            auto iter_ij = ((*iter_rn).second).begin();
            for (; iter_ij != ((*iter_rn).second).end(); iter_ij++) {
                const auto& pair = (*iter_ij).first;
                const auto& t = (*iter_ij).second;
                cell_index_t ra, rb, rc;
                orbital_index_t orb_i, orb_j;
                std::tie(ra, rb, rc) = rn;
                std::tie(orb_i, orb_j) = pair;
                outfile << std::setw(width_int) << ra
                        << std::setw(width_int) << rb
                        << std::setw(width_int) << rc
                        << std::setw(width_int) << orb_i
                        << std::setw(width_int) << orb_j
                        << std::setw(width) << tbplas::base::real_part(t)
                        << std::setw(width) << tbplas::base::imag_part(t) << "\n";
            }
        }
        outfile << "end_hopping\n";
        outfile.close();
    }
};

/**
 * @brief Container class for holding hopping terms between two primitive cells
 *
 * @tparam hop_t datatype of hopping terms
 */
template <typename hop_t>
class PCInterHopping : public InterHopping<hop_t> {
private:
    /// @brief pointer to the 'bra' primitive cell from which the hopping terms exist
    const PrimitiveCell<hop_t>* pc_bra_ = nullptr;

    /// @brief pointer to the 'ket' primitive cell to which the hopping terms exist
    const PrimitiveCell<hop_t>* pc_ket_ = nullptr;

public:
    PCInterHopping() = default;
    explicit PCInterHopping(
        const PrimitiveCell<hop_t>& pc_bra,
        const PrimitiveCell<hop_t>& pc_ket)
        : InterHopping<hop_t>()
        , pc_bra_(&pc_bra)
        , pc_ket_(&pc_ket) {};
    PCInterHopping(const PCInterHopping& other)
        : InterHopping<hop_t>(other)
        , pc_bra_(other.pc_bra_)
        , pc_ket_(other.pc_ket_) {};
    PCInterHopping(PCInterHopping&& other) noexcept
        : InterHopping<hop_t>(std::move(other))
        , pc_bra_(other.pc_bra_)
        , pc_ket_(other.pc_ket_) {};
    PCInterHopping& operator=(const PCInterHopping& other)
    {
        if (this != other) {
            BaseHopping<hop_t>::data_ = other.data_;
            pc_bra_ = other.pc_bra_;
            pc_ket_ = other.pc_ket_;
        }
        return *this;
    }
    PCInterHopping& operator=(PCInterHopping&& other) noexcept
    {
        if (this != other) {
            BaseHopping<hop_t>::data_ = std::move(other.data_);
            pc_bra_ = other.pc_bra_;
            pc_ket_ = other.pc_ket_;
        }
        return *this;
    }

    /**
     * @brief Get the pc_bra attribute
     *
     * @return pointer to 'bra' primitive cell
     */
    [[nodiscard]] const PrimitiveCell<hop_t>* get_pc_bra() const
    {
        return pc_bra_;
    }

    /**
     * @brief Get the pc_ket attribute
     *
     * @return pointer to 'bra' primitive cell
     */
    [[nodiscard]] const PrimitiveCell<hop_t>* get_pc_ket() const
    {
        return pc_ket_;
    }
};
} // namespace tbplas::builder

#endif // TBPLAS_BUILDER_PRIMITIVE_H
