package com.pedro.rtsp.rtsp;

import android.media.MediaCodec;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.pedro.rtsp.utils.ConnectCheckerRtsp;
import com.pedro.rtsp.utils.CreateSSLSocket;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by pedro on 10/02/17.
 */

public class RtspClient {

  private final String TAG = "RtspClient";
  private static final Pattern rtspUrlPattern =
      Pattern.compile("^rtsps?://([^/:]+)(?::(\\d+))*/([^/]+)/?([^*]*)$");

  private ConnectCheckerRtsp connectCheckerRtsp;
  //sockets objects
  private Socket connectionSocket;
  private BufferedReader reader;
  private BufferedWriter writer;
  private Thread thread;
  //for tcp
  private OutputStream outputStream;
  private volatile boolean streaming = false;
  //for secure transport
  private boolean tlsEnabled = false;
  private RtspSender rtspSender;
  private String url;
  private CommandsManager commandsManager;
  private int numRetry;
  private int reTries;
  private Handler handler;
  private Runnable runnable;

  public RtspClient(ConnectCheckerRtsp connectCheckerRtsp) {
    this.connectCheckerRtsp = connectCheckerRtsp;
    commandsManager = new CommandsManager();
    rtspSender = new RtspSender(connectCheckerRtsp);
    handler = new Handler(Looper.getMainLooper());
  }

  public void setOnlyAudio(boolean onlyAudio) {
    commandsManager.setOnlyAudio(onlyAudio);
  }

  public void setProtocol(Protocol protocol) {
    commandsManager.setProtocol(protocol);
  }

  public void setAuthorization(String user, String password) {
    commandsManager.setAuth(user, password);
  }

  public void setReTries(int reTries) {
    numRetry = reTries;
    this.reTries = reTries;
  }

  public boolean shouldRetry(String reason) {
    boolean validReason = !reason.contains("Endpoint malformed");
    return validReason && reTries > 0;
  }

  public boolean isStreaming() {
    return streaming;
  }

  public void setUrl(String url) {
    this.url = url;
  }

  public void setSampleRate(int sampleRate) {
    commandsManager.setSampleRate(sampleRate);
  }

  public String getHost() {
    return commandsManager.getHost();
  }

  public int getPort() {
    return commandsManager.getPort();
  }

  public String getPath() {
    return commandsManager.getPath();
  }

  public ConnectCheckerRtsp getConnectCheckerRtsp() {
    return connectCheckerRtsp;
  }

  public void setSPSandPPS(ByteBuffer sps, ByteBuffer pps, ByteBuffer vps) {
    commandsManager.setVideoInfo(sps, pps, vps);
  }

  public void setIsStereo(boolean isStereo) {
    commandsManager.setIsStereo(isStereo);
  }

  public void connect() {
    if (!streaming) {
      Matcher rtspMatcher = rtspUrlPattern.matcher(url);
      if (rtspMatcher.matches()) {
        tlsEnabled = rtspMatcher.group(0).startsWith("rtsps");
      } else {
        streaming = false;
        connectCheckerRtsp.onConnectionFailedRtsp(
            "Endpoint malformed, should be: rtsp://ip:port/appname/streamname");
        return;
      }
      String host = rtspMatcher.group(1);
      int port = Integer.parseInt((rtspMatcher.group(2) != null) ? rtspMatcher.group(2) : "554");
      String path = "/" + rtspMatcher.group(3) + "/" + rtspMatcher.group(4);
      commandsManager.setUrl(host, port, path);

      rtspSender.setSocketsInfo(commandsManager.getProtocol(),
          commandsManager.getVideoClientPorts(), commandsManager.getAudioClientPorts());
      rtspSender.setAudioInfo(commandsManager.getSampleRate());
      if (!commandsManager.isOnlyAudio()) {
        rtspSender.setVideoInfo(commandsManager.getSps(), commandsManager.getPps(),
            commandsManager.getVps());
      }
      thread = new Thread(new Runnable() {
        @Override
        public void run() {
          try {
            if (!tlsEnabled) {
              connectionSocket = new Socket();
              SocketAddress socketAddress =
                  new InetSocketAddress(commandsManager.getHost(), commandsManager.getPort());
              connectionSocket.connect(socketAddress, 5000);
            } else {
              connectionSocket = CreateSSLSocket.createSSlSocket(commandsManager.getHost(),
                  commandsManager.getPort());
              if (connectionSocket == null) throw new IOException("Socket creation failed");
            }
            connectionSocket.setSoTimeout(5000);
            reader = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
            outputStream = connectionSocket.getOutputStream();
            writer = new BufferedWriter(new OutputStreamWriter(outputStream));
            writer.write(commandsManager.createOptions());
            writer.flush();
            commandsManager.getResponse(reader, connectCheckerRtsp, false, false);
            writer.write(commandsManager.createAnnounce());
            writer.flush();
            //check if you need credential for stream, if you need try connect with credential
            String response = commandsManager.getResponse(reader, connectCheckerRtsp, false, false);
            int status = commandsManager.getResponseStatus(response);
            if (status == 403) {
              connectCheckerRtsp.onConnectionFailedRtsp("Error configure stream, access denied");
              Log.e(TAG, "Response 403, access denied");
              return;
            } else if (status == 401) {
              if (commandsManager.getUser() == null || commandsManager.getPassword() == null) {
                connectCheckerRtsp.onAuthErrorRtsp();
                return;
              } else {
                writer.write(commandsManager.createAnnounceWithAuth(response));
                writer.flush();
                int statusAuth = commandsManager.getResponseStatus(
                    commandsManager.getResponse(reader, connectCheckerRtsp, false, false));
                if (statusAuth == 401) {
                  connectCheckerRtsp.onAuthErrorRtsp();
                  return;
                } else if (statusAuth == 200) {
                  connectCheckerRtsp.onAuthSuccessRtsp();
                } else {
                  connectCheckerRtsp.onConnectionFailedRtsp(
                      "Error configure stream, announce with auth failed");
                }
              }
            } else if (status != 200) {
              connectCheckerRtsp.onConnectionFailedRtsp("Error configure stream, announce failed");
            }
            writer.write(commandsManager.createSetup(commandsManager.getTrackAudio()));
            writer.flush();
            commandsManager.getResponse(reader, connectCheckerRtsp, true, true);
            if (!commandsManager.isOnlyAudio()) {
              writer.write(commandsManager.createSetup(commandsManager.getTrackVideo()));
              writer.flush();
              commandsManager.getResponse(reader, connectCheckerRtsp, false, true);
            }
            writer.write(commandsManager.createRecord());
            writer.flush();
            commandsManager.getResponse(reader, connectCheckerRtsp, false, true);

            rtspSender.setDataStream(outputStream, commandsManager.getHost());
            int[] videoPorts = commandsManager.getVideoServerPorts();
            int[] audioPorts = commandsManager.getAudioServerPorts();
            if (!commandsManager.isOnlyAudio()) {
              rtspSender.setVideoPorts(videoPorts[0], videoPorts[1]);
            }
            rtspSender.setAudioPorts(audioPorts[0], audioPorts[1]);
            rtspSender.start();
            streaming = true;
            reTries = numRetry;
            connectCheckerRtsp.onConnectionSuccessRtsp();
          } catch (IOException | NullPointerException e) {
            Log.e(TAG, "connection error", e);
            connectCheckerRtsp.onConnectionFailedRtsp("Error configure stream, " + e.getMessage());
            streaming = false;
          }
        }
      });
      thread.start();
    }
  }

  public void disconnect() {
    handler.removeCallbacks(runnable);
    disconnect(true);
  }

  private void disconnect(final boolean clear) {
    if (streaming) rtspSender.stop();
    streaming = false;
    thread = new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          if (writer != null) {
            writer.write(commandsManager.createTeardown());
            writer.flush();
            if (clear) {
              commandsManager.clear();
            } else {
              commandsManager.retryClear();
            }
          }
          if (connectionSocket != null) connectionSocket.close();
          writer = null;
          connectionSocket = null;
        } catch (IOException e) {
          if (clear) {
            commandsManager.clear();
          } else {
            commandsManager.retryClear();
          }
          Log.e(TAG, "disconnect error", e);
        }
      }
    });
    thread.start();
    if (clear) {
      reTries = 0;
      connectCheckerRtsp.onDisconnectRtsp();
    }
  }

  public void sendVideo(ByteBuffer h264Buffer, MediaCodec.BufferInfo info) {
    if (isStreaming() && !commandsManager.isOnlyAudio()) {
      rtspSender.sendVideoFrame(h264Buffer, info);
    }
  }

  public void sendAudio(ByteBuffer aacBuffer, MediaCodec.BufferInfo info) {
    if (isStreaming()) {
      rtspSender.sendAudioFrame(aacBuffer, info);
    }
  }

  public void reConnect(long delay) {
    reTries--;
    disconnect(false);
    runnable = new Runnable() {
      @Override
      public void run() {
        connect();
      }
    };
    handler.postDelayed(runnable, delay);
  }

  public long getDroppedAudioFrames() {
    return rtspSender.getDroppedAudioFrames();
  }

  public long getDroppedVideoFrames() {
    return rtspSender.getDroppedVideoFrames();
  }

  public void resetSentAudioFrames() {
    rtspSender.resetSentAudioFrames();
  }

  public void resetSentVideoFrames() {
    rtspSender.resetSentVideoFrames();
  }

  public void resetDroppedAudioFrames() {
    rtspSender.resetDroppedAudioFrames();
  }

  public void resetDroppedVideoFrames() {
    rtspSender.resetDroppedVideoFrames();
  }

  public void resizeCache(int newSize) throws RuntimeException {
    rtspSender.resizeCache(newSize);
  }

  public int getCacheSize() {
    return rtspSender.getCacheSize();
  }

  public long getSentAudioFrames() {
    return rtspSender.getSentAudioFrames();
  }

  public long getSentVideoFrames() {
    return rtspSender.getSentVideoFrames();
  }
}

