#include "Polyomino.h"

#include "Position.h"

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


Polyomino::Polyomino(const std::set<Position>& positions) {
    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
    std::set<Position> newPositions;
    for (Position position : positions) {
        newPositions.insert(Position{position.x - minX, position.y - minY});
    }
    this->positions = std::move(newPositions);
}

Polyomino::Polyomino(const std::set<Position>& positions, int length) :
    positions(positions),
    length(length) {
}

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

    std::set<Position> newPositions;
    for (const Position position : this->positions) {
        newPositions.insert(Position{position.x - minX, position.y - minY});
    }
    this->positions = std::move(newPositions);
}

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

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

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

void Polyomino::goToSpawnPosition() {
    // initialize array
    std::vector<std::vector<int>> linesCompleteness;
    linesCompleteness.reserve(4);
    std::vector<int> empty;
    for (int j = 0; j < this->length; j++) {
        empty.push_back(0);
    }
    for (int i = 0; i < 4; i++) {
        linesCompleteness.push_back(empty);
    }

    // calculates amount of squares per rows and columns
    for (const Position position : this->positions) {
        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->positions) {
        if (position.x < minX) minX = position.x;
        if (position.y < minY) minY = position.y;
    }

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

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->positions.contains(Position{i, j})) {
                if (completedLine) return false;
                else startedLine = true;
            }
            else {
                if (startedLine) completedLine = true;
            }

            if (this->positions.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
    std::set<Position> emptyPositions;
    for (int i = 0; i < this->length - 1; i++) {
        this->tryToInsertPosition(emptyPositions, Position{i, 0});                                   // up row
        this->tryToInsertPosition(emptyPositions, Position{this->length - 1, i});                    // rigth column
        this->tryToInsertPosition(emptyPositions, Position{this->length - 1 - i, this->length - 1}); // bottom row
        this->tryToInsertPosition(emptyPositions, 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 (emptyPositions.size() < (this->length * this->length) - this->positions.size());
}

void Polyomino::tryToInsertPosition(std::set<Position>& emptyPositions, const Position& candidate) const {
    if (candidate.x >= this->length || candidate.x < 0 || candidate.y >= this->length || candidate.y < 0) return;
    if (this->positions.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 std::set<Position>& Polyomino::getPositions() 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;

    for (int y = this->length - 1; y >= 0; y--) {
        for (int x = 0; x < this->length; x++) {
            bool hasThisPosition = this->positions.contains(Position{x, y});
            bool hasOtherPosition = other.positions.contains(Position{x, y});
            if (hasThisPosition != hasOtherPosition) return hasThisPosition;
        }
    }
    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.positions.contains(Position{x, y})) {
                os << "*";
            }
            else {
                os << "-";
            }
        }
        os << std::endl;
    }
    return os;
}
