(** Cette fonction nous sera utile *)
let rec replicate (n : int) (x : 'a) : 'a list =
  if n = 0 then []
  else x :: (replicate (n-1) x)

(** Nous allons implémenter ici le jeu 2048 (https://play2048.co/), avec une
    interface utilisateur dans le terminal.

    Structures de données
    ---------------------

    Dans le jeu 2048, on déplace des tuiles annotées avec des nombres. On va
    simplement représenter chaque tuiles par un nombre. Les tuiles sont soit 0
    pour une tuile vide, soit une puissance de deux. *)

module Tile = struct
  type t = int

  let to_string (tile : t) : string =
    string_of_int tile
end

module Direction = struct
  (** Déplacement
      -----------

    On peut déplacer les tuiles dans une des quatre directions orthogonales.
    L'idéal pour représenter ces directions et d'introduire un type énuméré. *)

  type t =
    | Left
    | Right
    | Up
    | Down
end

module Grid = struct

  (** Ces tuiles sont placées sur une grille à deux dimensions, qu'on représente
      par une liste de liste de tuiles. *)
  type t = Tile.t list list

  (** La grille initiale est vide. Notre grille sera de 4 tuiles de hauteur et 4
      tuiles de largeur. On rajoutera des tuiles aléatoirement dessus au fur et
      à mesure de l'avancée du jeu. La fonction `replicate` va nous permettre
      d'initialiser une liste d'une certaine taille. *)

  (** La grille initiale est donc remplie de 0 : c'est une liste de 4 liste de 4 0. *)

  let empty : t =
    replicate 4 (replicate 4 0)

  (** On va vouloir afficher les grilles. Pour ce faire, on va définir une
      fonction qui prends une grille en argument et nous retourne la chaîne de
      caractère à afficher à l'écran. Pour afficher une grille, on l'affiche
      ligne par ligne. Pour chaque ligne, on affiche une tuile sur 5 caractères
      : 4 réservés pour le nombre (de 2 à 2048), et une espace. On utilise
      `Printf.sprintf` pour cela, car `Printf.sprintf "%4d" 16` crée la chaîne
      de caractère `" 16"`, c'est-à-dire qu'elle place des espaces avant le
      nombres à afficher pour toujours créer une chaîne de quatre caractères. La
      fonction `String.concat` quand à elle concatène une liste de chaîne en y
      ajoutant la chaîne voulue entre chaque paire d'éléments. *)

  let to_string (grid : t) : string =
    String.concat "\n"
      (List.map (fun line -> String.concat " " (List.map Tile.to_string line)) grid)

  (** Exercice: une grille est *valide* si elle a exactement une taille de 4x4 et
      qu'elle contient des tuiles qui sont des puissances de 2. Définir les deux fonctions
      suivantes :

   (* Renvoie True si la tuile est bien une puissance de 2 *)
   val is_valid_tile : tile -> bool

   (* Renvoie True si la grille est une grille valide, c'est-à-dire
      aux bonnes dimensions et ne contenant que des tuiles valides *)
   val is_valid_grid : grid -> bool
   *)

  (** Notre but est maintenant de définir une fonction `move` qui déplace les
      éléments d'une grille dans une direction donnée. La fonction aura le type
      suivant :

      val move : direction -> grid -> grid

      Pour cela, la fonction `move_line` va nous aider. Elle prends une ligne de
      tuiles, et déplace les tuiles vers la gauche.  Les tuiles de même valeurs
      sont fusionnés ensembles, et leur valeur sont ajoutées.  Ainsi, la ligne
      `[2, 2, 8, 8]`, déplacée vers la gauche, devient `[4, 16, 0, 0]` : les deux
      2 sont fusionnés en un 4, et les deux 8 sont fusionnés en un 16. *)

  let rec move_line (line : Tile.t list) : Tile.t list =
    match line with
    | 0 :: xs -> (move_line xs) @ [0]
    | x :: y :: xs when x = y -> ((x+y) :: (move_line xs)) @ [0]
    | x :: xs -> x :: move_line xs
    | [] -> []

  (** On aura aussi besoin de transposer des grilles, qui transforme une grille
      en changeant les lignes en colonnes, et inversement (c'est une transposition
      de matrices classique). *)

  let rec transpose (grid : t) : t = match grid with
     | []
     | [] :: _ -> []
     | rows    ->
         List.map List.hd rows :: transpose (List.map List.tl rows)

  (** Par exemple :

      1 4                1 2 3
      2 5  > transpose > 4 5 6
      3 6

      Pour avoir une définition simple de `move`, on va en fait se baser uniquement
      sur `move_line` pour tout exprimer sous forme de déplacement vers la gauche.
      Pour se déplacer vers la droite par exemple, on va inverser chaque ligne de
      notre grille, utiliser `move_line` pour faire un déplacement vers la gauche, et
      réinverser le résultat, ce qui a pour résultat final d'avoir fait un déplacement
      vers la droite.
      Pour faire un déplacement vers le haut, il faut faire une rotation avant tout.
      Idem pour un déplacement vers le bas.


      Pour faire une rotation vers la droite, on a presque ce que l'on veut avec
      `transpose`, mais les lignes sont dans le mauvais sens: on voudrait la matrice
      suivante :

      1 4                   3 2 1
      2 5  > rotate_right > 6 5 4
      3 6

      On peut donc juste inverser chaque ligne : **)

  let rotate_right (grid : t) : t =
    List.map List.rev (transpose grid)


  (** La rotation vers la gauche suit la même idée, mais plutôt que d'inverser les
     lignes, on souhaite inverser les colonnes.

     1 4                  4 5 6
     2 5  > rotate_left > 1 2 3
     3 6
   *)

  let rotate_left (grid : t) : t =
    List.rev (transpose grid)

  (** On peut maintenant définir `move` en se basant sur `move_line`, `List.rev`, et
      `rotate_left`/`rotate_right`. *)

  let rec move (dir : Direction.t) (grid : t) : t = match dir with
    | Left -> List.map move_line grid
    | Right -> List.map (fun x -> List.rev (move_line (List.rev x))) grid
    | Up -> rotate_right (move Left (rotate_left grid))
    | Down -> rotate_left (move Left (rotate_right grid))

  (** Ajouter des tuiles
      ------------------

      On a tout ce qu'il faut pour effectuer des déplacements, mais sans ajouter de
      tuiles à notre grille, celle-ci n'aura jamais aucune tuile.  On définit la
      fonction `add_two` qui ajoute une tuile 2 le plus à gauche possible d'une ligne,
      à la place d'un 0. *)

  let rec add_two (line : Tile.t list) : Tile.t list = match line with
    | [] -> []
    | 0 :: xs -> 2 :: xs
    | x :: xs -> x :: add_two xs

  (** Pour appliquer cela à une grille, on va définir une fonction plus générique qui applique une
      transformation à une grille.

      Une transformation est une liste de fonction, par exemple `[f1, f2, f3, f4]`.
      Appliquer une transformation à une grille composée des lignes `[l1, l2, l3, l4]` reviendra à
      calculer `[f1 l1, f2 l2, f3 l3, f4 l4]`.

      La fonction `List.combine` nous est très utile pour cela : elle prend deux
      listes, et combine les éléments sous forme de paires. Par exemple, `List.combine
      [1; 2; 3] [4; 5; 6] = [(1, 4); (2, 5); (3, 6)]`. *)


  let apply_transform (transform : (Tile.t list -> Tile.t list) list) (grid : t) : t =
    List.map (fun (f, x) -> f x) (List.combine transform grid)

  (** Finalement, on peut définir une fonction qui ajoute 2 dans une ligne à partir de
      l'indice de cette ligne.  Cette fonction appelle `apply_transform` avec une
      transformation qui garde les lignes identiques, sauf pour la ligne où on
      souhaite rajouter un 2.  Pour rappel, la fonction `id` est la fonction qui
      retourne son argument. *)

  let add_two_to_line (line_idx : int) (grid : t) : t =
    let id = fun x -> x in
    apply_transform ((replicate line_idx id) @ [add_two] @ (replicate (4-line_idx-1) id)) grid

end


(** Interface
    ---------

    Le reste du code sert à afficher notre grille à tout moment, et à lire les
    événements claviers afin de déplacer les tuiles de la façon demandée. *)

let rec loop (state : Grid.t) =
  Printf.printf "%s\n> %!" (Grid.to_string state);
  let direction = match read_line () with
  | "\027[A" -> Some Direction.Up
  | "\027[B" -> Some Direction.Down
  | "\027[D" -> Some Direction.Left
  | "\027[C" -> Some Direction.Right
  | _ -> None
  in
  match direction with
  | Some dir ->
    let state_after_move = Grid.move dir state in
    let random_idx = Random.int 3 in
    let state_after_adding_two = Grid.add_two_to_line random_idx state_after_move in
    loop state_after_adding_two
  | None -> loop state

let _ = loop Grid.empty