require 'bindata/lazy'

module BinData
  module AcceptedParametersPlugin
    # Mandatory parameters must be present when instantiating a data object.
    def mandatory_parameters(*args)
      accepted_parameters.mandatory(*args)
    end

    # Optional parameters may be present when instantiating a data object.
    def optional_parameters(*args)
      accepted_parameters.optional(*args)
    end

    # Default parameters can be overridden when instantiating a data object.
    def default_parameters(*args)
      accepted_parameters.default(*args)
    end

    # Mutually exclusive parameters may not all be present when
    # instantiating a data object.
    def mutually_exclusive_parameters(*args)
      accepted_parameters.mutually_exclusive(*args)
    end

    alias mandatory_parameter mandatory_parameters
    alias optional_parameter  optional_parameters
    alias default_parameter   default_parameters

    def accepted_parameters #:nodoc:
      @accepted_parameters ||= begin
        ancestor_params = superclass.respond_to?(:accepted_parameters) ?
                            superclass.accepted_parameters : nil
        AcceptedParameters.new(ancestor_params)
      end
    end

    # BinData objects accept parameters when initializing.  AcceptedParameters
    # allow a BinData class to declaratively identify accepted parameters as
    # mandatory, optional, default or mutually exclusive.
    class AcceptedParameters
      def initialize(ancestor_parameters = nil)
        if ancestor_parameters
          @mandatory = ancestor_parameters.mandatory.dup
          @optional  = ancestor_parameters.optional.dup
          @default   = ancestor_parameters.default.dup
          @mutually_exclusive = ancestor_parameters.mutually_exclusive.dup
        else
          @mandatory = []
          @optional  = []
          @default   = Hash.new
          @mutually_exclusive = []
        end
      end

      def mandatory(*args)
        unless args.empty?
          @mandatory.concat(to_syms(args))
          @mandatory.uniq!
        end
        @mandatory
      end

      def optional(*args)
        unless args.empty?
          @optional.concat(to_syms(args))
          @optional.uniq!
        end
        @optional
      end

      def default(args = nil)
        if args
          to_syms(args.keys)  # call for side effect of validating names
          args.each_pair do |param, value|
            @default[param.to_sym] = value
          end
        end
        @default
      end

      def mutually_exclusive(*args)
        arg1 = args.shift
        until args.empty?
          args.each do |arg2|
            @mutually_exclusive.push([arg1.to_sym, arg2.to_sym])
            @mutually_exclusive.uniq!
          end
          arg1 = args.shift
        end
        @mutually_exclusive
      end

      def all
        (@mandatory + @optional + @default.keys).uniq
      end

      #---------------
      private

      def to_syms(args)
        syms = args.collect(&:to_sym)
        ensure_valid_names(syms)
        syms
      end

      def ensure_valid_names(names)
        invalid_names = self.class.invalid_parameter_names
        names.each do |name|
          if invalid_names.include?(name)
            raise NameError.new("Rename parameter '#{name}' " \
                                "as it shadows an existing method.", name)
          end
        end
      end

      def self.invalid_parameter_names
        @invalid_names ||= begin
          all_names = LazyEvaluator.instance_methods(true) + Kernel.methods
          allowed_names = [:name, :type]
          invalid_names = (all_names - allowed_names).uniq

          Hash[*invalid_names.collect { |key| [key.to_sym, true] }.flatten]
        end
      end
    end
  end
end
