require 'strscan'

module WebSocket
  class Extensions

    class Parser
      TOKEN    = /([!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+)/
      NOTOKEN  = /([^!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z])/
      QUOTED   = /"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"])*)"/
      PARAM    = %r{#{TOKEN.source}(?:=(?:#{TOKEN.source}|#{QUOTED.source}))?}
      EXT      = %r{#{TOKEN.source}(?: *; *#{PARAM.source})*}
      EXT_LIST = %r{^#{EXT.source}(?: *, *#{EXT.source})*$}
      NUMBER   = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/

      ParseError = Class.new(ArgumentError)

      def self.parse_header(header)
        offers = Offers.new
        return offers if header == '' or header.nil?

        unless header =~ EXT_LIST
          raise ParseError, "Invalid Sec-WebSocket-Extensions header: #{header}"
        end

        scanner = StringScanner.new(header)
        value   = scanner.scan(EXT)

        until value.nil?
          params = value.scan(PARAM)
          name   = params.shift.first
          offer  = {}

          params.each do |key, unquoted, quoted|
            if unquoted
              data = unquoted
            elsif quoted
              data = quoted.gsub(/\\/, '')
            else
              data = true
            end
            if data != true and data =~ NUMBER
              data = data =~ /\./ ? data.to_f : data.to_i(10)
            end

            if offer.has_key?(key)
              offer[key] = [*offer[key]] + [data]
            else
              offer[key] = data
            end
          end

          offers.push(name, offer)

          scanner.scan(/ *, */)
          value = scanner.scan(EXT)
        end
        offers
      end

      def self.serialize_params(name, params)
        values = []

        print = lambda do |key, value|
          case value
          when Array   then value.each { |v| print[key, v] }
          when true    then values.push(key)
          when Numeric then values.push(key + '=' + value.to_s)
          else
            if value =~ NOTOKEN
              values.push(key + '="' + value.gsub(/"/, '\"') + '"')
            else
              values.push(key + '=' + value)
            end
          end
        end

        params.keys.sort.each { |key| print[key, params[key]] }

        ([name] + values).join('; ')
      end
    end

    class Offers
      def initialize
        @by_name  = {}
        @in_order = []
      end

      def push(name, params)
        @by_name[name] ||= []
        @by_name[name].push(params)
        @in_order.push(:name => name, :params => params)
      end

      def each_offer(&block)
        @in_order.each do |offer|
          block.call(offer[:name], offer[:params])
        end
      end

      def by_name(name)
        @by_name[name] || []
      end

      def to_a
        @in_order.dup
      end
    end

  end
end
