//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2020 James D. Mitchell
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

// This file defines a class UF used to make an equivalence relation on the
// integers {0, .., n - 1}, using the UNION-FIND METHOD: new pairs can be added
// and the appropriate classes combined quickly.

// The implementation here is based on the implementation in the datastructures
// package for GAP (v0.2.5) by Markus Pfeiffer, Max Horn, Christopher
// Jefferson, Steve Linton.

// An earlier version of the UF class was written by M. Young.

#ifndef LIBSEMIGROUPS_UF_HPP_
#define LIBSEMIGROUPS_UF_HPP_

#include <algorithm>      // for for_each
#include <array>          // for array
#include <cstddef>        // for size_t
#include <numeric>        // for iota
#include <unordered_map>  // for unordered_map
#include <vector>         // for vector

#include "adapters.hpp"  // for Hash
#include "config.hpp"    // for LIBSEMIGROUPS_SIZEOF_VOID_P
#include "debug.hpp"     // for LIBSEMIGROUPS_ASSERT
#include "iterator.hpp"  // for ConstIteratorStateless
#include "types.hpp"     // for SmallestInteger

namespace libsemigroups {
  namespace detail {

#if LIBSEMIGROUPS_SIZEOF_VOID_P == 8
    constexpr size_t RANK_BITS = 6;
#else
    constexpr size_t RANK_BITS = 5;
#endif

    template <typename T>
    class UF {
      ////////////////////////////////////////////////////////////////////////
      // Aliases - private
      ////////////////////////////////////////////////////////////////////////

      using node_type = typename T::value_type;
      using rank_type = typename T::value_type;

     public:
      ////////////////////////////////////////////////////////////////////////
      // Aliases - public
      ////////////////////////////////////////////////////////////////////////
      using size_type      = size_t;
      using container_type = T;
      using index_type     = typename T::value_type;

      ////////////////////////////////////////////////////////////////////////
      // Constructors + destructor
      ////////////////////////////////////////////////////////////////////////

      // not noexcept because the constructors of std::vector and std::array
      // aren't
      explicit UF(container_type&& c) : _data(std::move(c)) {
        init_data(_data.begin(), _data.end());
      }

      // not noexcept because the constructors of std::vector and std::array
      // aren't
      explicit UF(container_type const& c) : _data(c) {
        init_data(_data.begin(), _data.end());
      }

      // not noexcept because the constructors of std::vector and std::array
      // aren't
      explicit UF(size_type size = 0) : _data() {
        set_data_capacity(_data, size);
        std::iota(_data.begin(), _data.end(), 0);
        init_data(_data.begin(), _data.end());
      }

      // not noexcept because the constructors of std::vector and std::array
      // aren't
      UF(UF const&)            = default;
      UF& operator=(UF const&) = default;
      UF(UF&&)                 = default;
      UF& operator=(UF&&)      = default;
      ~UF()                    = default;

      ////////////////////////////////////////////////////////////////////////
      // Union-Find
      ////////////////////////////////////////////////////////////////////////

      // not noexcept because std::array::operator[] and
      // std::vector::operator[] aren't
      index_type find(index_type x) const {
        LIBSEMIGROUPS_ASSERT(x < _data.size());
        while (true) {
          index_type y = index(_data[x]);
          LIBSEMIGROUPS_ASSERT(0 <= y && y < _data.size());
          if (y == x) {
            return y;
          }
          index_type z = index(_data[y]);
          LIBSEMIGROUPS_ASSERT(0 <= z && z < _data.size());
          if (y == z) {
            return y;
          }
          _data[x] = node(z) | rank(_data[x]);
          x        = z;
        }
      }

      // not noexcept because UF::find isn't
      void unite(index_type x, index_type y) {
        LIBSEMIGROUPS_ASSERT(x < _data.size());
        LIBSEMIGROUPS_ASSERT(y < _data.size());
        x = find(x);
        y = find(y);
        if (x == y) {
          return;
        }
        rank_type rx = rank(_data[x]);
        rank_type ry = rank(_data[y]);
        if (rx > ry) {
          _data[y] = node(x) | ry;
        } else {
          _data[x] = node(y) | rx;
          if (rx == ry) {
            _data[y] = node(y) | (ry + 1);
          }
        }
      }

      ////////////////////////////////////////////////////////////////////////
      // operators
      ////////////////////////////////////////////////////////////////////////

      // not noexcept because UF::contains isn't
      bool operator==(UF const& that) const {
        return contains(that) && that.contains(*this);
      }

      // not noexcept because UF::operator== isn't
      bool operator!=(UF const& that) const {
        return !(*this == that);
      }

      // not noexcept because find isn't
      // if this contains that
      bool contains(UF const& that) const {
        LIBSEMIGROUPS_ASSERT(size() == that.size());
        // reps of that to reps of this
        std::unordered_map<index_type, index_type> lookup;

        for (index_type i = 0; i < size(); ++i) {
          index_type j  = that.find(i);
          auto       it = lookup.find(j);
          if (it == lookup.end()) {
            lookup.emplace(j, find(i));
          } else if (it->second != find(i)) {
            return false;
          }
        }
        return true;
      }

      ////////////////////////////////////////////////////////////////////////
      // Container like
      ////////////////////////////////////////////////////////////////////////

      // noexcept because std::array::size and std::vector::size are noexcept
      size_type size() const noexcept {
        return _data.size();
      }

      // noexcept because std::array::empty and std::vector::empty are noexcept
      bool empty() const noexcept {
        return _data.empty();
      }

      // not noexcept because std::array::operator[] and
      // std::vector::operator[] aren't
      size_type number_of_blocks() const {
        size_type count = 0;
        for (index_type i = 0; i < _data.size(); ++i) {
          if (index(_data[i]) == i) {
            count++;
          }
        }
        return count;
      }

      ////////////////////////////////////////////////////////////////////////
      // Modifiers
      ////////////////////////////////////////////////////////////////////////

      // not noexcept because std::array::operator[] and
      // std::vector::operator[] aren't
      void compress() {
        for (index_type i = 0; i < _data.size(); ++i) {
          index_type j  = find(index(_data[i]));
          rank_type  rk = (j == i ? 0 : 1);
          _data[i]      = node(j) | rk;
        }
      }

      // not noexcept because compress isn't
      void normalize() {
        compress();
        // old -> new class indexing
        std::unordered_map<index_type, index_type> lookup;
        container_type                             data;
        set_data_capacity(data, size());
        for (index_type i = 0; i < size(); ++i) {
          index_type j  = find(i);
          auto       it = lookup.find(j);
          if (it == lookup.end()) {
            lookup.emplace(j, i);
            data[i] = node(i);  // rank 0
          } else {
            data[i] = node(it->second) | 1;
          }
        }
        std::swap(_data, data);
      }

      // not noexcept because unite isn't
      void join(UF const& that) {
        LIBSEMIGROUPS_ASSERT(size() == that.size());
        for (index_type i = 0; i < _data.size(); ++i) {
          unite(index(_data[i]), index(that._data[i]));
        }
      }

      void swap(UF& that) noexcept {
        std::swap(_data, that._data);
      }

      ////////////////////////////////////////////////////////////////////////
      // Iterators
      ////////////////////////////////////////////////////////////////////////

      struct IteratorTraits : ConstIteratorTraits<container_type> {
        using internal_iterator_type = typename ConstIteratorTraits<
            container_type>::internal_iterator_type;
        using state_type = std::pair<internal_iterator_type,   // _data.cbegin()
                                     internal_iterator_type>;  // _data.cend()
        using value_type = index_type;
        using reference  = value_type;
        using const_reference   = value_type const;
        using const_pointer     = value_type const*;
        using pointer           = value_type*;
        using iterator_category = std::bidirectional_iterator_tag;

        struct Deref {
          value_type operator()(state_type, internal_iterator_type const& it)
              const noexcept {
            return index(*it);
          }
        };

        struct PrefixIncrement {
          void operator()(state_type const&       st,
                          internal_iterator_type& it) const noexcept {
            index_type i = std::distance(st.first, it);
            do {
              ++it;
              ++i;
            } while (it != st.second && index(*it) != i);
          }
        };

        struct PrefixDecrement {
          void operator()(state_type const&       st,
                          internal_iterator_type& it) const noexcept {
            index_type i = std::distance(st.first, it);
            do {
              --it;
              --i;
            } while (it != st.first && index(*it) != i);
          }
        };
      };

      using const_iterator = detail::ConstIteratorStateful<IteratorTraits>;
      using const_reverse_iterator = std::reverse_iterator<const_iterator>;

      // not noexcept because std::array::operator[] and
      // std::vector::operator[] aren't
      const_iterator cbegin() const {
        auto it = const_iterator(std::make_pair(_data.cbegin(), _data.cend()),
                                 _data.cbegin());
        return (index(_data[0]) == 0 ? it : ++it);
      }

      // not noexcept because std::array::operator[] and
      // std::vector::operator[] aren't
      const_iterator cend() const {
        return const_iterator(std::make_pair(_data.cbegin(), _data.cend()),
                              _data.cend());
      }

      // not noexcept because cend() isn't
      const_reverse_iterator crbegin() const {
        return const_reverse_iterator(cend());
      }

      // not noexcept because cbegin() isn't
      const_reverse_iterator crend() const {
        return const_reverse_iterator(cbegin());
      }

      struct DataIteratorTraits : ConstIteratorTraits<container_type> {
        using internal_iterator_type = typename ConstIteratorTraits<
            container_type>::internal_iterator_type;
        using value_type      = index_type;
        using reference       = value_type;
        using const_reference = value_type const;
        using const_pointer   = value_type const*;
        using pointer         = value_type*;

        struct Deref {
          value_type
          operator()(internal_iterator_type const& it) const noexcept {
            return index(*it);
          }
        };
      };

      using const_data_iterator
          = detail::ConstIteratorStateless<DataIteratorTraits>;

      // noexcept because _data.cbegin is noexcept
      const_data_iterator cbegin_data() const noexcept {
        return const_data_iterator(_data.cbegin());
      }

      // noexcept because _data.cend is noexcept
      const_data_iterator cend_data() const noexcept {
        return const_data_iterator(_data.cend());
      }

      struct RankIteratorTraits : ConstIteratorTraits<container_type> {
        using internal_iterator_type = typename ConstIteratorTraits<
            container_type>::internal_iterator_type;
        using value_type      = rank_type;
        using reference       = value_type;
        using const_reference = value_type const;
        using const_pointer   = value_type const*;
        using pointer         = value_type*;

        struct Deref {
          value_type
          operator()(internal_iterator_type const& it) const noexcept {
            return rank(*it);
          }
        };
      };

      using const_rank_iterator
          = detail::ConstIteratorStateless<RankIteratorTraits>;

      // noexcept because _data.cbegin is noexcept
      const_rank_iterator cbegin_rank() const noexcept {
        return const_rank_iterator(_data.cbegin());
      }

      // noexcept because _data.cbegin is noexcept
      const_rank_iterator cend_rank() const noexcept {
        return const_rank_iterator(_data.cend());
      }

      size_t hash() const {
        return Hash<container_type>()(_data);
      }

     private:
      ////////////////////////////////////////////////////////////////////////
      // Private
      ////////////////////////////////////////////////////////////////////////

      static constexpr index_type rank_mask
          = ((index_type(1) << RANK_BITS) - index_type(1));

      static inline rank_type rank(node_type i) noexcept {
        return i & rank_mask;
      }

      static inline node_type node(index_type i) noexcept {
        return i << RANK_BITS;
      }

      static inline index_type index(node_type i) noexcept {
        return i >> RANK_BITS;
      }

      template <typename SFINAE = void>
      static auto set_data_capacity(container_type& data, size_t N)
          -> std::enable_if_t<
              std::is_base_of<std::vector<node_type>, container_type>::value,
              SFINAE> {
        data.resize(N, 0);
      }

      template <typename SFINAE = void>
      static auto set_data_capacity(container_type&, size_t const)
          -> std::enable_if_t<
              !std::is_base_of<std::vector<node_type>, container_type>::value,
              SFINAE> {}

     protected:
      ////////////////////////////////////////////////////////////////////////
      // Protected
      ////////////////////////////////////////////////////////////////////////

      // not noexcept because std::for_each can throw
      static void init_data(typename container_type::iterator first,
                            typename container_type::iterator last) {
        std::for_each(first, last, [](index_type& i) { i = node(i); });
      }

      mutable container_type _data;
    };

    template <typename T>
    inline void swap(UF<T>& x, UF<T>& y) noexcept {
      x.swap(y);
    }

    // Dynamic Union-Find
    template <typename T = size_t>
    class Duf : public UF<std::vector<T>> {
     public:
      using UF<std::vector<T>>::UF;
      using size_type = typename UF<std::vector<T>>::size_type;
      // not noexcept because std::vector::resize can throw
      void resize(size_type N) {
        size_type const M = this->size();
        if (N <= M) {
          return;
        }
        this->_data.resize(N);
        std::iota(this->_data.begin() + M, this->_data.end(), M);
        this->init_data(this->_data.begin() + M, this->_data.end());
      }
    };

    // Static Union-Find
    template <size_t N,
              typename T =
                  typename SmallestInteger<N*(RANK_BITS == 5 ? 32 : 64)>::type>
    using Suf = UF<std::array<T, N>>;
  }  // namespace detail

  template <typename T>
  struct Hash<detail::Duf<T>> {
    size_t operator()(detail::Duf<T> const& x) const {
      return x.hash();
    }
  };
}  // namespace libsemigroups
#endif  // LIBSEMIGROUPS_UF_HPP_
