# RJR JSON-RPC Argument Representation
#
# Copyright (C) 2014 Mohammed Morsi <mo@morsi.org>
# Licensed under the Apache License, Version 2.0

require 'forwardable'

module RJR

# Encapsulates a list of JSON-RPC method arguments as sent
# from the client to the server, in addition to providing
# various helper / utility methods.
class Arguments
  include Enumerable
  extend Forwardable

  attr_accessor :args

  def_delegators :@args, :each, :<<, :length, :[], :index

  def initialize(args={})
    @args = args[:args] || []
  end

  # Validate arguments against acceptable values.
  #
  # Raises error if value is found which is not on
  # list of acceptable values.
  #
  # If acceptable values are hash's, keys are compared
  # and on matches the values are used as the # of following
  # arguments to skip in the validator
  #
  # *Note* args / acceptable params are converted to strings
  # before comparison
  #
  # @example
  #   args = Arguments.new :args => ['custom', 'another']
  #   args.validate! 'custom', 'another'  #=> nil
  #   args.validate! 'something'          #=> ArgumentError
  #
  #   args = Arguments.new :args => ['with_id', 123, 'another']
  #   args.validate! :with_id => 1, :another => 0 #=> nil
  #   args.validate! :with_id => 1                #=> ArgumentError
  def validate!(*acceptable)
    i = 0
    if acceptable.first.is_a?(Hash)
      # clone acceptable hash, swap keys for string
      acceptable = Hash[acceptable.first]
      acceptable.keys.each { |k|
        acceptable[k.to_s] = acceptable[k]
        acceptable.delete(k) unless k.is_a?(String)
      }

      # compare acceptable against arguments, raising error if issue found
      while(i < length) do
        val = self[i]
        passed = acceptable.has_key?(val)
        raise ArgumentError, "#{val} not an acceptable arg" unless passed
        skip = acceptable[val]
        i += (skip + 1)
      end

    else
      # clone acceptable array, swap values for string
      acceptable = Array.new(acceptable).map { |a| a.to_s }

      # compare acceptable against arguments, raising error if issue found
      while(i < length) do
        val = self[i].to_s
        passed = acceptable.include?(val)
        raise ArgumentError, "#{val} not an acceptable arg" unless passed
        i += 1
      end
    end

    nil
  end

  # Extract groups of values from argument list
  #
  # Groups are generated by comparing arguments to keys in the
  # specified map and on matches extracting the # of following
  # arguments specified by the map values
  #
  # Note arguments / keys are converted to strings before comparison
  #
  # @example
  #   args = Arguments.new :args => ['with_id', 123, 'custom', 'another']
  #   args.extract :with_id => 1, :custom => 0
  #     # => [['with_id', 123], ['custom']]
  #
  def extract(map)
    # clone map hash, swap keys for string
    map = Hash[map]
    map.keys.each { |k|
      map[k.to_s] = map[k]
      map.delete(k) unless k.is_a?(String)
    }

    groups = []
    i = 0
    while(i < length) do
      val = self[i]
      i += 1
      next unless !!map.has_key?(val)

      num = map[val]
      group = [val]
      0.upto(num-1) do |j|
        group << self[i]
        i += 1
      end
      groups << group
    end

    groups
  end

  # Return boolean if tag appears in argument list.
  # Simple wrapper around includes setting up the scope
  # of argument 'specifiers'
  #
  # @example
  #   args = Arguments.new :args => ['foobar']
  #   args.specifies?('foobar') #=> true
  #   args.specifies?('barfoo') #=> false
  def specifies?(tag)
    include?(tag)
  end

  # Return specifier corresponding given tag. The specifier
  # is defined as the value appearing in the argument list
  # immediately after the tag
  #
  # @example
  #   args = Argument.new :args => ['with_id', 123]
  #   args.specifier_for('with_id') #=> 123
  #   args.specifier_for('other')   #=> nil
  def specifier_for(tag)
    return nil unless specifies?(tag)
    self[index(tag) + 1]
  end
end
end
