import java.io.*;
import java.net.*;
import java.util.Date;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.stage.Stage;

public class TicTacToeServer extends Application 
    implements TicTacToeConstants {
  private int sessionNo = 1; // Numer sesji
  
  @Override // Przesłanianie metody start w klasie Application
  public void start(Stage primaryStage) {
    TextArea taLog = new TextArea();

    // Tworzenie sceny i umieszczanie jej w oknie
    Scene scene = new Scene(new ScrollPane(taLog), 450, 200);
    primaryStage.setTitle("TicTacToeServer"); // Ustawianie nagłówka okna
    primaryStage.setScene(scene); // Umieszczanie sceny w oknie
    primaryStage.show(); // Wyświetlanie okna

    new Thread( () -> {
      try {
        // Tworzenie gniazda serwera
        ServerSocket serverSocket = new ServerSocket(8000);
        Platform.runLater(() -> taLog.appendText(new Date() +
          ": Uruchomienie serwera w porcie 8000\n"));
  
        // Serwer jest gotowy do tworzenia sesji dla każdej pary graczy
        while (true) {
          Platform.runLater(() -> taLog.appendText(new Date() +
            ": Oczekiwanie na dołączenie graczy do sesji nr " + sessionNo + '\n'));
  
          // Łączenie się z graczem 1
          Socket player1 = serverSocket.accept();
  
          Platform.runLater(() -> {
            taLog.appendText(new Date() + ": Gracz 1 dołączył do sesji nr "
              + sessionNo + '\n');
            taLog.appendText("Adres IP gracza 1 " +
              player1.getInetAddress().getHostAddress() + '\n');
          });
  
          // Powiadamianie, że dana osoba jest graczem 1
          new DataOutputStream(
            player1.getOutputStream()).writeInt(PLAYER1);
  
          // Łączenie się z graczem 2
          Socket player2 = serverSocket.accept();
  
          Platform.runLater(() -> {
            taLog.appendText(new Date() +
              ": Gracz 2 dołączył do sesji nr " + sessionNo + '\n');
            taLog.appendText("Adres IP gracza 2 " +
              player2.getInetAddress().getHostAddress() + '\n');
          });
  
          // Powiadamianie użytkownika, że jest graczem 2
          new DataOutputStream(
            player2.getOutputStream()).writeInt(PLAYER2);
  
          // Wyświetlanie danej sesji i zwiększanie jej numeru
          Platform.runLater(() -> 
            taLog.appendText(new Date() + 
              ": Uruchamianie wątku dla sesji nr " + sessionNo++ + '\n'));
  
          // Uruchamianie nowego wątku dla sesji dla dwóch graczy 
          new Thread(new HandleASession(player1, player2)).start();
        }
      }
      catch(IOException ex) {
        ex.printStackTrace();
      }
    }).start();
  }

  // Definicja klasy wątku do obsługi nowej sesji dla dwóch graczy
  class HandleASession implements Runnable, TicTacToeConstants {
    private Socket player1;
    private Socket player2;
  
    // Tworzenie i inicjowanie komórek
    private char[][] cell =  new char[3][3];
  
    private DataInputStream fromPlayer1;
    private DataOutputStream toPlayer1;
    private DataInputStream fromPlayer2;
    private DataOutputStream toPlayer2;
  
    // Kontynuowanie rozgrywk
    private boolean continueToPlay = true;
  
    /** Tworzenie wątku */
    public HandleASession(Socket player1, Socket player2) {
      this.player1 = player1;
      this.player2 = player2;
  
      // Inicjowanie komórek
      for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
          cell[i][j] = ' ';
    }
  
    /** Implementacja metody run() dla wątku */
    public void run() {
      try {
        // Tworzenie wejściowego i wyjściowego strumienia danych
        DataInputStream fromPlayer1 = new DataInputStream(
          player1.getInputStream());
        DataOutputStream toPlayer1 = new DataOutputStream(
          player1.getOutputStream());
        DataInputStream fromPlayer2 = new DataInputStream(
          player2.getInputStream());
        DataOutputStream toPlayer2 = new DataOutputStream(
          player2.getOutputStream());
  
        // Można przesłać dowolne dane, aby powiadomić gracza 1, by rozpoczął grę.
        // Ta instrukcja ma jedynie informować gracza 1 o początku gry
        toPlayer1.writeInt(1);
  
        // Stała obsługa graczy oraz ustalanie i przesyłanie
        // stanu gry
        while (true) {
          // Przyjmowanie ruchu od gracza 1
          int row = fromPlayer1.readInt();
          int column = fromPlayer1.readInt();
          cell[row][column] = 'X';
  
          // Sprawdzanie, czy gracz 1 wygrał
          if (isWon('X')) {
            toPlayer1.writeInt(PLAYER1_WON);
            toPlayer2.writeInt(PLAYER1_WON);
            sendMove(toPlayer2, row, column);
            break; // Wyjście z pętli
          }
          else if (isFull()) { // Sprawdzanie, czy wszystkie komórki są zapełnione
            toPlayer1.writeInt(DRAW);
            toPlayer2.writeInt(DRAW);
            sendMove(toPlayer2, row, column);
            break;
          }
          else {
            // Powiadamianie gracza 2, że ma wykonać ruch
            toPlayer2.writeInt(CONTINUE);
  
            // Przesyłanie do gracza 2 wiersza i kolumny komórki zaznaczonej przez gracza 1
            sendMove(toPlayer2, row, column);
          }
  
          // Przyjmowanie ruchu od gracza 2
          row = fromPlayer2.readInt();
          column = fromPlayer2.readInt();
          cell[row][column] = 'O';
  
          // Sprawdzanie, czy gracz 2 wygrał
          if (isWon('O')) {
            toPlayer1.writeInt(PLAYER2_WON);
            toPlayer2.writeInt(PLAYER2_WON);
            sendMove(toPlayer1, row, column);
            break;
          }
          else {
            // Powiadamianie gracza 1, że powinien wykonać ruch	
            toPlayer1.writeInt(CONTINUE);
  
            // Przesyłanie do gracza 1 wiersza i kolumny komórki wybranej przez gracza 2
            sendMove(toPlayer1, row, column);
          }
        }
      }
      catch(IOException ex) {
        ex.printStackTrace();
      }
    }
  
    /** Przesyłanie ruchu do drugiego gracza */
    private void sendMove(DataOutputStream out, int row, int column)
        throws IOException {
      out.writeInt(row); // Przesyłanie indeksu wiersza
      out.writeInt(column); // Przesyłanie indeksu kolumny
    }
  
    /** Sprawdzanie, czy wszystkie komórki są zajęte */
    private boolean isFull() {
      for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
          if (cell[i][j] == ' ')
            return false; // Przynajmniej jedna komórka jest pusta
  
      // Wszystkie komórki są zajęte
      return true;
    }
  
    /** Sprawdzanie, czy gracz używający podanego symbolu wygrał */
    private boolean isWon(char token) {
      // Sprawdzanie wszystkich wierszy
      for (int i = 0; i < 3; i++)
        if ((cell[i][0] == token)
            && (cell[i][1] == token)
            && (cell[i][2] == token)) {
          return true;
        }
  
      /** Sprawdzanie wszystkich kolumn */
      for (int j = 0; j < 3; j++)
        if ((cell[0][j] == token)
            && (cell[1][j] == token)
            && (cell[2][j] == token)) {
          return true;
        }
  
      /** Sprawdzanie przekątnej lewa góra — prawa dół */
      if ((cell[0][0] == token)
          && (cell[1][1] == token)
          && (cell[2][2] == token)) {
        return true;
      }
  
      /** Sprawdzanie przekątnej lewa dół — prawa góra */
      if ((cell[0][2] == token)
          && (cell[1][1] == token)
          && (cell[2][0] == token)) {
        return true;
      }
  
      /** Sprawdzono wszystkie możliwości, ale nie znaleziono zwycięzcy */
      return false;
    }
  }
  
  /**
   * Metoda main jest potrzebna tylko w środowiskach IDE z ograniczoną obsługą platformy JavaFX.
   * Nie jest potrzebna przy uruchamianiu kodu w wierszu poleceń.
   */
  public static void main(String[] args) {
    launch(args);
  }
}
