import javax.swing.*;
import java.awt.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Gère une classe du projet.
 * @author V.BOULANGER
 */
public class Class implements Serializable {

    public static int _classId = 0;
    public static final int RECTANGLE = 0;
    public static final int CIRCLE = 1;

    private int _id;
    private String _name;
    private int _posX;
    private int _posY;
    private int _sizeX;
    private int _sizeY;
    private int _minSizeX;
    private int _minSizeY;
    private int _shape;

    private List<Attribute> _attributes;
    private List<Method> _methods;

    /**
     * Constructeur - Crée une instance de Class.
     * @param posX La position sur l'axe X de la nouvelle classe.
     * @param posY La position sur l'axe Y de la nouvelle classe.
     * @param shape La forme de la nouvelle classe (Class.RECTANGLE ou Class.CIRCLE)
     */
    public Class(int posX, int posY, int shape){
        _classId++;
        this._id = _classId;
        this._name = "Class" + _id;
        this._posX = posX;
        this._posY = posY;
        this._sizeX = 90;
        this._sizeY = 50;
        this._shape = shape;
        this._attributes = new ArrayList<Attribute>();
        this._methods = new ArrayList<Method>();
        this.computeMinSize();
    }

    /**
     * Constructeur - Crée une instance de Class avec les paramètres déterminés.
     * @param name Le nom de la classe importée.
     * @param posX La position sur l'axe X de la classe importée.
     * @param posY La position sur l'axe Y de la classe importée.
     * @param sizeX La taille sur l'axe X de la classe importée.
     * @param sizeY La taille sur l'axe Y de la classe importée.
     * @param shape La forme de la classe importée (Class.RECTANGLE ou Class.CIRCLE).
     * @param attributes La liste des attributs de la classe importée.
     * @param methods La liste des méthodes de la classe importée.
     */
    public Class(String name, int posX, int posY, int sizeX, int sizeY, int shape, ArrayList<Attribute> attributes, ArrayList<Method> methods){
        _classId++;
        this._id = _classId;
        this._name = name;
        this._posX = posX;
        this._posY = posY;
        this._sizeX = sizeX;
        this._sizeY = sizeY;
        this._shape = shape;
        this._attributes = attributes;
        this._methods = methods;
        this.computeMinSize();
    }

    /**
     * Récupère le numéro de la classe.
     * @return Le numéro de la classe.
     */
    public int getId() {
        return _id;
    }

    /**
     * Récupère le nom de la classe.
     * @return Le nom de la classe.
     */
    public String getName() {
        return _name;
    }

    /**
     * Paramètre le nom de la classe.
     * @param name Le nom de la classe.
     */
    public void setName(String name) {
        this._name = name;
        this.computeMinSize();
    }

    /**
     * Récupère la position sur l'axe X de la classe.
     * @return La position sur l'axe X de la classe.
     */
    public int getPosX() {
        return _posX;
    }

    /**
     * Paramètre la position sur l'axe X de la classe.
     * @param posX La position sur l'axe X de la classe.
     */
    public void setPosX(int posX) {
        this._posX = posX;
    }

    /**
     * Récupère la position sur l'axe Y de la classe.
     * @return La position sur l'axe Y de la classe.
     */
    public int getPosY() {
        return _posY;
    }

    /**
     * Paramètre la position sur l'axe Y de la classe.
     * @param posY La position sur l'axe Y de la classe.
     */
    public void setPosY(int posY) {
        this._posY = posY;
    }

    /**
     * Récupère la taille sur l'axe X de la classe.
     * @return La taille sur l'axe X de la classe.
     */
    public int getSizeX() {
        return _sizeX;
    }

    /**
     * Paramètre la taille sur l'axe X de la classe.
     * @param sizeX La taille sur l'axe X de la classe.
     */
    public void setSizeX(int sizeX) {
        if(sizeX < this.getMinSizeX()) this._sizeX = this.getMinSizeX();
        else this._sizeX = sizeX;
    }

    /**
     * Récupère la taille sur l'axe Y de la classe.
     * @return La taille sur l'axe Y de la classe.
     */
    public int getSizeY() {
        return _sizeY;
    }

    /**
     * Paramètre la taille sur l'axe Y de la classe.
     * @param sizeY La taille sur l'axe Y de la classe.
     */
    public void setSizeY(int sizeY) {
        if(sizeY < this.getMinSizeY()) this._sizeY = this.getMinSizeY();
        else this._sizeY = sizeY;
    }

    /**
     * Récupère la taille minimale sur l'axe X de la classe.
     * @return La taille minimale sur l'axe X de la classe.
     */
    public int getMinSizeX(){
        return this._minSizeX;
    }

    /**
     * Paramètre la taille minimale sur l'axe X de la classe.
     * @param minSizeX La taille minimale sur l'axe X de la classe.
     */
    public void setMinSizeX(int minSizeX){
        this._minSizeX = minSizeX;
    }

    /**
     * Récupère la taille minimale sur l'axe Y de la classe.
     * @return La taille minimale sur l'axe Y de la classe.
     */
    public int getMinSizeY(){
        return this._minSizeY;
    }

    /**
     * Paramètre la taille minimale sur l'axe Y de la classe.
     * @param minSizeY La taille minimale sur l'axe Y de la classe.
     */
    public void setMinSizeY(int minSizeY){
        this._minSizeY = minSizeY;
    }

    /**
     * Paramètre les tailles minimum de la classe sur les axes X et Y en fonction de son contenu
     */
    public void computeMinSize(){
        Font font1 = new Font("Arial", Font.PLAIN, 14);
        Font font2 = new Font("Arial", Font.PLAIN, 10);
        Canvas c = new Canvas();
        FontMetrics fm1 = c.getFontMetrics(font1);
        FontMetrics fm2 = c.getFontMetrics(font2);

        int space = 5;
        //Calcul de la taille en X
        int maxWidth = fm1.stringWidth(this.getName());
        //Parcours des attributs
        for(Attribute a : this.getAttributes()){
            String chain = a.getAccess() + " " + a.getName() + " : " + a.getType();
            int temp = fm2.stringWidth(chain);
            if(temp > maxWidth) maxWidth = temp;
        }
        //Parcours des méthodes
        for(Method m : this.getMethods()){
            String chain = m.getAccess() + " " + m.getName() + "(";
            ArrayList<String> listArguments = new ArrayList<String>();
            for(Argument ar : m.getArguments()){
                listArguments.add(ar.getName() + " : " + ar.getType());
            }
            chain += String.join(", ", listArguments) + ") : " + m.getType();
            int temp = fm2.stringWidth(chain);
            if(temp > maxWidth) maxWidth = temp;
        }
        int temp = fm2.stringWidth("attributes");
        if(temp > maxWidth) maxWidth = temp;
        this.setMinSizeX(maxWidth);
        //Calcul de la taille en Y
        int attributes = this.getAttributes().size();
        int methods = this.getMethods().size();
        this.setMinSizeY((attributes + methods + 5) * font2.getSize());
        //Réadaptation éventuelle de la taille de la classe
        if(this.getSizeX() < this.getMinSizeX()) this.setSizeX(this.getMinSizeX());
        if(this.getSizeY() < this.getMinSizeY()) this.setSizeY(this.getMinSizeY());
    }

    /**
     * Récupère la forme graphique de la classe.
     * @return La forme graphique de la classe (Class.RECTANGLE ou Class.CIRCLE).
     */
    public int getShape() {
        return _shape;
    }

    /**
     * Paramètre la forme graphique de la classe.
     * @param shape La forme graphique de la classe (Class.RECTANGLE ou Class.CIRCLE).
     */
    public void setShape(int shape) {
        this._shape = shape;
    }

    /**
     * Récupère tous les attributs de la classe.
     * @return Les attributs de la classe.
     */
    public List<Attribute> getAttributes(){
        return this._attributes;
    }

    /**
     * Ajoute un attribut à la classe.
     * @param a L'attribut à ajouter.
     */
    public void addAttribute(Attribute a){
        this._attributes.add(a);
        this.computeMinSize();
    }

    /**
     * Supprime un attribut de la classe.
     * @param index L'index de l'attribut à supprimer.
     */
    public void removeAttribute(int index){
        this._attributes.remove(index);
        this.computeMinSize();
    }

    /**
     * Supprime tous les attributs de la classe.
     */
    public void clearAttributes(){
        this._attributes.clear();
        this.computeMinSize();
    }

    /**
     * Faire remonter l'attribut sélectionné dans la liste.
     * @param index L'index de l'attribut concerné.
     * @return true if succeed, false if failed
     */
    public boolean upAttribute(int index){
        int attrNumber = this._attributes.size();
        //Si au moins 2 attributs et que l'attribut n'est pas en 1ère position, on le remonte
        if(index != 0 && attrNumber > 1){
            Collections.swap(_attributes, index, index - 1);
            return true;
        }
        return false;
    }

    /**
     * Faire descendre l'attribut sélectionné dans la liste.
     * @param index L'index de l'attribut sélectionné.
     * @return true : la méthode a été déplacée, false sinon.
     */
    public boolean downAttribute(int index){
        int attrNumber = this._attributes.size();
        //Si au moins 2 attributs et que l'attribut n'est pas en dernière position, on le descend
        if(index != attrNumber - 1 && attrNumber > 1){
            Collections.swap(_attributes, index, index + 1);
            return true;
        }
        return false;
    }

    /**
     * Récupère toutes les méthodes de la classe.
     * @return Les méthodes de la classe.
     */
    public List<Method> getMethods(){
        return this._methods;
    }

    /**
     * Ajoute une méthode à la classe.
     * @param m La méthode à ajouter.
     */
    public void addMethod(Method m){
        this._methods.add(m);
        this.computeMinSize();
    }

    /**
     * Ajoute une méthode à la classe.
     * @param m La méthode à ajouter.
     * @param index Le rang d'insertion.
     */
    public void addMethod(int index, Method m){
        this._methods.add(index, m);
        this.computeMinSize();
    }

    /**
     * Supprime une méthode de la classe.
     * @param index L'index de la méthode à supprimer.
     */
    public void removeMethod(int index){
        this._methods.remove(index);
        this.computeMinSize();
    }

    /**
     * Supprime toutes les méthodes de la classe.
     */
    public void clearMethods(){
        this._methods.clear();
        this.computeMinSize();
    }

    /**
     * Faire remonter la méthode sélectionnée dans la liste.
     * @param index L'index de la méthode concernée.
     * @return true if succeed, false if failed
     */
    public boolean upMethod(int index){
        int methNumber = this._methods.size();
        //Si au moins 2 méthodes et que la méthode n'est pas en 1ère position, on la remonte
        if(index != 0 && methNumber > 1){
            Collections.swap(_methods, index, index - 1);
            return true;
        }
        return false;
    }

    /**
     * Faire descendre la méthode sélectionnée dans la liste.
     * @param index L'index de la méthode concernée.
     * @return true if succeed, false if failed
     */
    public boolean downMethod(int index){
        int methNumber = this._methods.size();
        //Si au moins 2 méthodes et que la méthode n'est pas en dernière position, on la descend
        if(index != methNumber - 1 && methNumber > 1){
            Collections.swap(_methods, index, index + 1);
            return true;
        }
        return false;
    }

    /**
     * Permet de redimensionner la classe vers le haut.
     * @param posY La position en ordonnée du curseur.
     */
    public void resizeUp(int posY){
        int shiftY = this.getPosY() - this.getSizeY()/2 - posY;
        this.setSizeY(this.getSizeY() + shiftY);
        if(this.getSizeY() > this.getMinSizeY()) this.setPosY(posY + this.getSizeY()/2);
    }

    /**
     * Permet de redimensionner la classe vers le bas.
     * @param posY La position en ordonnée du curseur.
     */
    public void resizeDown(int posY){
        int shiftY = posY - this.getPosY() - this.getSizeY()/2;
        this.setSizeY(this.getSizeY() + shiftY);
        if(this.getSizeY() > this.getMinSizeY()) this.setPosY(posY - this.getSizeY()/2);
    }

    /**
     * Permet de redimensionner la classe vers la gauche.
     * @param posX La position en abscisse du curseur.
     */
    public void resizeLeft(int posX){
        int shiftX = this.getPosX() - this.getSizeX()/2 - posX;
        this.setSizeX(this.getSizeX() + shiftX);
        if(this.getSizeX() > this.getMinSizeX()) this.setPosX(posX + this.getSizeX()/2);
    }

    /**
     * Permet de redimensionner la classe vers la droite.
     * @param posX La position en abscisse du curseur.
     */
    public void resizeRight(int posX){
        int shiftX = posX - this.getPosX() - this.getSizeX()/2;
        this.setSizeX(this.getSizeX() + shiftX);
        if(this.getSizeX() > this.getMinSizeX()) this.setPosX(posX - this.getSizeX()/2);
    }
}
