#include "Polyomino.h"

#include "Position.h"

#include <vector>
#include <set>
#include <iostream>
#include <climits>
#include <algorithm>
#include <utility>
#include <cassert>

Polyomino::Polyomino(std::set<Position>&& positions) : positions(0) {
    int minX = INT_MAX;
    int maxX = INT_MIN;
    int minY = INT_MAX;
    int maxY = INT_MIN;
    for (const Position position : positions) {
        if (position.x < minX) minX = position.x;
        if (position.x > maxX) maxX = position.x;
        if (position.y < minY) minY = position.y;
        if (position.y > maxY) maxY = position.y;
    }

    this->length = std::max(maxX - minX + 1, maxY - minY + 1);

    // we normalize here instead of calling this->normalize() to reduce the number of calculations for the generation algorithm
    Polyomino newPolyomino({}, this->length);
    for (Position position : positions) {
        newPolyomino.insert(Position(position.x - minX, position.y - minY));
    }
    this->positions = std::move(newPolyomino.positions);
}

Polyomino::Polyomino(PolyominoData&& positions, std::int8_t length) :
    positions(std::move(positions)),
    length(length) {
}

void Polyomino::normalize() {
    int minX = INT_MAX;
    int minY = INT_MAX;
    for (const Position position : *this) {
        if (position.x < minX) minX = position.x;
        if (position.y < minY) minY = position.y;
    }

    Polyomino newPolyomino({}, this->length);
    for (const Position position : *this) {
        newPolyomino.insert(Position(position.x - minX, position.y - minY));
    }
    this->positions = std::move(newPolyomino.positions);
}

void Polyomino::rotateCW() {
    Polyomino temp({}, this->length);
    for (const Position position : *this) {
        temp.insert(Position(position.y, (length - 1) - (position.x)));
    }
    this->positions = std::move(temp.positions);
}

void Polyomino::rotate180() {
    Polyomino temp({}, this->length);
    for (const Position position : *this) {
        temp.insert(Position((length - 1) - (position.x), (length - 1) - (position.y)));
    }
    this->positions = std::move(temp.positions);
}

void Polyomino::rotateCCW() {
    Polyomino temp({}, this->length);
    for (const Position position : *this) {
        temp.insert(Position((length - 1) - (position.y), position.x));
    }
    this->positions = std::move(temp.positions);
}

void Polyomino::goToSpawnPosition() {
    // initialize array
    std::vector<int> empty(this->length, 0);
    std::vector<std::vector<int>> linesCompleteness(4, empty);

    // calculates amount of squares per rows and columns
    for (const Position position : *this) {
        linesCompleteness.at(0).at(position.y) += 1;                // 0 = bottom to top = no rotation
        linesCompleteness.at(1).at((length - 1) - position.x) += 1; // 1 = right to left = CW
        linesCompleteness.at(2).at((length - 1) - position.y) += 1; // 2 = top to bottom = 180
        linesCompleteness.at(3).at(position.x) += 1;                // 3 = left to right = CCW
    }

    // count empty lines and push the non-empty lines to the start of each vector
    int horizontalEmptyLines = 0;
    int verticalEmptyLines = 0;
    for (int i = 0; i < 4; i++) {
        for (int j = this->length - 1; j >= 0; j--) {
            if (linesCompleteness.at(i).at(j) == 0) {
                linesCompleteness.at(i).erase(linesCompleteness.at(i).begin() + j);
                linesCompleteness.at(i).push_back(0);

                if (i == 0) horizontalEmptyLines++;
                if (i == 1) verticalEmptyLines++;
            }
        }
    }

    // we ignore the sides which are not the widest
    bool currentFlattestSides[4] = {true, true, true, true};
    if (horizontalEmptyLines < verticalEmptyLines) {
        currentFlattestSides[0] = false;
        currentFlattestSides[2] = false;
    }
    if (verticalEmptyLines < horizontalEmptyLines) {
        currentFlattestSides[1] = false;
        currentFlattestSides[3] = false;
    }

    // checks for the flattest side
    int sideToBeOn = -1;
    this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, false);

    // if ther's no winner, checks for the side which has the flattest side to its left
    if (sideToBeOn == -1) {
        this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, true);
    }
    
    // if there's still no winner we take the side (amongst the remaining ones) that's the smallest once rotated
    if (sideToBeOn == -1) {
        std::vector<Polyomino> candidateRotations;
        std::vector<int> candidateSides;
        for (int i = 0; i < 4; i++) {
            this->normalize();
            if (currentFlattestSides[i]) {
                candidateRotations.push_back(*this);
                candidateSides.push_back(i);
            }
            this->rotateCW();
        }
        sideToBeOn = candidateSides.at(std::distance(candidateRotations.begin(), std::min_element(candidateRotations.begin(), candidateRotations.end())));
    }
    
    switch (sideToBeOn) {
        case 1 : {this->rotateCW(); break;}
        case 2 : {this->rotate180(); break;}
        case 3 : {this->rotateCCW(); break;}
        default: break;
    }
    if (sideToBeOn % 2 == 1) {
        std::swap(verticalEmptyLines, horizontalEmptyLines);
    }

    
    int minX = INT_MAX;
    int minY = INT_MAX;
    for (const Position position : *this) {
        if (position.x < minX) minX = position.x;
        if (position.y < minY) minY = position.y;
    }

    // center the piece with an up bias
    Polyomino temp({}, this->length);
    for (const Position position : *this) {
        temp.insert(Position((position.x - minX) + (verticalEmptyLines / 2), (position.y - minY) + ((horizontalEmptyLines + 1) / 2)));
    }
    this->positions = std::move(temp.positions);
}

void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesCompleteness, bool currentFlattestSides[4], int& sideToBeOn, bool checkLeftSide) const {
    for (int j = 0; j < this->length; j++) {
        // we check which sides are the flattest on this line
        int max = 0;
        std::set<int> maxOwners;
        for (int i = 0; i < 4; i++) {
            if (!currentFlattestSides[i]) continue;

            int sideToCheck = i;
            if (checkLeftSide) {
                sideToCheck = (i + 3) % 4;
            }

            if (linesCompleteness.at(sideToCheck).at(j) > max) {
                max = linesCompleteness.at(sideToCheck).at(j);
                maxOwners.clear();
                maxOwners.insert(i);
            }
            else if (linesCompleteness.at(sideToCheck).at(j) == max) {
                maxOwners.insert(i);
            }
        }

        // if there's no tie we choose the only side
        if (maxOwners.size() == 1) {
            sideToBeOn = *maxOwners.begin();
            return;
        }

        // else we only keep the flattest on this line and ignore the others
        else {
            for (int i = 0; i < 4; i++) {
                currentFlattestSides[i] = currentFlattestSides[i] && maxOwners.contains(i);
            }
        }
    }
}

bool Polyomino::isConvex() const {
    for (int j = 0; j < this->length; j++) {
        bool startedLine = false;
        bool completedLine = false;
        bool startedColumn = false;
        bool completedColumn = false;
        for (int i = 0; i < this->length; i++) {
            if (this->contains(Position(i, j))) {
                if (completedLine) return false;
                else startedLine = true;
            }
            else {
                if (startedLine) completedLine = true;
            }

            if (this->contains(Position(j, i))) {
                if (completedColumn) return false;
                else startedColumn = true;
            }
            else {
                if (startedColumn) completedColumn = true;
            }
        }
    }
    return true;
}

bool Polyomino::hasHole() const {
    // add every empty square on the outer of the box containing the polyomino
    Polyomino temp({}, this->length);
    for (int i = 0; i < this->length - 1; i++) {
        this->tryToInsertPosition(temp, Position(i, 0));                                   // up row
        this->tryToInsertPosition(temp, Position(this->length - 1, i));                    // rigth column
        this->tryToInsertPosition(temp, Position(this->length - 1 - i, this->length - 1)); // bottom row
        this->tryToInsertPosition(temp, Position(0, this->length - 1 - i));                // left column
    }

    // if we didn't reached all empty squares in the box then there was some contained within the polyomino, i.e. there was a hole
    return (temp.getSize() < (this->length * this->length) - this->positions.size());
}

void Polyomino::tryToInsertPosition(Polyomino& emptyPositions, const Position& candidate) const {
    if (candidate.x >= this->length || candidate.x < 0 || candidate.y >= this->length || candidate.y < 0) return;
    if (this->contains(candidate) || emptyPositions.contains(candidate)) return;

    // if it's a new empty square, try its neighbors
    emptyPositions.insert(candidate);
    tryToInsertPosition(emptyPositions, Position(candidate.x, candidate.y + 1));
    tryToInsertPosition(emptyPositions, Position(candidate.x + 1, candidate.y));
    tryToInsertPosition(emptyPositions, Position(candidate.x, candidate.y - 1));
    tryToInsertPosition(emptyPositions, Position(candidate.x - 1, candidate.y));
}

const Polyomino::PolyominoData& Polyomino::getPositionsData() const {
    return this->positions;
}

int Polyomino::getLength() const {
    return this->length;
}

int Polyomino::getPolyominoSize() const {
    return this->positions.size();
}

bool Polyomino::operator<(const Polyomino& other) const {
    if (this->length != other.length) return this->length < other.length;

    assert(other.positions.any() && "The other polyomino is empty !");

    static const PolyominoData longMask = 0xFFFFFFFFFFFFFFFF;
    const int longsNeeded = this->length * this->length / 64 + 1;

    for (int i = 0; i < longsNeeded; i++) {
        unsigned long l1 = (this->positions >> (i * sizeof(std::uint64_t)) & longMask).to_ulong();
        unsigned long l2 = (other.positions >> (i * sizeof(std::uint64_t)) & longMask).to_ulong();
        if (l1 != l2) {
            return l1 < l2;
        }
    }

    return false;
}

bool Polyomino::operator==(const Polyomino& other) const {
    return this->positions == other.positions;
}

std::ostream& operator<<(std::ostream& os, const Polyomino& polyomino) {
    for (int y = polyomino.length - 1; y >= 0; y--) {
        for (int x = 0; x < polyomino.length; x++) {
            if (polyomino.contains(Position(x, y))) {
                os << "*";
            }
            else {
                os << "-";
            }
        }
        os << std::endl;
    }
    return os;
}

int Polyomino::getSize() const {
    return positions.count();
}

bool Polyomino::contains(const Position& position) const {
    return this->positions[getBitIndex(position)];
}

void Polyomino::insert(const Position& position) {
    this->positions.set(getBitIndex(position), true);
}

void Polyomino::erase(const Position& position) {
    this->positions.set(getBitIndex(position), false);
}

std::size_t Polyomino::getBitIndex(const Position& position) const {
    return position.y * this->length + position.x;
}

Polyomino::ConstIterator Polyomino::begin() const {
    return ConstIterator(*this, positions._Find_first());
}

Polyomino::ConstIterator Polyomino::end() const {
    return ConstIterator(*this, positions.size());
}