defmodule GenRtmpServer.Protocol do
  @moduledoc """
  Ranch protocol that abstracts the RTMP protocol logic away
  """

  @behaviour :ranch_protocol
  @behaviour Rtmp.Behaviours.EventReceiver
  @behaviour Rtmp.Behaviours.SocketHandler

  alias Rtmp.ServerSession.Events, as: RtmpEvents
  alias Rtmp.Protocol.Messages, as: RtmpMessages

  use GenServer
  require Logger

  defmodule State do
    defstruct socket: nil,
              transport: nil,
              session_id: nil,
              bytes_read: 0,
              bytes_sent: 0,
              handshake_completed: false,
              handshake_instance: nil,
              protocol_handler_pid: nil,
              session_handler_pid: nil,
              gen_rtmp_server_adopter: nil,
              adopter_state: nil,
              session_config: nil,
              log_files: %{},
              adopter_args: nil,
              total_bytes_sent: 0,
              total_packets_sent: 0,
              total_bytes_dropped: 0,
              total_packets_dropped: 0
  end

  @doc "Starts the protocol for the accepted socket"
  def start_link(ref, socket, transport, [module, options = %GenRtmpServer.RtmpOptions{}, adopter_args]) do
    :proc_lib.start_link(__MODULE__, :init, [ref, socket, transport, module, options, adopter_args])
  end

  @doc false
  def send_event(pid, event) do
    GenServer.cast(pid, {:session_event, event})
  end

  @doc false
  def send_data(pid, binary, packet_type) do
    GenServer.cast(pid, {:rtmp_output, binary, packet_type})
  end

  def init(ref, socket, transport, module, options, adopter_args) do
    :ok = :proc_lib.init_ack({:ok, self()})
    :ok = :ranch.accept_ack(ref)

    session_id = UUID.uuid4()
    client_ip_string = get_ip_address(socket)

    _ = Logger.info "#{session_id}: client connected from ip #{client_ip_string}"

    {handshake_instance, %Rtmp.Handshake.ParseResult{bytes_to_send: bytes_to_send}}
      = Rtmp.Handshake.new(:unknown) # Let the client's handshake tell us the format

    :ok = transport.send(socket, bytes_to_send)

    options_list = GenRtmpServer.RtmpOptions.to_keyword_list(options)
    session_config = create_session_config(options_list)

    state = %State{
      socket: socket,
      transport: transport,
      handshake_instance: handshake_instance,
      session_id: session_id,
      gen_rtmp_server_adopter: module,
      session_config: session_config,
      adopter_args: adopter_args,
    }

    set_socket_options(state)
    :gen_server.enter_loop(__MODULE__, [], state)
  end

  def handle_cast({:session_event, event}, state) do
    state = handle_event(event, state)
    {:noreply, state}
  end

  def handle_cast({:rtmp_output, binary, packet_type}, state) do
    # If the network buffer becomes full due to a connection that can't handle the amount
    # of audio/video data going out to the server, we need to compensate by dropping
    # audio/video data as necessary in order to not back everything up.  All non-audio/video
    # messages should not be dropped though.

    # TODO: Should probably make sure sequence header packets are never dropped.
    # Otherwise the video is useless.

    can_be_dropped = case packet_type do
      :audio -> true
      :video -> true
      _ -> false
    end

    state = case can_be_dropped do
      false -> send_undroppable_packet(state, binary)
      true -> send_droppable_packet(state, binary)
    end

    {:noreply, state}
  end

  def handle_info({:tcp, _, binary}, state = %State{handshake_completed: false}) do
    case Rtmp.Handshake.process_bytes(state.handshake_instance, binary) do
      {instance, result = %Rtmp.Handshake.ParseResult{current_state: :waiting_for_data}} ->
        if byte_size(result.bytes_to_send) > 0, do: state.transport.send(state.socket, result.bytes_to_send)

        new_state = %{state | handshake_instance: instance}
        set_socket_options(new_state)
        {:noreply, new_state}
      
      {instance, result = %Rtmp.Handshake.ParseResult{current_state: :success}} ->
        if byte_size(result.bytes_to_send) > 0, do: state.transport.send(state.socket, result.bytes_to_send)

        {_, %Rtmp.Handshake.HandshakeResult{remaining_binary: remaining_binary}}
          = Rtmp.Handshake.get_handshake_result(instance)

        {:ok, protocol_pid} = Rtmp.Protocol.Handler.start_link(state.session_id, self(), __MODULE__)
        {:ok, session_pid} = Rtmp.ServerSession.Handler.start_link(state.session_id, state.session_config)

        :ok = Rtmp.Protocol.Handler.set_session(protocol_pid, session_pid, Rtmp.ServerSession.Handler)
        :ok = Rtmp.ServerSession.Handler.set_rtmp_output_handler(session_pid, protocol_pid, Rtmp.Protocol.Handler)
        :ok = Rtmp.ServerSession.Handler.set_event_handler(session_pid, self(), __MODULE__)

        {:ok, adopter_state} = state.gen_rtmp_server_adopter.init(state.session_id, get_ip_address(state.socket), state.adopter_args)
        :ok = Rtmp.Protocol.Handler.notify_input(protocol_pid, remaining_binary)
        :ok = Rtmp.ServerSession.Handler.send_stream_zero_begin(session_pid)

        state = %{state |
          handshake_instance: nil,
          handshake_completed: true,
          protocol_handler_pid: protocol_pid,
          session_handler_pid: session_pid,
          adopter_state: adopter_state
        }

        state = prepare_log_files(state)

        set_socket_options(state)
        {:noreply, state}

      {_, %Rtmp.Handshake.ParseResult{current_state: :failure}} ->
        _ = Logger.info "#{state.session_id}: Client failed the handshake, disconnecting..."

        state.transport.close(state.socket)
        {:noreply, state}
    end    
  end

  def handle_info({:tcp, _, binary}, state = %State{}) do
    log_io_data(state, :input, binary)

    :ok = Rtmp.Protocol.Handler.notify_input(state.protocol_handler_pid, binary)
    set_socket_options(state)
    {:noreply, state}
  end

  def handle_info({:tcp_closed, _}, state = %State{}) do
    _ = Logger.info "#{state.session_id}: socket closed"

    {:ok, adopter_state} = state.gen_rtmp_server_adopter.handle_disconnection(state.adopter_state)
    state = %{state | adopter_state: adopter_state}

    {:stop, :normal, state}
  end

  def handle_info({:rtmp_send, data, send_to_stream_id, forced_timestamp}, state = %State{}) do
    rtmp_send(data, send_to_stream_id, state, forced_timestamp)
    {:noreply, state}
  end

  def handle_info(:send_ping_request, state = %State{}) do
    :ok = Rtmp.ServerSession.Handler.send_ping_request(state.session_handler_pid)
    {:noreply, state}
  end

  def handle_info({:inet_reply, _socket, _status}, state) do
    {:noreply, state}
  end
  
  def handle_info(message, state = %State{}) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.handle_message(message, state.adopter_state)
    state = %{state | adopter_state: adopter_state}

    set_socket_options(state) # Just in case
    {:noreply, state}
  end

  def code_change(old_version, state, _) do
    case state.gen_rtmp_server_adopter.code_change(old_version, state.adopter_state) do
      {:ok, new_adopter_state} ->
        state = %{state | adopter_state: new_adopter_state}
        {:ok, state}

      {:error, reason} ->
        {:error, reason}
    end
  end
  
  defp set_socket_options(state = %State{}) do
    # Ignore the result, as theoretically a tcp closed message should be arriving
    # that will kill this connection
    _ = state.transport.setopts(state.socket, active: :once, packet: :raw, buffer: 4096)
  end

  defp create_session_config(options) do
    config = %Rtmp.ServerSession.Configuration{}
    config = case Keyword.fetch(options, :fms_version) do
      {:ok, value} -> %{config| fms_version: value}
      :error -> config
    end

    config = case Keyword.fetch(options, :chunk_size) do
      {:ok, value} -> %{config| chunk_size: value}
      :error -> config
    end

    config = case Keyword.fetch(options, :log_mode) do
      {:ok, value} -> %{config| io_log_mode: value}
      :error -> config
    end

    config
  end

  defp get_ip_address(socket) do
    {:ok, {ip, _port}} = :inet.peername(socket)
    ip |> Tuple.to_list() |> Enum.join(".")
  end

  defp handle_event(event = %RtmpEvents.ConnectionRequested{}, state) do
    case state.gen_rtmp_server_adopter.connection_requested(event, state.adopter_state) do
      {:accepted, adopter_state} -> 
        _ = Logger.info("#{state.session_id}: Connection request accepted (app: '#{event.app_name}')")
        handle_accepted_request(state, event.request_id, adopter_state)

      {{:rejected, command, reason}, adopter_state} ->
        _ = Logger.info("#{state.session_id}: Connection request rejected (app: '#{event.app_name}') - #{reason}")
        handle_rejected_request(command, state, event.request_id, adopter_state)
    end
  end

  defp handle_event(event = %RtmpEvents.PublishStreamRequested{}, state) do
    case state.gen_rtmp_server_adopter.publish_requested(event, state.adopter_state) do
      {:accepted, adopter_state} ->
        _ = Logger.info("#{state.session_id}: Publish stream request accepted (app: '#{event.app_name}', key: '#{event.stream_key}')")
        handle_accepted_request(state, event.request_id, adopter_state)

      {{:rejected, command, reason}, adopter_state} ->
        _ = Logger.info("#{state.session_id}: Publish stream request rejected (app: '#{event.app_name}', key: '#{event.stream_key}') - #{reason}")

        handle_rejected_request(command, state, event.request_id, adopter_state)
    end
  end

  defp handle_event(event = %RtmpEvents.PublishingFinished{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.publish_finished(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(%RtmpEvents.PeerChunkSizeChanged{}, state) do
    state
  end

  defp handle_event(event = %RtmpEvents.AudioVideoDataReceived{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.audio_video_data_received(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(event = %RtmpEvents.StreamMetaDataChanged{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.metadata_received(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(event = %RtmpEvents.PlayStreamRequested{}, state) do
    case state.gen_rtmp_server_adopter.play_requested(event, state.adopter_state) do
      {:accepted, adopter_state} ->
        _ = Logger.info("#{state.session_id}: Play stream request accepted (app: '#{event.app_name}', key: '#{event.stream_key}')")
        handle_accepted_request(state, event.request_id, adopter_state)

      {{:rejected, command, reason}, adopter_state} ->
        _ = Logger.info("#{state.session_id}: Play stream request rejected (app: '#{event.app_name}', key: '#{event.stream_key}') - #{reason}")

        handle_rejected_request(command, state, event.request_id, adopter_state)
    end
  end

  defp handle_event(event = %RtmpEvents.PlayStreamFinished{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.play_finished(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(event = %RtmpEvents.NewByteIOTotals{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.byte_io_totals_updated(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(event = %RtmpEvents.AcknowledgementReceived{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.acknowledgement_received(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(event = %RtmpEvents.PingRequestSent{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.ping_request_sent(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(event = %RtmpEvents.PingResponseReceived{}, state) do
    {:ok, adopter_state} = state.gen_rtmp_server_adopter.ping_response_received(event, state.adopter_state)
    %{state | adopter_state: adopter_state}
  end

  defp handle_event(event, state) do
    _ = Logger.warn("#{state.session_id}: No code to handle RTMP session event of type #{inspect(event)}")
    state
  end

  defp handle_accepted_request(state, request_id, adopter_state) do
    :ok = Rtmp.ServerSession.Handler.accept_request(state.session_handler_pid, request_id)
    %{state | adopter_state: adopter_state}
  end

  defp handle_rejected_request(command, state, _request_id, new_adopter_state) do
    case command do
      :disconnect -> state.transport.close(state.socket)
      _ -> :ok
    end

    %{state | adopter_state: new_adopter_state}
  end

  defp rtmp_send(data, send_to_stream_id, state, forced_timestamp) do
    content = form_outbound_rtmp_message(data)
    :ok = Rtmp.ServerSession.Handler.send_rtmp_message(state.session_handler_pid, content, send_to_stream_id, forced_timestamp)
  end

  defp form_outbound_rtmp_message(av_data = %GenRtmpServer.AudioVideoData{}) do
    case av_data.data_type do
      :audio -> %RtmpMessages.AudioData{data: av_data.data}
      :video -> %RtmpMessages.VideoData{data: av_data.data}
    end
  end

  defp form_outbound_rtmp_message(%GenRtmpServer.MetaData{details: metadata}) do
    %RtmpMessages.Amf0Data{parameters: [
      "onMetaData",
      %{
        "width" => metadata.video_width,
        "height" => metadata.video_height,
        "framerate" => metadata.video_frame_rate,
        "videocodecid" => metadata.video_codec,
        "audiocodecid" => metadata.audio_codec,
        "audiochannels" => metadata.audio_channels,
        "audiodatarate" => metadata.audio_bitrate_kbps,
        "audiosamplerate" => metadata.audio_sample_rate,
        "videodatarate" => metadata.video_bitrate_kbps
      }
    ]}
  end

  defp form_outbound_rtmp_message(data) do
    raise("No known way to form outbound rtmp message for data: #{inspect(data)}")
  end

  defp prepare_log_files(state = %State{session_config: %Rtmp.ServerSession.Configuration{io_log_mode: :none}}) do
    state
  end

  defp prepare_log_files(state = %State{session_config: %Rtmp.ServerSession.Configuration{io_log_mode: :raw_io}}) do
    path = "dumps"

    :ok = File.mkdir_p!(path)
    input = File.open!("#{path}/#{state.session_id}.input.rtmp", [:binary, :write, :exclusive])
    output = File.open!("#{path}/#{state.session_id}.output.rtmp", [:binary, :write, :exclusive])

    log_files = Map.put(state.log_files, :input_append, input)
    log_files = Map.put(log_files, :output_append, output)
    %{state | log_files: log_files}
  end

  defp log_io_data(%State{session_config: %Rtmp.ServerSession.Configuration{io_log_mode: :none}}, _input_or_output, _data) do
    :ok
  end

  defp log_io_data(_, _, <<>>) do
    :ok
  end

  defp log_io_data(state = %State{session_config: %Rtmp.ServerSession.Configuration{io_log_mode: :raw_io}}, :input, data) do
    file = Map.fetch!(state.log_files, :input_append)
    IO.binwrite(file, data)
  end

  defp log_io_data(state = %State{session_config: %Rtmp.ServerSession.Configuration{io_log_mode: :raw_io}}, :output, data) do
    file = Map.fetch!(state.log_files, :output_append)
    IO.binwrite(file, data)
  end

  defp send_undroppable_packet(state, binary) do
    :gen_tcp.send(state.socket, binary)
    mark_packet_as_sent(state, binary)
  end

  defp send_droppable_packet(state, binary) do
    case :erlang.port_command(state.socket, binary, [:nosuspend]) do
      true -> mark_packet_as_sent(state, binary)

      false ->
        # Port is too busy to send, drop the packet
        mark_packet_as_dropped(state, binary)
    end
  end

  defp mark_packet_as_sent(state, binary) do
    %{state |
      total_bytes_sent: state.total_bytes_sent + byte_size(binary),
      total_packets_sent: state.total_packets_sent + 1
    }
  end

  defp mark_packet_as_dropped(state, binary) do
    %{state |
      total_bytes_dropped: state.total_bytes_dropped + byte_size(binary),
      total_packets_dropped: state.total_packets_dropped + 1
    }
  end

end