//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2019 Finn Smith
//
// 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 contains an implemesntation of fast boolean matrices up to
// dimension 8 x 8.

#include "libsemigroups/bmat8.hpp"

#include <algorithm>  // for uniform_int_distribution
#include <array>      // for array
#include <climits>    // for CHAR_BIT
#include <iostream>   // for operator<<, ostringstream
#include <random>     // for mt19937, random_device
#include <vector>     // for vector

#include "libsemigroups/exception.hpp"  // for LIBSEMIGROUPS_EXCEPTION

namespace libsemigroups {

  ////////////////////////////////////////////////////////////////////////
  // Static helper data and functions
  ////////////////////////////////////////////////////////////////////////

  static std::array<uint64_t, 8> const ROW_MASK = {0xff00000000000000,
                                                   0xff000000000000,
                                                   0xff0000000000,
                                                   0xff00000000,
                                                   0xff000000,
                                                   0xff0000,
                                                   0xff00,
                                                   0xff};

  static std::array<uint64_t, 8> const COL_MASK = {0x8080808080808080,
                                                   0x4040404040404040,
                                                   0x2020202020202020,
                                                   0x1010101010101010,
                                                   0x808080808080808,
                                                   0x404040404040404,
                                                   0x202020202020202,
                                                   0x101010101010101};

  static std::array<uint64_t, 64> const BIT_MASK = {0x8000000000000000,
                                                    0x4000000000000000,
                                                    0x2000000000000000,
                                                    0x1000000000000000,
                                                    0x800000000000000,
                                                    0x400000000000000,
                                                    0x200000000000000,
                                                    0x100000000000000,
                                                    0x80000000000000,
                                                    0x40000000000000,
                                                    0x20000000000000,
                                                    0x10000000000000,
                                                    0x8000000000000,
                                                    0x4000000000000,
                                                    0x2000000000000,
                                                    0x1000000000000,
                                                    0x800000000000,
                                                    0x400000000000,
                                                    0x200000000000,
                                                    0x100000000000,
                                                    0x80000000000,
                                                    0x40000000000,
                                                    0x20000000000,
                                                    0x10000000000,
                                                    0x8000000000,
                                                    0x4000000000,
                                                    0x2000000000,
                                                    0x1000000000,
                                                    0x800000000,
                                                    0x400000000,
                                                    0x200000000,
                                                    0x100000000,
                                                    0x80000000,
                                                    0x40000000,
                                                    0x20000000,
                                                    0x10000000,
                                                    0x8000000,
                                                    0x4000000,
                                                    0x2000000,
                                                    0x1000000,
                                                    0x800000,
                                                    0x400000,
                                                    0x200000,
                                                    0x100000,
                                                    0x80000,
                                                    0x40000,
                                                    0x20000,
                                                    0x10000,
                                                    0x8000,
                                                    0x4000,
                                                    0x2000,
                                                    0x1000,
                                                    0x800,
                                                    0x400,
                                                    0x200,
                                                    0x100,
                                                    0x80,
                                                    0x40,
                                                    0x20,
                                                    0x10,
                                                    0x8,
                                                    0x4,
                                                    0x2,
                                                    0x1};

  // Cyclically shifts bits to left by 8m
  // https://stackoverflow.com/a/776523
  static uint64_t cyclic_shift(uint64_t n) noexcept {
    const unsigned int mask
        = (CHAR_BIT * sizeof(n) - 1);  // assumes width is a power of 2.
    // assert ( (c<=mask) &&"rotate by type width or more");
    unsigned int c = 8;
    c &= mask;
    return (n << c) | (n >> ((-c) & mask));
  }

  // Given 8 x 8 matrices A and B, this function returns a matrix where each
  // row is the corresponding row of B if that row of B is a subset of the
  // corresponding row of A, and 0 otherwise.
  static uint64_t zero_if_row_not_contained(uint64_t A, uint64_t B) noexcept {
    uint64_t w = A & B;
    for (size_t i = 0; i < 8; ++i) {
      if ((w & ROW_MASK[i]) == (B & ROW_MASK[i])) {
        w |= (B & ROW_MASK[i]);
      } else {
        w &= ~(ROW_MASK[i]);
      }
    }
    return w;
  }

  static std::array<uint64_t, 8> for_sorting = {0, 0, 0, 0, 0, 0, 0, 0};

  static inline void swap_for_sorting(size_t a, size_t b) noexcept {
    if (for_sorting[b] < for_sorting[a]) {
      std::swap(for_sorting[a], for_sorting[b]);
    }
  }

  ////////////////////////////////////////////////////////////////////////
  // BMat8 - static data members - private
  ////////////////////////////////////////////////////////////////////////

  std::random_device                      BMat8::_rd;
  std::mt19937                            BMat8::_gen(_rd());
  std::uniform_int_distribution<uint64_t> BMat8::_dist(0, 0xffffffffffffffff);

  ////////////////////////////////////////////////////////////////////////
  // BMat8 - constructors - public
  ////////////////////////////////////////////////////////////////////////

  BMat8::BMat8(std::vector<std::vector<bool>> const& mat) {
    if (0 == mat.size() || mat.size() > 8) {
      LIBSEMIGROUPS_EXCEPTION(
          "expected a vector with size in [1, 8], got a vector of size %d",
          mat.size());
    }
    _data        = 0;
    uint64_t pow = 1;
    pow          = pow << 63;
    for (auto row : mat) {
      if (row.size() != mat.size()) {
        LIBSEMIGROUPS_EXCEPTION("the vectors must all have the same length!");
      }
      for (auto entry : row) {
        if (entry) {
          _data ^= pow;
        }
        pow = pow >> 1;
      }
      pow = pow >> (8 - mat.size());
    }
  }

  // Not noexcept because it can throw.
  BMat8 BMat8::random(size_t dim) {
    if (0 == dim || dim > 8) {
      LIBSEMIGROUPS_EXCEPTION("the argument should be in [1, 8], got %d", dim);
    }
    BMat8 bm = BMat8::random();
    for (size_t i = dim; i < 8; ++i) {
      bm._data &= ~(ROW_MASK[i]);
      bm._data &= ~(COL_MASK[i]);
    }
    return bm;
  }

  ////////////////////////////////////////////////////////////////////////
  // BMat8 - member functions - public
  ////////////////////////////////////////////////////////////////////////

  BMat8 BMat8::row_space_basis() const noexcept {
    uint64_t out            = 0;
    uint64_t combined_masks = 0;

    BMat8 bm(_data);
    bm.sort_rows();
    uint64_t no_dups = bm._data;
    for (size_t i = 0; i < 7; ++i) {
      combined_masks |= ROW_MASK[i];
      while ((no_dups & ROW_MASK[i + 1]) << 8 == (no_dups & ROW_MASK[i])
             && (no_dups & ROW_MASK[i]) != 0) {
        no_dups = ((no_dups & combined_masks)
                   | (no_dups & ~combined_masks & ~ROW_MASK[i + 1]) << 8);
      }
    }
    uint64_t cm = no_dups;
    for (size_t i = 0; i < 7; ++i) {
      cm = cyclic_shift(cm);
      out |= zero_if_row_not_contained(no_dups, cm);
    }
    for (size_t i = 0; i < 8; ++i) {
      if ((out & ROW_MASK[i]) == (no_dups & ROW_MASK[i])) {
        out &= ~ROW_MASK[i];
      } else {
        out |= (no_dups & ROW_MASK[i]);
      }
    }
    combined_masks = 0;
    for (size_t i = 0; i < 8; ++i) {
      combined_masks |= ROW_MASK[i];
      while ((out & ROW_MASK[i]) == 0 && ((out & ~combined_masks) != 0)) {
        out = (out & combined_masks) | ((out & ~combined_masks) << 8);
      }
    }
    return BMat8(out);
  }

  void BMat8::set(size_t i, size_t j, bool val) {
    if (7 < i || 7 < j) {
      LIBSEMIGROUPS_EXCEPTION(
          "the arguments should each be at most 7, got %d as the %s argument",
          (7 < i) ? i : j,
          (7 < i) ? "first" : "second");
    }
    _data ^= (-val ^ _data) & BIT_MASK[8 * i + j];
  }

  BMat8 BMat8::operator*(BMat8 const& that) const noexcept {
    uint64_t y    = that.transpose()._data;
    uint64_t data = 0;
    uint64_t tmp  = 0;
    uint64_t diag = 0x8040201008040201;
    for (int i = 0; i < 8; ++i) {
      tmp = _data & y;
      tmp |= tmp >> 1;
      tmp |= tmp >> 2;
      tmp |= tmp >> 4;
      tmp &= 0x0101010101010101;
      tmp *= 255;
      tmp &= diag;
      data |= tmp;
      y    = cyclic_shift(y);
      diag = cyclic_shift(diag);
    }
    return BMat8(data);
  }

  size_t BMat8::row_space_size() const {
    std::array<char, 256> lookup;
    lookup.fill(0);
    std::vector<uint8_t> row_vec = row_space_basis().rows();
    auto                 last = std::remove(row_vec.begin(), row_vec.end(), 0);
    row_vec.erase(last, row_vec.end());
    for (uint8_t x : row_vec) {
      lookup[x] = true;
    }
    std::vector<uint8_t> row_space(row_vec.begin(), row_vec.end());
    for (size_t i = 0; i < row_space.size(); ++i) {
      for (uint8_t row : row_vec) {
        uint8_t x = row_space[i] | row;
        if (!lookup[x]) {
          row_space.push_back(x);
          lookup[x] = true;
        }
      }
    }
    return row_space.size() + 1;
  }

  std::vector<uint8_t> BMat8::rows() const {
    std::vector<uint8_t> rows;
    for (size_t i = 0; i < 8; ++i) {
      uint8_t row = static_cast<uint8_t>(_data << (8 * i) >> 56);
      rows.push_back(row);
    }
    return rows;
  }

  ////////////////////////////////////////////////////////////////////////
  // BMat8 - member functions - private
  ////////////////////////////////////////////////////////////////////////

  void BMat8::sort_rows() noexcept {
    for (size_t i = 0; i < 8; ++i) {
      for_sorting[i] = (_data << 8 * i) & ROW_MASK[0];
    }
    swap_for_sorting(0, 1);
    swap_for_sorting(2, 3);
    swap_for_sorting(0, 2);
    swap_for_sorting(1, 3);
    swap_for_sorting(1, 2);
    swap_for_sorting(4, 5);
    swap_for_sorting(6, 7);
    swap_for_sorting(4, 6);
    swap_for_sorting(5, 7);
    swap_for_sorting(5, 6);
    swap_for_sorting(0, 4);
    swap_for_sorting(1, 5);
    swap_for_sorting(1, 4);
    swap_for_sorting(2, 6);
    swap_for_sorting(3, 7);
    swap_for_sorting(3, 6);
    swap_for_sorting(2, 4);
    swap_for_sorting(3, 5);
    swap_for_sorting(3, 4);
    _data = 0;
    for (size_t i = 0; i < 7; ++i) {
      _data |= for_sorting[i];
      _data = (_data >> 8);
    }
    _data |= for_sorting[7];
  }

  ////////////////////////////////////////////////////////////////////////
  // BMat8 - friend functions
  ////////////////////////////////////////////////////////////////////////

  std::ostringstream& operator<<(std::ostringstream& os, BMat8 const& bm) {
    uint64_t x   = bm.to_int();
    uint64_t pow = 1;
    pow          = pow << 63;
    for (size_t i = 0; i < 8; ++i) {
      for (size_t j = 0; j < 8; ++j) {
        if (pow & x) {
          os << "1";
        } else {
          os << "0";
        }
        x = x << 1;
      }
      os << "\n";
    }
    return os;
  }

  ////////////////////////////////////////////////////////////////////////
  // BMat8 helpers
  ////////////////////////////////////////////////////////////////////////

  namespace bmat8_helpers {
    size_t minimum_dim(BMat8 const& x) noexcept {
      size_t   i = 0;
      uint64_t c = x.to_int();
      if (!c) {
        return 0;
      }
      uint64_t d = x.to_int();
      uint64_t y = x.transpose().to_int();
      uint64_t z = x.transpose().to_int();

      do {
        d = d >> 8;
        y = y >> 8;
        ++i;
      } while (i < 8 && (d << (8 * i)) == c && (y << (8 * i)) == z);

      return 9 - i;
    }
  }  // namespace bmat8_helpers
}  // namespace libsemigroups
