package org.Models;

/**
 * Uniquement afin de se servir d'une paire de coordonées.
 */
import java.awt.Point;

import java.util.ArrayList;
import java.util.List;
import java.util.Observable;

/**
 * Représente la grille de jeu du Tetris, gère l'état des cases, la pièce courante,
 * le score, la suppression des lignes et les déplacements/rotations des pièces.
 */
@SuppressWarnings("deprecation")
public class Grille extends Observable {
    private boolean[][] grille;
    public int nbLignes;
    public int nbColonnes;

    private PieceCourante pieceCourante;
    private int pieceCouranteX;
    private int pieceCouranteY;
    private boolean enPause = false;

    private int score = 0;
    private int nbLignesSupprimees = 0;

    /**
     * Crée une nouvelle grille de jeu avec le nombre de lignes et de colonnes spécifié.
     *
     * @param nbLignes nombre de lignes de la grille
     * @param nbColonnes nombre de colonnes de la grille
     */
    public Grille(int nbLignes, int nbColonnes) {
        this.nbLignes = nbLignes;
        this.nbColonnes = nbColonnes;
        this.grille = new boolean[nbLignes][nbColonnes];
        initGrille();
    }

    /**
     * Initialise toutes les cases de la grille à vide (false).
     */
    public void initGrille() {
        for (int i = 0; i < nbLignes; i++) {
            for (int j = 0; j < nbColonnes; j++) {
                this.grille[i][j] = false;
            }
        }
    }

    /**
     * Modifie l'état d'une case de la grille.
     *
     * @param i indice de ligne
     * @param j indice de colonne
     * @param value true si la case est occupée, false sinon
     */
    public void setCase(int i, int j, boolean value) {
        if (i >= 0 && i < nbLignes && j >= 0 && j < nbColonnes) {
            this.grille[i][j] = value;
            setChanged();
            notifyObservers();
        }
    }

    /**
     * Retourne l'état d'une case de la grille.
     *
     * @param i indice de ligne
     * @param j indice de colonne
     * @return true si la case est occupée, false sinon
     */
    public boolean getCase(int i, int j) {
        if (i >= 0 && i < nbLignes && j >= 0 && j < nbColonnes) {
            return this.grille[i][j];
        }
        return false;
    }

    /**
     * Retourne la pièce courante.
     *
     * @return la pièce courante
     */
    public PieceCourante getPieceCourante() {
        return pieceCourante;
    }

    /**
     * Définit la pièce courante et la place en haut de la grille.
     *
     * @param pieceCourante la nouvelle pièce courante
     */
    public void setPieceCourante(PieceCourante pieceCourante) {
        this.pieceCourante = pieceCourante;
        this.pieceCouranteX = 3;
        this.pieceCouranteY = 0;
        setChanged();
        notifyObservers();
    }

    /**
     * Retourne la position X de la pièce courante.
     *
     * @return position X
     */
    public int getPieceCouranteX() {
        return pieceCouranteX;
    }

    /**
     * Retourne la position X de la pièce courante.
     *
     * @return position X
     */
    public int getPieceCouranteY() {
        return pieceCouranteY;
    }

    /**
     * Déplace la pièce courante dans la direction spécifiée si possible.
     *
     * @param direction direction du déplacement
     */
    public void deplacerPiece(Direction direction) {

        switch (direction) {
            case BAS:
                if (peuxBouger(direction, this.motifPieceCouranteColoriee())) {
                    deplacerPieceBas();
                }
                break;
            case GAUCHE:
                if (peuxBouger(direction, this.motifPieceCouranteColoriee())) {
                    deplacerPieceGauche();
                }
                break;
            case DROITE:
                if (peuxBouger(direction, this.motifPieceCouranteColoriee())) {
                    deplacerPieceDroite();
                }
                break;
            case TOUTENBAS:
                if (peuxBouger(direction, this.motifPieceCouranteColoriee())) {
                    deplacerPieceToutEnBas();
                }
            default:
                break;
        }
    }

    /**
     * Déplace la pièce courante d'une case vers le bas.
     */
    private void deplacerPieceBas() {
        pieceCouranteY++;

        setChanged();
        notifyObservers();
    }

    /**
     * Déplace la pièce courante d'une case vers la gauche.
     */
    private void deplacerPieceGauche() {
        pieceCouranteX--;

        setChanged();
        notifyObservers();
    }

    /**
     * Déplace la pièce courante d'une case vers la droite.
     */
    private void deplacerPieceDroite() {
        pieceCouranteX++;

        setChanged();
        notifyObservers();
    }

    /**
     * Fait descendre la pièce courante jusqu'à ce qu'elle ne puisse plus descendre.
     */
    private void deplacerPieceToutEnBas() {
        while (peuxBouger(Direction.BAS, this.motifPieceCouranteColoriee())) {
            deplacerPieceBas();
        }
    }

    /**
     * Retourne le nombre de lignes de la grille.
     *
     * @return nombre de lignes
     */
    public int getNbLignes() {
        return nbLignes;
    }

    /**
     * Retourne le nombre de colonnes de la grille.
     *
     * @return nombre de colonnes
     */
    public int getNbColonnes() {
        return nbColonnes;
    }

    /**
     * Retourne la liste des points occupés par la pièce courante dans la grille.
     *
     * @return liste des points colorés
     */
    public List<Point> motifPieceCouranteColoriee() {
        List<Point> casesColores = new ArrayList<>();

        boolean[][] motif = pieceCourante.getMotif();
        for (int i = 0; i < motif.length; i++) {
            for (int j = 0; j < motif[i].length; j++) {
                if (motif[i][j]) {
                    casesColores.add(new Point(pieceCouranteX + j, pieceCouranteY + i));
                }
            }
        }

        return casesColores;
    }

    /**
     * Vérifie si la pièce peut bouger dans la direction donnée.
     *
     * @param direction direction du déplacement
     * @param motif motif de la pièce
     * @return true si le déplacement est possible, false sinon
     */
    public boolean peuxBouger(Direction direction, List<Point> motif) {
        int deltaX = 0;
        int deltaY = 0;

        switch (direction) {
            case GAUCHE:
                deltaX = -1;
                break;
            case DROITE:
                deltaX = 1;
                break;
            case BAS:
                deltaY = 1;
                break;
            case TOUTENBAS:
                break;
            default:
                break;
        }

        List<Point> nouvellePosition = calculerNouvellePosition(motif, deltaX, deltaY);
        return positionValide(nouvellePosition);
    }

    /**
     * Retourne la matrice représentant la grille.
     *
     * @return matrice de booleans
     */
    public boolean[][] getGrille() {
        return grille;
    }


    /**
     * Fixe la pièce courante sur la grille et vérifie la suppression de lignes.
     */
    public void fixerPiece() {
        for (Point caseColoree : motifPieceCouranteColoriee()) {
            setCase(caseColoree.y, caseColoree.x, true);
        }

        
        verifierEtSupprimerLignesSiBesoin();
    }

    /**
     * Vérifie si la pièce courante doit être fixée (ne peut plus descendre).
     *
     * @return true si la pièce doit être fixée, false sinon
     */
    public boolean doitFixerPiece() {
        if (!peuxBouger(Direction.BAS, this.motifPieceCouranteColoriee())) {
            return true;
        }

        return false;
    }

    /**
     * Met la grille en pause ou la reprend.
     *
     * @param enPause true pour mettre en pause, false pour reprendre
     */
    public void setEnPause(boolean enPause) {
        this.enPause = enPause;
    }

    /**
     * Indique si la grille est en pause.
     *
     * @return true si en pause, false sinon
     */
    public boolean estEnPause() {
        return enPause;
    }

    /**
     * Vérifie et supprime les lignes complètes si besoin, met à jour le score.
     */
    public void verifierEtSupprimerLignesSiBesoin() {
        int tmpNbLignesSupprimees = 0;
        System.out.println("Debut uppression d'une ligne......");
        for (int i = nbLignes - 1; i >= 0; i--) {
            boolean ligneSupprimable = true;

            for (int j = 0; j < nbColonnes; j++) {
                if (!this.grille[i][j]) {
                    ligneSupprimable = false;
                    break;
                }
            }

            if (ligneSupprimable) {
                supprimerLigne(i);
                nbLignesSupprimees += 1;
                i++;
                tmpNbLignesSupprimees++;
            }
        }
        System.out.println(tmpNbLignesSupprimees + " Lignes supprimées");
        switch (tmpNbLignesSupprimees) {
            case 1:
                score += 100;
                break;
            case 2:
                score += 300;
                break;
            case 3:
                score += 500;
                break;
            case 4:
                score += 800;
                break;
            default:
                break;
        }
    }

    /**
     * Retourne le score actuel.
     *
     * @return score
     */
    public int getScore() {
        return score;
    }

    /**
     * Définit le score.
     *
     * @param score nouveau score
     */
    public void setScore(int score) {
        this.score = score;
    }

    /**
     * Retourne le nombre total de lignes supprimées.
     *
     * @return nombre de lignes supprimées
     */
    public int getNbLignesSupprimees() {
        return nbLignesSupprimees;
    }

    /**
     * Définit le nombre de lignes supprimées.
     *
     * @param nbLignesSupprimees nouveau nombre de lignes supprimées
     */
    public void setNbLignesSupprimees(int nbLignesSupprimees) {
        this.nbLignesSupprimees = nbLignesSupprimees;
    }

    /**
     * Supprime la ligne d'indice i et fait descendre les lignes au-dessus.
     *
     * @param i indice de la ligne à supprimer
     */
    public void supprimerLigne(int i) {
        for (int ligne = i; ligne > 0; ligne--) {
            for (int j = 0; j < nbColonnes; j++) {
                this.setCase(ligne, j, this.getCase(ligne - 1, j));
            }
        }

        for (int j = 0; j < nbColonnes; j++) {
            this.setCase(0, j, false);
        }

        setChanged();
        notifyObservers();
    }

    /**
     * Retourne la liste des points d'une ligne donnée.
     *
     * @param i indice de la ligne
     * @return liste des points de la ligne
     */
    public List<Point> getLignePoints(int i) {
        List<Point> coordonnesCase = new ArrayList<>();

        for (int j = 0; j < nbColonnes; j++) {
            coordonnesCase.add(new Point(j, i));
        }

        return coordonnesCase;
    }

    /**
     * Fait descendre une ligne d'une position.
     *
     * @param i indice de la ligne à descendre
     */
    public void descendreLigne(int i) {
        for (int j = 0; j < nbColonnes; j++) {
            List<Point> coordonnesCase = new ArrayList<>();
            coordonnesCase.add(new Point(j, i));

            if (this.grille[i][j]) {
                this.setCase(i + 1, j, true);
            }

            this.setCase(i, j, false);
        }
    }

    /**
     * Tente de tourner la pièce courante dans l'orientation donnée.
     *
     * @param orientation sens de rotation
     */
    public void tournerPiece(Orientation orientation) {
        if (peuxTournerPiece(orientation)) {
            boolean[][] motif = pieceCourante.getMotif();
            boolean[][] nouveauMotif;

            if (orientation == Orientation.SENSHORAIRE) {
                nouveauMotif = PieceCourante.rotationHoraire(motif);
            } else {
                nouveauMotif = PieceCourante.rotationAntiHoraire(motif);
            }

            pieceCourante.setMotif(nouveauMotif);

            setChanged();
            notifyObservers();
        }
    }

    /**
     * Vérifie si la pièce courante peut tourner dans l'orientation donnée.
     *
     * @param orientation sens de rotation
     * @return true si la rotation est possible, false sinon
     */
    public boolean peuxTournerPiece(Orientation orientation) {
        List<Point> positionApresRotation = calculerPositionApresRotation(orientation);
        return positionValide(positionApresRotation);
    }

    /**
     * Calcule la position de la pièce après rotation.
     *
     * @param orientation sens de rotation
     * @return liste des points après rotation
     */
    private List<Point> calculerPositionApresRotation(Orientation orientation) {
        List<Point> nouvellePosition = new ArrayList<>();
        boolean[][] motifActuel = pieceCourante.getMotif();
        boolean[][] motifTourne;

        boolean[][] motifCopie = new boolean[motifActuel.length][motifActuel[0].length];
        for (int i = 0; i < motifActuel.length; i++) {
            for (int j = 0; j < motifActuel[i].length; j++) {
                motifCopie[i][j] = motifActuel[i][j];
            }
        }

        if (orientation == Orientation.SENSHORAIRE) {
            motifTourne = PieceCourante.rotationHoraire(motifCopie);
        } else {
            motifTourne = PieceCourante.rotationAntiHoraire(motifCopie);
        }

        for (int i = 0; i < motifTourne.length; i++) {
            for (int j = 0; j < motifTourne[i].length; j++) {
                if (motifTourne[i][j]) {
                    nouvellePosition.add(new Point(pieceCouranteX + j, pieceCouranteY + i));
                }
            }
        }

        return nouvellePosition;
    }

    /**
     * Vérifie si la position donnée est valide (dans la grille et non occupée).
     *
     * @param points liste des points à vérifier
     * @return true si la position est valide, false sinon
     */
    public boolean positionValide(List<Point> points) {
        for (Point point : points) {
            if (point.x < 0 || point.x >= nbColonnes || point.y < 0 || point.y >= nbLignes) {
                return false;
            }

            if (grille[point.y][point.x]) {
                return false;
            }
        }
        return true;
    }


    /**
     * Calcule la nouvelle position d'une liste de points après déplacement.
     *
     * @param points liste de points d'origine
     * @param deltaX déplacement en X
     * @param deltaY déplacement en Y
     * @return nouvelle liste de points déplacés
     */
    private List<Point> calculerNouvellePosition(List<Point> points, int deltaX, int deltaY) {
        List<Point> nouvellePosPoints = new ArrayList<>();

        for (Point point : points) {
            nouvellePosPoints.add(new Point(point.x + deltaX, point.y + deltaY));
        }

        return nouvellePosPoints;
    }

    /**
     * Réinitialise la grille, le score, le nombre de lignes supprimées et la pièce courante.
     */
    public void reinitialiserGrille() {
        initGrille();
        score = 0;
        nbLignesSupprimees = 0;
        pieceCourante = null;
        pieceCouranteX = 3;
        pieceCouranteY = 0;
        setChanged();
        notifyObservers();
    }

}