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.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

public class TicTacToeClient extends Application 
    implements TicTacToeConstants {
  // Informuje, na którego gracza przypada ruch
  private boolean myTurn = false;

  // Symbol używany przez gracza
  private char myToken = ' ';

  // Symbol używany przez przeciwnika
  private char otherToken = ' ';

  // Tworzenie i inicjowanie komórek
  private Cell[][] cell =  new Cell[3][3];

  // Tworzenie i inicjowanie etykiety z nagłówkiem
  private Label lblTitle = new Label();

  // Tworzenie i inicjowanie etykiety ze stanem gry
  private Label lblStatus = new Label();

  // Wiersz i kolumna komórki zaznaczonej w bieżącym ruchu
  private int rowSelected;
  private int columnSelected;

  // Strumienie wejściowy i wyjściowy do komunikacji z serwerem
  private DataInputStream fromServer;
  private DataOutputStream toServer;

  // Gra jest kontynuowana?
  private boolean continueToPlay = true;

  // Oczekiwanie na zaznaczenia komórki przez gracza
  private boolean waiting = true;

  // Nazwa lub adres IP hosta
  private String host = "localhost";

  @Override // Przesłanianie metody start z klasy Application
  public void start(Stage primaryStage) {
    // Panel z komórkami
    GridPane pane = new GridPane(); 
    for (int i = 0; i < 3; i++)
      for (int j = 0; j < 3; j++)
        pane.add(cell[i][j] = new Cell(i, j), j, i);

    BorderPane borderPane = new BorderPane();
    borderPane.setTop(lblTitle);
    borderPane.setCenter(pane);
    borderPane.setBottom(lblStatus);
    
    // Tworzenie sceny i umieszczanie jej w oknie
    Scene scene = new Scene(borderPane, 320, 350);
    primaryStage.setTitle("TicTacToeClient"); // Ustawianie nagłówka okna
    primaryStage.setScene(scene); // Umieszczanie sceny w oknie
    primaryStage.show(); // Wyświetlanie okna

    // Łączenie się z serwerem
    connectToServer();
  }

  private void connectToServer() {
    try {
      // Tworzenie gniazda do łączenia się z serwerem
      Socket socket = new Socket(host, 8000);

      // Tworzenie strumienia wejściowego do pobierania danych z serwera
      fromServer = new DataInputStream(socket.getInputStream());

      // Tworzenie strumienia wyjściowego do przesyłania danych na serwer
      toServer = new DataOutputStream(socket.getOutputStream());
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }

    // Sterowanie grą w odrębnym wątku
    new Thread(() -> {
      try {
        // Pobieranie powiadomienia od serwera
        int player = fromServer.readInt();
  
        // Użytkownik jest graczem 1 czy 2?
        if (player == PLAYER1) {
          myToken = 'X';
          otherToken = 'O';
          Platform.runLater(() -> {
            lblTitle.setText("Gracz 1 używający symbolu 'X'");
            lblStatus.setText("Oczekiwanie na dołączenie gracza 2");
          });
  
          // Otrzymanie od serwera powiadomienia o rozpoczęciu gry 
          fromServer.readInt(); // Wczytane dane są ignorowane
  
          // Drugi gracz dołączył do gry
          Platform.runLater(() -> 
            lblStatus.setText("Gracz 2 dołączył. Rozpoczynasz grę"));
  
          // Kolejka danego użytkownika
          myTurn = true;
        }
        else if (player == PLAYER2) {
          myToken = 'O';
          otherToken = 'X';
          Platform.runLater(() -> {
            lblTitle.setText("Gracz 2 używający symbolu 'O'");
            lblStatus.setText("Oczekiwanie na ruch gracza 1");
          });
        }
  
        // Kontynuowanie gry
        while (continueToPlay) {      
          if (player == PLAYER1) {
            waitForPlayerAction(); // Oczekiwanie na ruch gracza 1
            sendMove(); // Przesyłanie ruchu na serwer
            receiveInfoFromServer(); // Przyjmowanie informacji z serwera
          }
          else if (player == PLAYER2) {
            receiveInfoFromServer(); // Przyjmowanie informacji z serwera
            waitForPlayerAction(); // Oczekiwanie na ruch gracza 2
            sendMove(); // Przesyłanie ruchu gracza 2 na serwer
          }
        }
      }
      catch (Exception ex) {
        ex.printStackTrace();
      }
    }).start();
  }

  /** Oczekiwanie na zaznaczenie komórki przez gracza */
  private void waitForPlayerAction() throws InterruptedException {
    while (waiting) {
      Thread.sleep(100);
    }

    waiting = true;
  }

  /** Przesyłanie ruchu danego gracza na serwer */
  private void sendMove() throws IOException {
    toServer.writeInt(rowSelected); // Przesyłanie wiersza
    toServer.writeInt(columnSelected); // Przesyłanie kolumny
  }

  /** Przyjmowanie informacji z serwera */
  private void receiveInfoFromServer() throws IOException {
    // Pobieranie stanu gry
    int status = fromServer.readInt();

    if (status == PLAYER1_WON) {
      // Gracz 1 wygrał, koniec gry
      continueToPlay = false;
      if (myToken == 'X') {
        Platform.runLater(() -> lblStatus.setText("Wygrałeś! (X)"));
      }
      else if (myToken == 'O') {
        Platform.runLater(() -> 
          lblStatus.setText("Wygrał gracz 1 (X)!"));
        receiveMove();
      }
    }
    else if (status == PLAYER2_WON) {
      // Wygrał gracz 2, koniec gry
      continueToPlay = false;
      if (myToken == 'O') {
        Platform.runLater(() -> lblStatus.setText("Wygrałeś! (O)"));
      }
      else if (myToken == 'X') {
        Platform.runLater(() -> 
          lblStatus.setText("Wygrał gracz 2 (O)!"));
        receiveMove();
      }
    }
    else if (status == DRAW) {
      // Brak zwycięzcy, koniec gry
      continueToPlay = false;
      Platform.runLater(() -> 
        lblStatus.setText("Koniec gry, brak zwycięzcy!"));

      if (myToken == 'O') {
        receiveMove();
      }
    }
    else {
      receiveMove();
      Platform.runLater(() -> lblStatus.setText("Twój ruch"));
      myTurn = true; // Ruch danego gracza
    }
  }

  private void receiveMove() throws IOException {
    // Pobieranie ruchu drugiego gracza
    int row = fromServer.readInt();
    int column = fromServer.readInt();
    Platform.runLater(() -> cell[row][column].setToken(otherToken));
  }

  // Klasa wewnętrzna reprezentująca komórkę
  public class Cell extends Pane {
    // Indeksy wiersza i kolumny danej komórki na planszy
    private int row;
    private int column;

    // Symbol umieszczony w komórce
    private char token = ' ';

    public Cell(int row, int column) {
      this.row = row;
      this.column = column;
      this.setPrefSize(2000, 2000); // Co się stanie, jeśli pominiesz ten wiersz?
      setStyle("-fx-border-color: black"); // Obramowanie komórki
      this.setOnMouseClicked(e -> handleMouseClick());  
    }

    /** Zwracanie symbolu */
    public char getToken() {
      return token;
    }

    /** Ustawianie nowego symbolu */
    public void setToken(char c) {
      token = c;
      repaint();
    }

    protected void repaint() {
      if (token == 'X') {
        Line line1 = new Line(10, 10, 
          this.getWidth() - 10, this.getHeight() - 10);
        line1.endXProperty().bind(this.widthProperty().subtract(10));
        line1.endYProperty().bind(this.heightProperty().subtract(10));
        Line line2 = new Line(10, this.getHeight() - 10, 
          this.getWidth() - 10, 10);
        line2.startYProperty().bind(
          this.heightProperty().subtract(10));
        line2.endXProperty().bind(this.widthProperty().subtract(10));
        
        // Dodawanie linii do panelu
        this.getChildren().addAll(line1, line2); 
      }
      else if (token == 'O') {
        Ellipse ellipse = new Ellipse(this.getWidth() / 2, 
          this.getHeight() / 2, this.getWidth() / 2 - 10, 
          this.getHeight() / 2 - 10);
        ellipse.centerXProperty().bind(
          this.widthProperty().divide(2));
        ellipse.centerYProperty().bind(
            this.heightProperty().divide(2));
        ellipse.radiusXProperty().bind(
            this.widthProperty().divide(2).subtract(10));        
        ellipse.radiusYProperty().bind(
            this.heightProperty().divide(2).subtract(10));   
        ellipse.setStroke(Color.BLACK);
        ellipse.setFill(Color.WHITE);
        
        getChildren().add(ellipse); // Dodawanie elipsy do panelu
      }
    }

    /* Obsługa zdarzenia kliknięcia myszą */
    private void handleMouseClick() {
      // Jeśli komórka nie jest zajęta i przypada ruch danego gracza
      if (token == ' ' && myTurn) {
        setToken(myToken);  // Należy umieścić symbol tego gracza w komórce
        myTurn = false;
        rowSelected = row;
        columnSelected = column;
        lblStatus.setText("Oczekiwanie na ruch przeciwnika");
        waiting = false; // Gracz wykonał poprawny ruch
      }
    }
  }

  /**
   * 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);
  }
}
