module HyperStore
  class StateWrapper < BaseStoreClass
    module ArgumentValidator
      class InvalidOptionError < StandardError; end

      def validate_args!(klass, *args, &block)
        name, initial_value, opts = parse_arguments(*args, &block)

        opts[:scope]     ||= default_scope(klass)
        opts[:initializer] = validate_initializer(initial_value, klass, opts)
        opts[:block]       = block if block

        if opts[:reader]
          opts[:reader] = opts[:reader] == true ? name : opts[:reader]
        end

        [name, opts]
      end

      private

      def invalid_option(message)
        raise InvalidOptionError, message
      end

      # Parses the arguments given to get the name, initial_value (if any), and options
      def parse_arguments(*args)
        # If the only argument is a hash, the first key => value is name => inital_value
        if args.first.is_a?(Hash)
          # If the first key passed in is not the name, raise an error
          if [:reader, :initializer, :scope].include?(args.first.keys.first.to_sym)
            message = 'The name of the state must be specified first as '\
                      "either 'state :name' or 'state name: nil'"
            invalid_option(message)
          end

          name, initial_value = args[0].shift
        # Otherwise just the name is passed in by itself first
        else
          name = args.shift
        end

        # [name, initial_value (can be nil), args (if nil then return an empty hash)]
        [name, initial_value, args[0] || {}]
      end

      # Converts the initialize option to a Proc
      def validate_initializer(initial_value, klass, opts) # rubocop:disable Metrics/MethodLength
        # If we pass in the name as a hash with a value ex: state foo: :bar,
        # we just put that value inside a Proc and return that
        if initial_value != nil
          dup_or_return_intial_value(initial_value)
        # If we pass in the initialize option
        elsif opts[:initializer]
          # If it's a Symbol we convert to to a Proc that calls the method on the instance
          if [Symbol, String].include?(opts[:initializer].class)
            method_name = opts[:initializer]
            if [:class, :shared].include?(opts[:scope])
              -> { klass.send(:"#{method_name}") }
            else
              ->(instance) { instance.send(:"#{method_name}") }
            end
          # If it is already a Proc we do nothing and just return what was given
          elsif opts[:initializer].is_a?(Proc)
            opts[:initializer]
          # If it's not a Proc or a String we raise an error and return an empty Proc
          else
            invalid_option("'state' option 'initialize' must either be a Symbol or a Proc")
            -> {}
          end
        # Otherwise if it's not specified we just return an empty Proc
        else
          -> {}
        end
      end

      # Dup the initial value if possible, otherwise just return it
      # Ruby has no nice way of doing this...
      def dup_or_return_intial_value(value)
        value =
          begin
            value.dup
          rescue
            value
          end

        -> { value }
      end
    end
  end
end