/**
   * * Method to face the key exchange protocol. Here a basic Diffie-Hellman protocol is implemented
   * (book version). From the server, client receives global parameters and client must calculate
   * the shared key.
   *
   * @throws IOException Communication errors will throw this one
   * @throws ParseException Appears if something unexpected is received
   */
  private void exchangePhase() throws IOException, ParseException {
    DHExStartRequest startRequest = new DHExStartRequest(++counter);
    log.debug("Message to send: [" + startRequest.toJSON() + "]");
    writer.write(startRequest.toJSON().getBytes("UTF-8"));
    writer.flush();

    // get the public parameters and calculate the shared key
    byte[] buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    String reply = new String(buff, "UTF-8");

    log.debug("Message received: [" + reply + "]");
    DHExStartResponse response = new DHExStartResponse();
    response.fromJSON(StringParser.getUTFString(reply));

    generator = response.getGenerator();
    prime = response.getPrime();
    pkServer = response.getPkServer();
    skClient = response.getSkClient();

    // after that the client got the public parameters, it calculates the shared key
    if (skClient != BigInteger.ZERO) {
      BigInteger[] pair = DHEx.createDHPair(generator, prime, skClient);
      pkClient = pair[1];
    } else {
      BigInteger tempKey = DHEx.createPrivateKey(2048);
      BigInteger[] pair = DHEx.createDHPair(generator, prime, tempKey);
      skClient = pair[0];
      pkClient = pair[1];
    }

    DHExRequest dhexRequest = new DHExRequest(pkClient, ++counter);
    log.debug("Message to send: [" + dhexRequest.toJSON() + "]");
    writer.write(dhexRequest.toJSON().getBytes("UTF-8"));
    writer.flush();

    // calculate the shared key
    buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    reply = new String(buff, "UTF-8");

    log.debug("Message received: [" + reply + "]");
    DHExResponse dhResponse = new DHExResponse();
    dhResponse.fromJSON(StringParser.getUTFString(reply));

    sharedKey = DHEx.getDHSharedKey(pkServer, skClient, prime);
    log.debug("The shared key is: [" + sharedKey + "]");

    // finalize the process sending the shared key (for checking only)
    DHExDoneRequest doneRequest = new DHExDoneRequest(sharedKey, ++counter);
    log.debug("Message to send: [" + doneRequest.toJSON() + "]");
    writer.write(doneRequest.toJSON().getBytes("UTF-8"));
    writer.flush();
  }
  /**
   * * All the symmetric encryption and decryption occurs here. Ciphers must be instantiated and
   * executing using the derivated keys from p1 and p2. From a text corpus client and server
   * interchange some lines, all of them encrypted. The challenge for client is to decrypt and
   * answer accordingly to server expectations.
   *
   * @throws IOException Communication errors will throw this one
   * @throws ParseException Appears if something unexpected is received
   */
  private void communicationPhase() throws IOException, ParseException {
    // stream cipher is instantiated here!
    streamCipher = new StreamCipher(sharedKey, prime, p1, p2);

    // send ACK informing that we are ready to encrypt/decrypt
    SpecsDoneRequest request = new SpecsDoneRequest(++counter);
    log.debug("Message to send: [" + request.toJSON() + "]");
    writer.write(request.toJSON().getBytes("UTF-8"));
    writer.flush();

    // Receive all text from Server
    receiveAllLines();

    // Reset the Shift Register prior to sending out CLIENT_TEXT messages
    streamCipher.reset();

    // Send all text to Server
    sendAllLines();

    // Wait for last server message
    byte[] buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    String reply = new String(buff, "UTF-8");

    log.debug("Message received: [" + reply + "]");
    CommDoneResponse response = new CommDoneResponse();
    response.fromJSON(StringParser.getUTFString(reply));

    // answers properly
    CommDoneRequest doneRequest = new CommDoneRequest();
    log.debug("Message to send: [" + doneRequest.toJSON() + "]");
    writer.write(doneRequest.toJSON().getBytes("UTF-8"));
  }
  private TextResponse strictReceive(long length) throws IOException, ParseException {
    TextResponse response = new TextResponse();

    byte[] buffer = new byte[(int) length];
    reader.read(buffer);
    String reply = new String(buffer, "UTF-8");
    log.debug("Message received: [" + reply + "]");
    response.fromJSON(StringParser.getUTFString(reply));

    return response;
  }
  private void sendLine(long id, String line) throws IOException, ParseException {
    String encrpytedMsg = streamCipher.encrypt(line);
    TextRequest msgRequest = new TextRequest(id, encrpytedMsg, ++counter);

    // Send TEXT Message Length
    NextMessageLengthRequest lenRequest =
        new NextMessageLengthRequest(id, msgRequest.toJSON(), ++counter);
    log.debug("Message to send: [" + lenRequest.toJSON() + "]");
    writer.write(lenRequest.toJSON().getBytes("UTF-8"));
    writer.flush();

    // Length Acknowledgement
    byte[] buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    String reply = new String(buff, "UTF-8");
    log.debug("Message received: [" + reply + "]");

    MessageLengthReceivedResponse lenResponse = new MessageLengthReceivedResponse();
    lenResponse.fromJSON(StringParser.getUTFString(reply));

    // Send TEXT Message
    System.out.println("CLIENT_TEXT >>>>>>>>> ID: " + id);
    System.out.println("Plain Text: \n" + line);
    System.out.println("Cipher Text: \n" + encrpytedMsg);

    log.debug("Message to send: [" + msgRequest.toJSON() + "]");
    writer.write(msgRequest.toJSON().getBytes("UTF-8"));
    writer.flush();

    // Wait Acknowledgement of SERVER_TEXT_RECV
    buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    reply = new String(buff, "UTF-8");
    log.debug("Message received: [" + reply + "]");

    TextReceivedResponse txtResponse = new TextReceivedResponse();
    txtResponse.fromJSON(StringParser.getUTFString(reply));
  }
  /**
   * * If everything goes well, client must finish the communication properly with the server.
   *
   * @throws IOException Communication errors will throw this one
   * @throws ParseException Appears if something unexpected is received
   */
  private void exit() throws IOException, ParseException {
    // process the last message...
    byte[] buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    String reply = new String(buff, "UTF-8");

    log.debug("Message received: [" + reply + "]");
    FinishResponse response = new FinishResponse();
    response.fromJSON(StringParser.getUTFString(reply));

    // try to close the socket...
    log.info("Client Tasks completed successfully. Terminating cleanly...");
    if (socket != null && !socket.isClosed()) socket.close();
  }
  /**
   * * This method starts the negotiation between the client and the server. It sends a CLIENT_HELLO
   * message and waits until receives a SERVER_HELLO message.
   *
   * @throws IOException Communication errors will throw this one
   * @throws ParseException Appears if something unexpected is received
   */
  private void contactPhase() throws IOException, ParseException {
    HelloRequest request = new HelloRequest(studentId, ++counter);
    log.debug("Message to send: [" + request.toJSON() + "]");
    writer.write(request.toJSON().getBytes("UTF-8"));
    writer.flush();

    byte[] buffer = new byte[BUFFER_SIZE];
    reader.read(buffer);
    String reply = new String(buffer, "UTF-8");

    log.debug("Message received: [" + reply + "]");
    HelloResponse response = new HelloResponse();
    response.fromJSON(StringParser.getUTFString(reply));
  }
  private boolean receiveLine() throws IOException, ParseException {
    long messageLength = 0L, lineNumber = 0L;

    // we read the size of the line corpus
    byte[] buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    String reply = new String(buff, "UTF-8");

    // two possible messages could be received, next length and text done
    // but client also can request some info...
    log.debug("Message received: [" + reply + "]");
    if (reply.contains(ServerMessageType.SERVER_TEXT_DONE.toString())) return true;
    else if (reply.contains(ServerMessageType.SERVER_NEXT_LENGTH.toString())) {
      NextLengthResponse response = new NextLengthResponse();
      response.fromJSON(StringParser.getUTFString(reply));

      messageLength = response.getLength();
      lineNumber = response.getId();
    } else {
      NextLengthRequest request = new NextLengthRequest(++counter);
      log.debug("Message to send: [" + request.toJSON() + "]");
      writer.write(request.toJSON().getBytes("UTF-8"));
      writer.flush();
    }

    // Send Acknowledgement of Message Length
    MessageLengthReceivedRequest msgACKRequest = null;
    msgACKRequest = new MessageLengthReceivedRequest(lineNumber, ++counter);
    log.debug("Message to send: [" + msgACKRequest.toJSON() + "]");
    writer.write(msgACKRequest.toJSON().getBytes("UTF-8"));
    writer.flush();

    // Get complete TEXT Message
    TextResponse response = strictReceive(messageLength);

    // decrypt the content of the message
    String decryptedBody = decrypt(response.getBody());
    System.out.println("SERVER_TEXT <<<<<<<<<<<< ID: " + response.getId());
    System.out.println("Cipher Text: \n" + response.getBody());
    System.out.println("Plain Text: \n" + decryptedBody);

    // Inform Client TEXT Message Received
    TextReceivedRequest request = new TextReceivedRequest(lineNumber, ++counter);
    log.debug("Message to send: [" + request.toJSON() + "]");
    writer.write(request.toJSON().getBytes("UTF-8"));
    writer.flush();

    return false;
  }
  /**
   * * Previously to encrypt messages, server and client must agree the amount of messages they will
   * use. In the specification phase client will receive the information needed to encrypt and
   * decrypt server messages.
   *
   * @throws IOException Communication errors will throw this one
   * @throws ParseException Appears if something unexpected is received
   */
  private void specificationPhase() throws IOException, ServerDHKeyException, ParseException {
    byte[] buff = new byte[BUFFER_SIZE];
    reader.read(buff);
    String reply = new String(buff, "UTF-8");

    // from the message we got the number of lines that will be sent
    log.debug("Message received: [" + reply + "]");
    if (reply.contains("SERVER_DHEX_ERROR")) throw new ServerDHKeyException();

    SpecsResponse specsResponse = new SpecsResponse();
    specsResponse.fromJSON(StringParser.getUTFString(reply));

    linesToWork = specsResponse.getOutLines();
    p1 = specsResponse.getP1();
    p2 = specsResponse.getP2();
  }