/**
   POGL : programmation objet et génie logiciel
   Classes internes

   @author Thibaut Balabonski @ Université Paris-Sud
   @version 4 février 2019
*/

/**
   En plus des habituels attributs, constructeurs et méthodes, une classe peut
   contenir les définitions d'autres classes, appelées classes internes.
   Ceci permet notamment de définir des structures internes à une classe
   donnée.

   Par exemple, nous pouvons définir une liste chaînée comme étant constituée
   d'un ensemble de blocs. La classe [Block] sera naturellement une classe
   interne de la classe [List].
*/

class IntList {

    // On donne comme attribut à la liste un pointeur vers son premier bloc,
    // du type [IntBlock] défini juste après par une classe interne.
    private IntBlock firstBlock;

    // Définition de classe interne, avec exactement la syntaxe habituelle
    class IntBlock {
        // On a comme d'habitude des attributs, des constructeurs, des méthodes
	private IntBlock nextBlock;
	private final Integer elt;
	public IntBlock(int n) {
            this.elt = n;
        }
        public int getElt() {
            return this.elt;
        }
    }

    // Pour ajouter un élément au début de la liste
    public void addHead(int n) {
        // on crée d'abord un nouveau bloc [b]
        IntBlock b = new IntBlock(n);
        // puis on ajuste les pointeurs : [b] devient le nouveau premier
        // bloc, et son successeur est l'ancien premier bloc
        b.nextBlock = this.firstBlock;
        this.firstBlock = b;
    }

    // Récupérer le premier bloc
    // On restreint la visibilité au paquet, car ce genre de structure interne
    // n'a pas vocation à être montré à l'extérieur
    protected IntBlock getFirstBlock() {
        return this.firstBlock;
    }
    
}

/**
   Visibilité de la classe interne depuis la classe externe

   Une classe externe a accès à tous les champs de ses classes internes,
   y compris les champs privés.
   Ainsi dans le code précédent nous avons pu écrire
       b.nextBlock = ...
   Dans une méthode de [IntList].
*/

/**
   Visibilité de la classe interne depuis l'extérieur

   Si une classe interne est déclarée publique, ou par défaut si rien n'est
   précisé, elle est connue du monde extérieur sous un nom construit selon le
   schéma suivant :
       NomClasseExterne . NomClasseInterne
   De plus, toute classe extérieur peut alors aussi faire référence aux champs
   de la classe interne.
   Si la classe interne est déclarée privée, ceci n'est pas possible.

   On peut donc faire référence aux blocs de notre liste depuis une classe
   extérieure sous le nom de classe
       IntList.IntBlock
*/

class TestIntList {
    public static void main(String[] args) {
        IntList list = new IntList();
        list.addHead(1);
        list.addHead(2);
        list.addHead(4);
        // Vus de l'extérieurs les blocs ont pour type [IntList.IntBlock].
        // Les règles de visibilité des champs publics, protégés ou privés
        // sont les mêmes que d'habitude
        IntList.IntBlock b = list.getFirstBlock();
        // IntList.IntBlock c = b.nextBlock; // Impossible, [nextBlock] privé
        System.out.println(b.getElt());
    }
}

/**
   Création d'instances

   Une instance de la classe interne ne peut être construite que par
   l'intermédiaire d'une instance de la classe externe. Cela peut se faire
   de deux façons :
   - soit au sein d'une méthode d'instance de la classe externe (c'est-à-dire
     une méthode dans laquelle [this] existe et désigne une instance de la
     classe externe) ; c'est notamment ce qui se passe dans la ligne
         IntBlock b = new IntBlock(n);
     de la méthode [addHead],

   - soit en faisant explicitement référence à une instance [extObj] de la
     classe externe, en remplaçant [new] par
         extObj . new 

   Ainsi, chaque instance de la classe interne est liée à une instance de la
   classe externe (celle qui a servi à sa création).
*/

class TestInternalConstructor {
    public static void main(String[] args) {
        IntList list = new IntList();
        IntList.IntBlock b = list.new IntBlock(1);
    }
}

/**
   Visibilité depuis la classe interne
   
   Chaque instance de la classe interne peut accéder à l'instance de la classe
   externe à laquelle elle est liée avec la notation
       NomClasseExterne . this

   Par ce biais, la classe interne peut accéder à tous les champs de la classe
   externe, y compris les champs privés. En particulier, pour les attributs de
   classe la classe interne accèdera à la valeur partagée, et pour les attributs
   d'instance l'instance de la classe interne accèdera à la valeur de l'attribut
   pour l'instance à laquelle elle est liée.

   Cela aurait permis la variante suivante.
*/

class IntList2 {
    private IntBlock firstBlock;
    class IntBlock {
	private IntBlock nextBlock;
	private final Integer elt;
	public IntBlock(int n) {
            this.elt = n;
        }
        // Nous pouvons donc ajouter dans la classe interne une méthode
        // de la forme suivante.
        public boolean isFirstBlock() {
            return this == IntList.this.firstBlock;
        }
        // Note : de même que [this] peut être omis lors de l'accès aux champs,
        // on aurait pu dans cet exemple noter seulement
        //   return this == firstBlock;
    }
}


/**
   Classes internes de classe

   Commes les autres champs, les classes internes peuvent être d'instance 
   (par défaut) ou de classe (si qualifiées par [static]).

   Rappel des différences entre les deux :
   - un attribut d'instance est un élément de l'état de chaque instance, ses
     valeurs pour chaque instance peuvent différer et évoluent indépendamment,
     on y accède exclusivement à partir d'une instance
   - un attribut de classe possède une seule valeur, partagée par toutes les
     instances de la classe ; les modifications apportées sur une instance sont
     répercutées sur les autres instances ; on peut y accéder à partir d'une
     instance ou à partir du nom de la classe

   - une méthode d'instance est appelée à partir d'une instance, qui est l'un
     des paramètres de la méthode (qu'on appelle le paramètre implicite, et
     auquel on peut faire référence avec [this])
   - une méthode de classe peut être appelée à partir d'une instance ou
     directement avec le nom de la classe ; dans tous les cas, une méthode de
     classe n'a pas de paramètre implicite (une référence à [this] serait
     rejetée)

   - on a déjà vu qu'une d'instance d'une classe interne d'instance (la version
     par défaut présentée ci-dessus), est liée à une instance de la classe
     externe, et que cette instance de la classe externe correspondait à un
     paramètre implicite du constructeur de la classe interne

   À l'inverse, une instance d'une classe interne de classe n'est pas liée
   à une instance particulière de la classe externe, mais à la classe externe
   en général. En particulier, le constructeur d'une classe interne de classe
   ne prend pas de paramètre implicite, et les restrictions précédentes sur la
   construction ne s'appliquent donc pas.

   En conséquence, une classe interne de classe n'a accès qu'aux champs de
   classe (publics ou privés). Elle peut y accéder soit directement par leur
   nom, soit sous la forme
       NomClasseExterne . NomChamp

*/


/**
   Classes paramétrées et classes internes

   Les paramètres de type d'une classe externe sont transmis à ses classes
   internes, sans qu'il faille réutiliser la notation [<T>] (le faire serait
   interprété comme un paramètre supplémentaire, s'appliquant uniquement à
   la classe interne, et qu'on aurait par malheur désigné par le même nom).

   Une version paramétrique de l'exemple précédent serait donc comme suit.
*/

class List<T> {
    private Block firstBlock;
    // La classe interne [Block] ne prend pas de paramètre de type
    // supplémentaire, mais peut en revanche faire référence à [T].
    class Block {
	private Block nextBlock;
	private final T elt;
	public Block(T n) { this.elt = n; }
        public T getElt() { return this.elt; }
    }
    public void addHead(T n) {
        Block b = new Block(n);
        b.nextBlock = this.firstBlock;
        this.firstBlock = b;
    }
    protected Block getFirstBlock() { return this.firstBlock; }
}

class TestTList {
    public static void main(String[] args) {
        List<Integer> list = new List<>();
        list.addHead(1);
        list.addHead(2);
        list.addHead(4);
        // Vu de l'extérieur, le nom de [Block] est préfixé par [List]
        // appliqué à un paramètre de type.
        List<Integer>.Block b = list.getFirstBlock();
        // List<Integer>.Block c = b.nextBlock; // Impossible
        System.out.println(b.getElt());
    }
}