# frozen_string_literal: true

# Copyright 2016-2018 The NATS Authors
# 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.
#

module NATS
  module Protocol
    MSG = /\AMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?(\d+)\r\n/i
    HMSG = /\AHMSG\s+([^\s]+)\s+([^\s]+)\s+(([^\s]+)[^\S\r\n]+)?([\d]+)\s+(\d+)\r\n/i
    OK = /\A\+OK\s*\r\n/i
    ERR = /\A-ERR\s+('.+')?\r\n/i
    PING = /\APING\s*\r\n/i
    PONG = /\APONG\s*\r\n/i
    INFO = /\AINFO\s+([^\r\n]+)\r\n/i
    UNKNOWN = /\A(.*)\r\n/

    AWAITING_CONTROL_LINE = 1
    AWAITING_MSG_PAYLOAD = 2

    CR_LF = "\r\n"
    CR_LF_SIZE = CR_LF.bytesize

    PING_REQUEST = "PING#{CR_LF}".freeze
    PONG_RESPONSE = "PONG#{CR_LF}".freeze

    SUB_OP = "SUB"
    EMPTY_MSG = ""

    class Parser
      def initialize(nc)
        @nc = nc
        reset!
      end

      def reset!
        @buf = nil
        @parse_state = AWAITING_CONTROL_LINE

        @sub = nil
        @sid = nil
        @reply = nil
        @needed = nil
        @header_needed = nil
      end

      def parse(data)
        @buf = @buf ? @buf << data : data
        while @buf
          case @parse_state
          when AWAITING_CONTROL_LINE
            case @buf
            when MSG
              @buf = $'
              @sub, @sid, @reply, @needed = $1, $2.to_i, $4, $5.to_i
              @parse_state = AWAITING_MSG_PAYLOAD
            when HMSG
              @buf = $'
              @sub, @sid, @reply, @header_needed, @needed = $1, $2.to_i, $4, $5.to_i, $6.to_i
              @parse_state = AWAITING_MSG_PAYLOAD
            when OK # No-op right now
              @buf = $'
            when ERR
              @buf = $'
              @nc.send(:process_err, $1)
            when PING
              @buf = $'
              @nc.send(:process_ping)
            when PONG
              @buf = $'
              @nc.send(:process_pong)
            when INFO
              @buf = $'
              # First INFO message is processed synchronously on connect,
              # and onwards we would be receiving asynchronously INFO commands
              # signaling possible changes in the topology of the NATS cluster.
              @nc.send(:process_info, $1)
            when UNKNOWN
              @buf = $'
              @nc.send(:process_err, "Unknown protocol: #{$1}")
            else
              # If we are here we do not have a complete line yet that we understand.
              return
            end
            @buf = nil if @buf && @buf.empty?

          when AWAITING_MSG_PAYLOAD
            return unless @needed && @buf.bytesize >= (@needed + CR_LF_SIZE)
            if @header_needed
              hbuf = @buf.slice(0, @header_needed)
              payload = @buf.slice(@header_needed, (@needed - @header_needed))
              @nc.send(:process_msg, @sub, @sid, @reply, payload, hbuf)
            else
              @nc.send(:process_msg, @sub, @sid, @reply, @buf.slice(0, @needed), nil)
            end
            @buf = @buf.slice((@needed + CR_LF_SIZE), @buf.bytesize)

            @sub = @sid = @reply = @needed = @header_needed = nil
            @parse_state = AWAITING_CONTROL_LINE
            @buf = nil if @buf && @buf.empty?
          end
        end
      end
    end
  end
end
