module Hyperstack
  module Internal
    module Store
      class StateWrapper < BaseStoreClass
        module ArgumentValidator
          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 Legacy::Store::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
  end
end
