package examples.snake;

import io.termd.core.function.BiConsumer;
import io.termd.core.function.Consumer;
import io.termd.core.tty.TtyConnection;
import io.termd.core.tty.TtyEvent;
import io.termd.core.util.Vector;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * The snake game implementation, fully non blocking, one thread to handle all players : massive scalability
 */
public class SnakeGame implements Consumer<TtyConnection> {

  @Override
  public void accept(final TtyConnection conn) {
    if (conn.size() != null) {
      new Game(conn).execute();
    } else {
      conn.setSizeHandler(new Consumer<Vector>() {
        @Override
        public void accept(Vector size) {
          new Game(conn).execute();
        }
      });
    }
  }

  enum Direction {
    LEFT, RIGHT, UP, DOWN
  }

  /**
   * The game automaton state
   */
  class GameState {

    final int width, height;
    HashSet<Vector> tiles;
    LinkedList<Vector> snake = new LinkedList<Vector>();
    Direction direction;

    GameState(int width, int height, int size) {
      this.width = width;
      this.height = height;
      tiles = new HashSet<Vector>();
      while (size > 0) {
        int x = new Random().nextInt(width);
        int y = new Random().nextInt(height);
        Vector tile = new Vector(x, y);
        if (tiles.add(tile)) {
          size--;
        }
      }
      snake.addFirst(new Vector(0, 0));
      snake.addFirst(new Vector(1, 0));
      snake.addFirst(new Vector(2, 0));
      snake.addFirst(new Vector(3, 0));
      direction = Direction.RIGHT;
    }

    /**
     * Update the state with one game iteration
     *
     * @throws Exception when user lose
     */
    void update() throws Exception {
      Vector curr = snake.peekFirst();
      Vector next = null;
      switch (direction) {
        case RIGHT:
          next = new Vector(curr.x() + 1, curr.y());
          break;
        case LEFT:
          next = new Vector(curr.x() - 1, curr.y());
          break;
        case UP:
          next = new Vector(curr.x(), curr.y() - 1);
          break;
        case DOWN:
          next = new Vector(curr.x(), curr.y() + 1);
          break;
      }
      if (next.x() < 0 || next.x() >= width || next.y() < 0 || next.y() >= height || snake.contains(next)) {
        throw new Exception("lost");
      }
      if (!tiles.remove(next)) {
        // Eat a tile : grow
        snake.removeLast();
      }
      snake.addFirst(next);
    }
  }

  /**
   * The game itself.
   */
  class Game {

    final TtyConnection conn;
    GameState game;
    boolean interrupted;

    public Game(final TtyConnection conn) {
      this.conn = conn;

      // When user resize the screen : launch a new game
      conn.setSizeHandler(new Consumer<Vector>() {
        @Override
        public void accept(Vector size) {
          reset(size);
        }
      });

      // Ctrl-C ends the game
      conn.setEventHandler(new BiConsumer<TtyEvent, Integer>() {
        @Override
        public void accept(TtyEvent event, Integer ch) {
          switch (event) {
            case INTR:
              interrupted = true;
              conn.close();
              break;
          }
        }
      });

      // Keyboard handling
      conn.setStdinHandler(new Consumer<int[]>() {
        @Override
        public void accept(int[] keys) {
          if (keys.length == 3) {
            if (keys[0] == 27 && keys[1] == '[') {
              switch (keys[2]) {
                case 'A':
                  game.direction = Direction.UP;
                  break;
                case 'B':
                  game.direction = Direction.DOWN;
                  break;
                case 'C':
                  game.direction = Direction.RIGHT;
                  break;
                case 'D':
                  game.direction = Direction.LEFT;
                  break;
              }
            }
          }
        }
      });

      // Init current game
      reset(conn.size());
    }

    /**
     * Execute one iteration of the game, at the end schedule the next iteration until user lose or hits Ctrl-C
     */
    void execute() {
      if (interrupted) {
        return;
      }
      GameState game = this.game;

      // Compute the ANSI magic string that draws the game
      StringBuilder buf = new StringBuilder();
      for (int y = 0;y < game.height;y++) {
        buf.append("\033[").append(y + 1).append(";1H\033[K");
      }
      for (Vector tile : game.tiles) {
        buf.append("\033[").append(tile.y() + 1).append(";").append(tile.x() + 1).append("H").append("X");
      }
      for (Vector tile : game.snake) {
        buf.append("\033[").append(tile.y() + 1).append(";").append(tile.x() + 1).append("H").append('0');
      }
      buf.append("\033[").append(game.height).append(";").append(game.width).append("H\033[K");

      // Update screen
      conn.write(buf.toString());

      // Now update game and handle losing the game
      try {
        game.update();
      } catch (Exception e) {
        conn.write("YOU LOST");
        conn.close();
        return;
      }

      // Schedule a new execution of the game
      conn.schedule(new Runnable() {
        @Override
        public void run() {
          execute();
        }
      }, 500, TimeUnit.MILLISECONDS);
    }

    private void reset(Vector size) {
      // Fill factory area / 25
      game = new GameState(size.x(), size.y(), (size.x() * size.y()) / 10);
    }
  }
}