// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.location.suplclient.supl;

import com.google.common.base.Preconditions;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

/**
 * A TCP client that is used to send and receive SUPL request and responses by the SUPL client. The
 * constructor establishes a connection to the SUPL server specified by a given address and port.
 */
final class SuplTcpConnection {

  private static final Logger logger = Logger.getLogger(SuplTcpConnection.class.getName());
  private static final int READ_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(10);
  private static final short HEADER_SIZE = 2;
  /** BUFFER_SIZE data size that is enough to hold SUPL responses */
  private static final int RESPONSE_BUFFER_SIZE = 16384;

  private final ByteBuffer messageLengthReadBuffer =
      ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN);

  private Socket socket;
  private BufferedInputStream bufferedInputStream;

  public SuplTcpConnection(SuplConnectionRequest request) throws UnknownHostException, IOException {
    socket = createSocket(request);
    socket.setSoTimeout(READ_TIMEOUT_MILLIS);
    socket.setReceiveBufferSize(RESPONSE_BUFFER_SIZE);
    bufferedInputStream = new BufferedInputStream(socket.getInputStream());

    System.out.println("Connection established to " + socket.getOutputStream());
  }

  /** Sends a byte array of SUPL data to the server */
  public void sendSuplRequest(byte[] data) throws IOException {
    socket.getOutputStream().write(data);
  }

  /**
   * Reads SUPL server response and returns it as a byte array.
   *
   * <p>Upon the SUPL protocol, the size of the payload is stored in the first two bytes of the
   * response, hence these two bytes are read first followed by reading a payload of that size. Null
   * is returned if the size of the payload is not readable.
   */
  public byte[] getSuplResponse() throws IOException {
    byte[] buffer = new byte[RESPONSE_BUFFER_SIZE];
    int sizeOfRead = bufferedInputStream.read(buffer, 0, HEADER_SIZE);
    if (sizeOfRead == HEADER_SIZE) {
      messageLengthReadBuffer.clear();
      messageLengthReadBuffer.put(0, buffer[0]);
      messageLengthReadBuffer.put(1, buffer[1]);
      int messageLength = messageLengthReadBuffer.getShort(0);

      int bytesRead = sizeOfRead;
      while (bytesRead < messageLength) {
        sizeOfRead = bufferedInputStream.read(buffer, bytesRead, messageLength - bytesRead);
        Preconditions.checkState(
            sizeOfRead != -1,
            "Expected length of the messagte in bytes = "
                + messageLength
                + " while total number of bytes received = "
                + bytesRead);
        bytesRead = bytesRead + sizeOfRead;
      }
      return Arrays.copyOf(buffer, messageLength);
    } else {
      return null;
    }
  }

  /** Closes the TCP socket */
  public void closeSocket() throws IOException {
    socket.close();
  }

  private static Socket createSocket(SuplConnectionRequest request) throws IOException {
    String host = request.getServerHost();
    int port = request.getServerPort();
    logger.info("Connecting to " + host + " on port " + port);

    if (request.isSslEnabled()) {
      Preconditions.checkState(
          SuplConstants.SuplServerConstants.SSL_PORTS.contains(port),
          "An SSL connection is requested on a non SSL port, this should not happen.");
      SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
      SSLSocket sslSocket = (SSLSocket) factory.createSocket(host, port);
      sslSocket.startHandshake();
      return sslSocket;
    } else {
      Preconditions.checkState(
          SuplConstants.SuplServerConstants.NON_SSL_PORTS.contains(port),
          "A NON-SSL connection is requested on an SSL port, this should not happen.");
      return new Socket(host, port);
    }
  }
}