module WebSocket
  class Extensions

    autoload :Parser, File.expand_path('../extensions/parser', __FILE__)

    ExtensionError = Class.new(ArgumentError)

    MESSAGE_OPCODES = [1, 2]

    def initialize
      @rsv1 = @rsv2 = @rsv3 = nil

      @by_name  = {}
      @in_order = []
      @sessions = []
      @index    = {}
    end

    def add(ext)
      unless ext.respond_to?(:name) and ext.name.is_a?(String)
        raise TypeError, 'extension.name must be a string'
      end

      unless ext.respond_to?(:type) and ext.type == 'permessage'
        raise TypeError, 'extension.type must be "permessage"'
      end

      unless ext.respond_to?(:rsv1) and [true, false].include?(ext.rsv1)
        raise TypeError, 'extension.rsv1 must be true or false'
      end

      unless ext.respond_to?(:rsv2) and [true, false].include?(ext.rsv2)
        raise TypeError, 'extension.rsv2 must be true or false'
      end

      unless ext.respond_to?(:rsv3) and [true, false].include?(ext.rsv3)
        raise TypeError, 'extension.rsv3 must be true or false'
      end

      if @by_name.has_key?(ext.name)
        raise TypeError, %Q{An extension with name "#{ext.name}" is already registered}
      end

      @by_name[ext.name] = ext
      @in_order.push(ext)
    end

    def generate_offer
      sessions = []
      offer    = []
      index    = {}

      @in_order.each do |ext|
        session = ext.create_client_session
        next unless session

        record = [ext, session]
        sessions.push(record)
        index[ext.name] = record

        offers = session.generate_offer
        offers = offers ? [offers].flatten : []

        offers.each do |off|
          offer.push(Parser.serialize_params(ext.name, off))
        end
      end

      @sessions = sessions
      @index    = index

      offer.size > 0 ? offer.join(', ') : nil
    end

    def activate(header)
      responses = Parser.parse_header(header)
      @sessions = []

      responses.each_offer do |name, params|
        unless record = @index[name]
          raise ExtensionError, %Q{Server sent am extension response for unknown extension "#{name}"}
        end

        ext, session = *record

        if reserved = reserved?(ext)
          raise ExtensionError, %Q{Server sent two extension responses that use the RSV#{reserved[0]} } +
                               %Q{ bit: "#{reserved[1]}" and "#{ext.name}"}
        end

        unless session.activate(params) == true
          raise ExtensionError, %Q{Server send unacceptable extension parameters: #{Parser.serialize_params(name, params)}}
        end

        reserve(ext)
        @sessions.push(record)
      end
    end

    def generate_response(header)
      offers   = Parser.parse_header(header)
      sessions = []
      response = []

      @in_order.each do |ext|
        offer = offers.by_name(ext.name)
        next if offer.empty? or reserved?(ext)

        next unless session = ext.create_server_session(offer)

        reserve(ext)
        sessions.push([ext, session])
        response.push(Parser.serialize_params(ext.name, session.generate_response))
      end

      @sessions = sessions
      response.size > 0 ? response.join(', ') : nil
    end

    def valid_frame_rsv(frame)
      allowed = {:rsv1 => false, :rsv2 => false, :rsv3 => false}

      if MESSAGE_OPCODES.include?(frame.opcode)
        @sessions.each do |ext, session|
          allowed[:rsv1] ||= ext.rsv1
          allowed[:rsv2] ||= ext.rsv2
          allowed[:rsv3] ||= ext.rsv3
        end
      end

      (allowed[:rsv1] || !frame.rsv1) &&
      (allowed[:rsv2] || !frame.rsv2) &&
      (allowed[:rsv3] || !frame.rsv3)
    end
    alias :valid_frame_rsv? :valid_frame_rsv

    def process_incoming_message(message)
      @sessions.reverse.inject(message) do |msg, (ext, session)|
        begin
          session.process_incoming_message(msg)
        rescue => error
          raise ExtensionError, [ext.name, error.message].join(': ')
        end
      end
    end

    def process_outgoing_message(message)
      @sessions.inject(message) do |msg, (ext, session)|
        begin
          session.process_outgoing_message(msg)
        rescue => error
          raise ExtensionError, [ext.name, error.message].join(': ')
        end
      end
    end

    def close
      return unless @sessions

      @sessions.each do |ext, session|
        session.close rescue nil
      end
    end

  private

    def reserve(ext)
      @rsv1 ||= ext.rsv1 && ext.name
      @rsv2 ||= ext.rsv2 && ext.name
      @rsv3 ||= ext.rsv3 && ext.name
    end

    def reserved?(ext)
      return [1, @rsv1] if @rsv1 and ext.rsv1
      return [2, @rsv2] if @rsv2 and ext.rsv2
      return [3, @rsv3] if @rsv3 and ext.rsv3
      false
    end

  end
end
