// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2013 Christian Seiler <christian@iwakd.de>
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

#ifndef EIGEN_CXX11_TENSORSYMMETRY_DYNAMICSYMMETRY_H
#define EIGEN_CXX11_TENSORSYMMETRY_DYNAMICSYMMETRY_H

namespace Eigen {

class DynamicSGroup
{
  public:
    inline explicit
    DynamicSGroup() : m_numIndices(1), m_elements(), m_generators(), m_globalFlags(0)
    {
        m_elements.push_back(ge(Generator(0, 0, 0)));
    }
    inline
    DynamicSGroup(const DynamicSGroup& o) : m_numIndices(o.m_numIndices), m_elements(o.m_elements), m_generators(o.m_generators), m_globalFlags(o.m_globalFlags) { }
    inline
    DynamicSGroup(DynamicSGroup&& o) : m_numIndices(o.m_numIndices), m_elements(), m_generators(o.m_generators), m_globalFlags(o.m_globalFlags)
    {
        std::swap(m_elements, o.m_elements);
    }
    inline DynamicSGroup&
    operator=(const DynamicSGroup& o)
    {
        m_numIndices = o.m_numIndices;
        m_elements = o.m_elements;
        m_generators = o.m_generators;
        m_globalFlags = o.m_globalFlags;
        return *this;
    }
    inline DynamicSGroup&
    operator=(DynamicSGroup&& o)
    {
        m_numIndices = o.m_numIndices;
        std::swap(m_elements, o.m_elements);
        m_generators = o.m_generators;
        m_globalFlags = o.m_globalFlags;
        return *this;
    }

    void
    add(int one, int two, int flags = 0);

    template<typename Gen_>
    inline void
    add(Gen_)
    {
        add(Gen_::One, Gen_::Two, Gen_::Flags);
    }
    inline void
    addSymmetry(int one, int two)
    {
        add(one, two, 0);
    }
    inline void
    addAntiSymmetry(int one, int two)
    {
        add(one, two, NegationFlag);
    }
    inline void
    addHermiticity(int one, int two)
    {
        add(one, two, ConjugationFlag);
    }
    inline void
    addAntiHermiticity(int one, int two)
    {
        add(one, two, NegationFlag | ConjugationFlag);
    }

    template<typename Op, typename RV, typename Index, std::size_t N, typename... Args>
    inline RV
    apply(const std::array<Index, N>& idx, RV initial, Args&&... args) const
    {
        eigen_assert(N >= m_numIndices && "Can only apply symmetry group to objects that have at least the required amount of indices.");

        for (std::size_t i = 0; i < size(); i++)
        {
            initial = Op::run(h_permute(i, idx, typename internal::gen_numeric_list<int, N>::type()), m_elements[i].flags, initial, std::forward<Args>(args)...);
        }

        return initial;
    }

    template<typename Op, typename RV, typename Index, typename... Args>
    inline RV
    apply(const std::vector<Index>& idx, RV initial, Args&&... args) const
    {
        eigen_assert(idx.size() >= m_numIndices && "Can only apply symmetry group to objects that have at least the required amount of indices.");

        for (std::size_t i = 0; i < size(); i++)
        {
            initial = Op::run(h_permute(i, idx), m_elements[i].flags, initial, std::forward<Args>(args)...);
        }

        return initial;
    }

    inline int
    globalFlags() const
    {
        return m_globalFlags;
    }
    inline std::size_t
    size() const
    {
        return m_elements.size();
    }

    template<typename Tensor_, typename... IndexTypes>
    inline internal::tensor_symmetry_value_setter<Tensor_, DynamicSGroup>
    operator()(Tensor_& tensor, typename Tensor_::Index firstIndex, IndexTypes... otherIndices) const
    {
        static_assert(sizeof...(otherIndices) + 1 == Tensor_::NumIndices, "Number of indices used to access a tensor coefficient must be equal to the rank of the tensor.");
        return operator()(tensor, std::array<typename Tensor_::Index, Tensor_::NumIndices> {{firstIndex, otherIndices...}});
    }

    template<typename Tensor_>
    inline internal::tensor_symmetry_value_setter<Tensor_, DynamicSGroup>
    operator()(Tensor_& tensor, std::array<typename Tensor_::Index, Tensor_::NumIndices> const& indices) const
    {
        return internal::tensor_symmetry_value_setter<Tensor_, DynamicSGroup>(tensor, *this, indices);
    }
  private:
    struct GroupElement
    {
        std::vector<int> representation;
        int flags;
        bool
        isId() const
        {
            for (std::size_t i = 0; i < representation.size(); i++)
                if (i != (size_t)representation[i])
                {
                    return false;
                }

            return true;
        }
    };
    struct Generator
    {
        int one;
        int two;
        int flags;
        constexpr inline
        Generator(int one_, int two_, int flags_) : one(one_), two(two_), flags(flags_) {}
    };

    std::size_t m_numIndices;
    std::vector<GroupElement> m_elements;
    std::vector<Generator> m_generators;
    int m_globalFlags;

    template<typename Index, std::size_t N, int... n>
    inline std::array<Index, N>
    h_permute(std::size_t which, const std::array<Index, N>& idx, internal::numeric_list<int, n...>) const
    {
        return std::array<Index, N> {{ idx[n >= m_numIndices ? n : m_elements[which].representation[n]]... }};
    }

    template<typename Index>
    inline std::vector<Index>
    h_permute(std::size_t which, std::vector<Index> idx) const
    {
        std::vector<Index> result;
        result.reserve(idx.size());

        for (auto k : m_elements[which].representation)
        {
            result.push_back(idx[k]);
        }

        for (std::size_t i = m_numIndices; i < idx.size(); i++)
        {
            result.push_back(idx[i]);
        }

        return result;
    }

    inline GroupElement
    ge(Generator const& g) const
    {
        GroupElement result;
        result.representation.reserve(m_numIndices);
        result.flags = g.flags;

        for (std::size_t k = 0; k < m_numIndices; k++)
        {
            if (k == (std::size_t)g.one)
            {
                result.representation.push_back(g.two);
            }

            else if (k == (std::size_t)g.two)
            {
                result.representation.push_back(g.one);
            }

            else
            {
                result.representation.push_back(int(k));
            }
        }

        return result;
    }

    GroupElement
    mul(GroupElement, GroupElement) const;
    inline GroupElement
    mul(Generator g1, GroupElement g2) const
    {
        return mul(ge(g1), g2);
    }

    inline GroupElement
    mul(GroupElement g1, Generator g2) const
    {
        return mul(g1, ge(g2));
    }

    inline GroupElement
    mul(Generator g1, Generator g2) const
    {
        return mul(ge(g1), ge(g2));
    }

    inline int
    findElement(GroupElement e) const
    {
        for (auto ee : m_elements)
        {
            if (ee.representation == e.representation)
            {
                return ee.flags ^ e.flags;
            }
        }

        return -1;
    }

    void
    updateGlobalFlags(int flagDiffOfSameGenerator);
};

// dynamic symmetry group that auto-adds the template parameters in the constructor
template<typename... Gen>
class DynamicSGroupFromTemplateArgs : public DynamicSGroup
{
  public:
    inline
    DynamicSGroupFromTemplateArgs() : DynamicSGroup()
    {
        add_all(internal::type_list<Gen...>());
    }
    inline
    DynamicSGroupFromTemplateArgs(DynamicSGroupFromTemplateArgs const& other) : DynamicSGroup(other) { }
    inline
    DynamicSGroupFromTemplateArgs(DynamicSGroupFromTemplateArgs&& other) : DynamicSGroup(other) { }
    inline DynamicSGroupFromTemplateArgs<Gen...>&
    operator=(const DynamicSGroupFromTemplateArgs<Gen...>& o)
    {
        DynamicSGroup::operator=(o);
        return *this;
    }
    inline DynamicSGroupFromTemplateArgs<Gen...>&
    operator=(DynamicSGroupFromTemplateArgs<Gen...>&& o)
    {
        DynamicSGroup::operator=(o);
        return *this;
    }

  private:
    template<typename Gen1, typename... GenNext>
    inline void
    add_all(internal::type_list<Gen1, GenNext...>)
    {
        add(Gen1());
        add_all(internal::type_list<GenNext...>());
    }

    inline void
    add_all(internal::type_list<>)
    {
    }
};

inline DynamicSGroup::GroupElement
DynamicSGroup::mul(GroupElement g1, GroupElement g2) const
{
    eigen_internal_assert(g1.representation.size() == m_numIndices);
    eigen_internal_assert(g2.representation.size() == m_numIndices);

    GroupElement result;
    result.representation.reserve(m_numIndices);

    for (std::size_t i = 0; i < m_numIndices; i++)
    {
        int v = g2.representation[g1.representation[i]];
        eigen_assert(v >= 0);
        result.representation.push_back(v);
    }

    result.flags = g1.flags ^ g2.flags;
    return result;
}

inline void
DynamicSGroup::add(int one, int two, int flags)
{
    eigen_assert(one >= 0);
    eigen_assert(two >= 0);
    eigen_assert(one != two);

    if ((std::size_t)one >= m_numIndices || (std::size_t)two >= m_numIndices)
    {
        std::size_t newNumIndices = (one > two) ? one : two + 1;

        for (auto& gelem : m_elements)
        {
            gelem.representation.reserve(newNumIndices);

            for (std::size_t i = m_numIndices; i < newNumIndices; i++)
            {
                gelem.representation.push_back(i);
            }
        }

        m_numIndices = newNumIndices;
    }

    Generator g{one, two, flags};
    GroupElement e = ge(g);

    /* special case for first generator */
    if (m_elements.size() == 1)
    {
        while (!e.isId())
        {
            m_elements.push_back(e);
            e = mul(e, g);
        }

        if (e.flags > 0)
        {
            updateGlobalFlags(e.flags);
        }

        // only add in case we didn't have identity
        if (m_elements.size() > 1)
        {
            m_generators.push_back(g);
        }

        return;
    }

    int p = findElement(e);

    if (p >= 0)
    {
        updateGlobalFlags(p);
        return;
    }

    std::size_t coset_order = m_elements.size();
    m_elements.push_back(e);

    for (std::size_t i = 1; i < coset_order; i++)
    {
        m_elements.push_back(mul(m_elements[i], e));
    }

    m_generators.push_back(g);

    std::size_t coset_rep = coset_order;

    do
    {
        for (auto g : m_generators)
        {
            e = mul(m_elements[coset_rep], g);
            p = findElement(e);

            if (p < 0)
            {
                // element not yet in group
                m_elements.push_back(e);

                for (std::size_t i = 1; i < coset_order; i++)
                {
                    m_elements.push_back(mul(m_elements[i], e));
                }
            }

            else if (p > 0)
            {
                updateGlobalFlags(p);
            }
        }

        coset_rep += coset_order;
    }
    while (coset_rep < m_elements.size());
}

inline void
DynamicSGroup::updateGlobalFlags(int flagDiffOfSameGenerator)
{
    switch (flagDiffOfSameGenerator)
    {
    case 0:
    default:
        // nothing happened
        break;

    case NegationFlag:
        // every element is it's own negative => whole tensor is zero
        m_globalFlags |= GlobalZeroFlag;
        break;

    case ConjugationFlag:
        // every element is it's own conjugate => whole tensor is real
        m_globalFlags |= GlobalRealFlag;
        break;

    case (NegationFlag | ConjugationFlag):
        // every element is it's own negative conjugate => whole tensor is imaginary
        m_globalFlags |= GlobalImagFlag;
        break;
        /* NOTE:
         *   since GlobalZeroFlag == GlobalRealFlag | GlobalImagFlag, if one generator
         *   causes the tensor to be real and the next one to be imaginary, this will
         *   trivially give the correct result
         */
    }
}

} // end namespace Eigen

#endif // EIGEN_CXX11_TENSORSYMMETRY_DYNAMICSYMMETRY_H

/*
 * kate: space-indent on; indent-width 2; mixedindent off; indent-mode cstyle;
 */
