module OverSIP::WebSocket

  class WsFraming

    include ::OverSIP::Logger

    OPCODE = {
      0  => :continuation,
      1  => :text,
      2  => :binary,
      8  => :close,
      9  => :ping,
      10 => :pong
    }

    keepalive_ping_frame = "".encode ::Encoding::BINARY
    keepalive_ping_frame << 137
    keepalive_ping_frame << "keep-alive".bytesize
    keepalive_ping_frame << "keep-alive".encode(::Encoding::BINARY)
    KEEPALIVE_PING_FRAME = keepalive_ping_frame


    attr_writer :ws_app


    def self.class_init
      @@max_frame_size = ::OverSIP.configuration[:websocket][:max_ws_frame_size]
    end


    def log_id
      @log_id ||= "WsFramming #{@connection.connection_log_id}"
    end


    def initialize connection, buffer
      @connection = connection
      @buffer = buffer
      @utf8_validator = ::OverSIP::WebSocket::FramingUtils::Utf8Validator.allocate
      @state = :init
    end


    def do_keep_alive interval
      @keep_alive_timer = ::EM::PeriodicTimer.new(interval) do
        log_system_debug "sending keep-alive ping frame: payload_length=10"  if $oversip_debug
        @connection.send_data KEEPALIVE_PING_FRAME
      end
    end


    def receive_data
      while (case @state
        when :init
          return false  if @buffer.size < 2

          byte1 = @buffer.read(1).getbyte(0)
          byte2 = @buffer.read(1).getbyte(0)

          # FIN is the bit 0.
          @fin = (byte1 & 0b10000000) == 0b10000000

          # RSV1-3 are bits 1-3.
          @rsv1 = (byte1 & 0b01000000) == 0b01000000
          @rsv2 = (byte1 & 0b00100000) == 0b00100000
          @rsv3 = (byte1 & 0b00010000) == 0b00010000

          if @rsv1 or @rsv2 or @rsv3
            log_system_notice "frame has RSV bits set, clossing the connection"
            send_close_frame 1002, "RSV bit set not supported"
            return false
          end

          # opcode are bits 4-7.
          @opcode = byte1 & 0b00001111
          unless (@sym_opcode = OPCODE[@opcode])
            send_close_frame 1002, "unknown opcode=#{@opcode}"
            return false
          end

          # MASK is bit 8.
          @mask = (byte2 & 0b10000000) == 0b10000000
          unless @mask
            send_close_frame 1002, "MASK bit not set"
            return false
          end

          # payload_len are bits 9-15.
          length = byte2 & 0b01111111

          case length
          # Length defined by 8 bytes.
          when 127
            @state = :payload_length_8_bytes
          # Length defined by 2 bytes.
          when 126
            @state = :payload_length_2_bytes
          # Length defined by already received 7 bits.
          else
            @payload_length = length
            @state = :masking_key
          end

          @payload = nil
          true

        when :payload_length_2_bytes
          return false  if @buffer.size < 2

          # Get the payload length and remove first two bytes fro
          # the buffer at the same time.
          @payload_length = @buffer.read(2).unpack('n').first

          @state = :masking_key
          true

        when :payload_length_8_bytes
          return false  if @buffer.size < 8

          # Get the payload length.
          # NOTE: Just take the last 4 bytes (4 GB frame is enough!!!),
          # Check that first 4 bytes are 0000. If not then the frame is bigger
          # than 4 GB and must be rejected!

          if @buffer.read(4).unpack('N').first != 0
            log_system_notice "frame size bigger than 4 GB, rejected"
            send_close_frame 1008
            return false
          end

          @payload_length = @buffer.read(4).unpack('N').first

          @state = :masking_key
          true

        when :masking_key
          return false  if @buffer.size < 4

          # Get the masking key (4 bytes) and remove first 4 bytes
          # from the buffer.
          @masking_key = @buffer.read(4)

          @state = :check_frame
          true

        when :check_frame
          # All control frames MUST have a payload length of 125 bytes or
          # less and MUST NOT be fragmented.
          if control_frame? and @payload_length > 125
            log_system_notice "received invalid control frame (payload_length > 125), sending close frame"
            send_close_frame 1002
            return false
          end

          if control_frame? and not @fin
            log_system_notice "received invalid control frame (FIN=0), sending close frame"
            send_close_frame 1002, "forbidden FIN=0 in control frame"
            return false
          end

          # A continuation frame can only arrive if previously a text/binary frame
          # arrived with FIN=0.
          if continuation_frame? and not @msg_sym_opcode
            log_system_notice "invalid continuation frame received (no previous unfinished message), sending close frame"
            send_close_frame 1002, "invalid continuation frame received"
            return false
          end

          # If a previous frame had FIN=0 and opcode=text/binary, then it cannot arrive
          # a new frame with opcode=text/binary.
          if @msg_sym_opcode and text_or_binary_frame?
            log_system_notice "invalid text/binary frame received (expecting a continuation frame), sending close frame"
            send_close_frame 1002, "expected a continuation frame"
            return false
          end

          # Check max frame size.
          if @payload_length > @@max_frame_size
            send_close_frame 1009, "frame too big"
            return false
          end

          @state = :payload_data
          true

        when :payload_data
          return false  if @buffer.size < @payload_length

          unless @payload_length.zero?
            # NOTE: @payload will always be Encoding::BINARY
            @payload = ::OverSIP::WebSocket::FramingUtils.unmask @buffer.read(@payload_length), @masking_key
          end
          # NOTE: @payload could be nil.

          @state = :process_frame
          true

        when :process_frame
          # Set it here as it could be changed later in this block.
          @state = :init

          case @sym_opcode

          when :text
            log_system_debug "received text frame: FIN=#{@fin}, RSV1-3=#{@rsv1}/#{@rsv2}/#{@rsv3}, payload_length=#{@payload_length}"  if $oversip_debug

            # Store the opcode of the first frame (if there is more frames for same message
            # they will have opcode=continuation).
            @msg_sym_opcode = @sym_opcode

            # Reset the UTF8 validator.
            @utf8_validator.reset

            if @payload
              if (valid_utf8 = @utf8_validator.validate(@payload)) == false
                log_system_notice "received single text frame contains invalid UTF-8, closing the connection"
                send_close_frame 1007, "single text frame contains invalid UTF-8"
                return false
              end

              if @fin and not valid_utf8
                log_system_notice "received single text frame contains incomplete UTF-8, closing the connection"
                send_close_frame 1007, "single text frame contains incomplete UTF-8"
                return false
              end

              return false  unless @ws_app.receive_payload_data @payload
            end

            # If message is finished tell it to the WS application.
            if @fin
              @ws_app.message_done @msg_sym_opcode
              @msg_sym_opcode = nil
            end

          when :binary
            log_system_debug "received binary frame: FIN=#{@fin}, RSV1-3=#{@rsv1}/#{@rsv2}/#{@rsv3}, payload_length=#{@payload_length}"  if $oversip_debug

            # Store the opcode of the first frame (if there is more frames for same message
            # they will have opcode=continuation).
            @msg_sym_opcode = @sym_opcode

            if @payload
              return false  unless @ws_app.receive_payload_data @payload
            end

            # If message is finished tell it to the WS application.
            if @fin
              @ws_app.message_done @msg_sym_opcode
              @msg_sym_opcode = nil
            end

          when :continuation
            log_system_debug "received continuation frame: FIN=#{@fin}, RSV1-3=#{@rsv1}/#{@rsv2}/#{@rsv3}, payload_length=#{@payload_length}"  if $oversip_debug

            if @payload
              if @msg_sym_opcode == :text
                if (valid_utf8 = @utf8_validator.validate(@payload)) == false
                  log_system_notice "received continuation text frame contains invalid UTF-8, closing the connection"
                  send_close_frame 1007, "continuation text frame contains invalid UTF-8"
                  return false
                end

                if @fin and not valid_utf8
                  log_system_notice "received continuation final text frame contains incomplete UTF-8, closing the connection"
                  send_close_frame 1007, "continuation final text frame contains incomplete UTF-8"
                  return false
                end
              end

              return false  unless @ws_app.receive_payload_data @payload
            end

            # If message is finished tell it to the WS application.
            if @fin
              @ws_app.message_done @msg_sym_opcode
              @msg_sym_opcode = nil
            end

          when :close
            if @payload_length >= 2
              status = ""
              status << @payload.getbyte(0) << @payload.getbyte(1)
              status =  status.unpack('n').first
              if (reason = @payload[2..-1])
                # Reset the UTF8 validator.
                @utf8_validator.reset

                # The UTF-8 validator returns:
                # - true: Valid UTF-8 string.
                # - nil: Valid but not terminated UTF-8 string.
                # - false: Invalid UTF-8 string.
                # So it must be true for the close frame reason.
                unless @utf8_validator.validate(reason)
                  log_system_notice "received close frame with invalid UTF-8 data in the reason: status=#{status.inspect}"
                  send_close_frame 1007, "close frame reason contains incomplete UTF-8"
                  return false
                end
              end
            else
              status = nil
            end

            case status
            when 1002
              log_system_notice "received close frame due to WS protocol error: status=1002, reason=#{reason.inspect}"
            when 1003
              log_system_notice "received close frame due to sent data type: status=1003, reason=#{reason.inspect}"
            when 1007
              log_system_notice "received close frame due to non valid UTF-8 data sent: status=1007, reason=#{reason.inspect}"
            when 1009
              log_system_notice "received close frame due to too big message sent: status=1009, reason=#{reason.inspect}"
            when 1010
              log_system_notice "received close frame due to extensions negotiation failure: status=1010, reason=#{reason.inspect}"
            else
              log_system_debug "received close frame: status=#{status.inspect}, reason=#{reason.inspect}"  if $oversip_debug
            end

            send_close_frame nil, nil, true
            return false

          when :ping
            log_system_debug "received ping frame: payload_length=#{@payload_length}"  if $oversip_debug
            send_pong_frame @payload

          when :pong
            log_system_debug "received pong frame: payload_length=#{@payload_length}"  if $oversip_debug

          end

          true

        when :ws_closed
          false

        when :tcp_closed
          false

        end)
      end # while

    end  # receive_data


    def control_frame?
      @opcode > 2
    end


    def text_or_binary_frame?
      @opcode == 1 or @opcode == 2
    end


    def continuation_frame?
      @opcode == 0
    end


    # NOTE: A WS message is always set in a single WS frame.
    def send_text_frame message
      case @state
      when :ws_closed
        log_system_debug "cannot send text frame, WebSocket session is closed"  if $oversip_debug
        return false
      when :tcp_closed
        log_system_debug "cannot send text frame, TCP session is closed"  if $oversip_debug
        return false
      end
      log_system_debug "sending text frame: payload_length=#{message.bytesize}"  if $oversip_debug

      frame = "".encode ::Encoding::BINARY

      # byte1 = OPCODE_TO_INT[:text] | 0b10000000 => 129
      #
      # - FIN bit set.
      # - RSV1-3 bits not set.
      # - opcode = 1
      frame << 129

      length = message.bytesize
      if length <= 125
        frame << length # since rsv4 is 0
      elsif length < 65536 # write 2 byte length
        frame << 126
        frame << [length].pack('n')
      else # write 8 byte length
        frame << 127
        frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
      end

      if message.encoding == ::Encoding::BINARY
        frame << message
      else
        frame << message.force_encoding(::Encoding::BINARY)
      end

      @connection.send_data frame
      true
    end


    def send_binary_frame message
      case @state
      when :ws_closed
        log_system_debug "cannot send binary frame, WebSocket session is closed"  if $oversip_debug
        return false
      when :tcp_closed
        log_system_debug "cannot send binary frame, TCP session is closed"  if $oversip_debug
        return false
      end
      log_system_debug "sending binary frame: payload_length=#{message.bytesize}"  if $oversip_debug

      frame = "".encode ::Encoding::BINARY

      # byte1 = OPCODE_TO_INT[:binary] | 0b10000000 => 130
      #
      # - FIN bit set.
      # - RSV1-3 bits not set.
      # - opcode = 2
      frame << 130

      length = message.bytesize
      if length <= 125
        frame << length # since rsv4 is 0
      elsif length < 65536 # write 2 byte length
        frame << 126
        frame << [length].pack('n')
      else # write 8 byte length
        frame << 127
        frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
      end

      if message.encoding == ::Encoding::BINARY
        frame << message
      else
        frame << message.force_encoding(::Encoding::BINARY)
      end

      @connection.send_data frame
      true
    end


    def send_ping_frame data=nil
      case @state
      when :ws_closed
        log_system_debug "cannot send ping frame, WebSocket session is closed"  if $oversip_debug
        return false
      when :tcp_closed
        log_system_debug "cannot send ping frame, TCP session is closed"  if $oversip_debug
        return false
      end
      if data
        log_system_debug "sending ping frame: payload_length=#{data.bytesize}"  if $oversip_debug
      else
        log_system_debug "sending ping frame: payload_length=0"  if $oversip_debug
      end

      frame = "".encode ::Encoding::BINARY

      # byte1 = OPCODE_TO_INT[:ping] | 0b10000000 => 137
      #
      # - FIN bit set.
      # - RSV1-3 bits not set.
      # - opcode = 9
      frame << 137

      length = ( data ? data.bytesize : 0 )
      frame << length

      if data
        if data.encoding == ::Encoding::BINARY
          frame << data
        else
          frame << data.force_encoding(::Encoding::BINARY)
        end
      end

      @connection.send_data frame
      true
    end


    def send_pong_frame data=nil
      case @state
      when :ws_closed
        log_system_debug "cannot send pong frame, WebSocket session is closed"  if $oversip_debug
        return false
      when :tcp_closed
        log_system_debug "cannot send pong frame, TCP session is closed"  if $oversip_debug
        return false
      end
      if data
        log_system_debug "sending pong frame: payload_length=#{data.bytesize}"  if $oversip_debug
      else
        log_system_debug "sending pong frame: payload_length=0"  if $oversip_debug
      end

      frame = "".encode ::Encoding::BINARY

      # byte1 = OPCODE_TO_INT[:pong] | 0b10000000 => 138
      #
      # - FIN bit set.
      # - RSV1-3 bits not set.
      # - opcode = 10
      frame << 138

      length = ( data ? data.bytesize : 0 )
      frame << length

      if data
        if data.encoding == ::Encoding::BINARY
          frame << data
        else
          frame << data.force_encoding(::Encoding::BINARY)
        end
      end

      @connection.send_data frame
      true
    end


    def send_close_frame status=nil, reason=nil, in_reply_to_close=nil
      @keep_alive_timer.cancel  if @keep_alive_timer

      case @state
      when :ws_closed
        log_system_debug "cannot send close frame, WebSocket session is closed"  if $oversip_debug
        return false
      when :tcp_closed
        log_system_debug "cannot send close frame, TCP session is closed"  if $oversip_debug
        return false
      end

      unless in_reply_to_close
        log_system_notice "sending close frame: status=#{status.inspect}, reason=#{reason.inspect}"
      else
        log_system_debug "sending reply close frame: status=#{status.inspect}, reason=#{reason.inspect}"  if $oversip_debug
      end

      @state = :ws_closed
      @buffer.clear

      frame = "".encode ::Encoding::BINARY

      # byte1 = OPCODE_TO_INT[:close] | 0b10000000 => 136
      #
      # - FIN bit set.
      # - RSV1-3 bits not set.
      # - opcode = 8
      frame << 136
      if status
        length = ( reason ? 2 + reason.bytesize : 2 )
      else
        length = 0
      end

      frame << length # since rsv4 is 0
      if status
        frame << [status].pack('n')
        if reason
          if reason.encoding == ::Encoding::BINARY
            frame << reason
          else
            frame << reason.force_encoding(::Encoding::BINARY)
          end
        end
      end

      @connection.ignore_incoming_data
      @connection.send_data frame

      unless in_reply_to_close
        # Let's some time for the client to send us a close frame (it will
        # be ignored anyway) before closing the TCP connection.
        ::EM.add_timer(0.2) do
          @connection.close_connection_after_writing
        end
      else
        @connection.close_connection_after_writing
      end
      true
    end


    def tcp_closed
      @keep_alive_timer.cancel  if @keep_alive_timer
      @state = :tcp_closed
      # Tell it to the WS application.
      @ws_app.tcp_closed rescue nil
    end

  end  # class WsFraming

end