# frozen_string_literal: true

require "pbt/arbitrary/arbitrary"
require "pbt/arbitrary/constant"
require "pbt/arbitrary/constant_arbitrary"
require "pbt/arbitrary/array_arbitrary"
require "pbt/arbitrary/integer_arbitrary"
require "pbt/arbitrary/tuple_arbitrary"
require "pbt/arbitrary/fixed_hash_arbitrary"
require "pbt/arbitrary/choose_arbitrary"
require "pbt/arbitrary/one_of_arbitrary"
require "pbt/arbitrary/map_arbitrary"
require "pbt/arbitrary/filter_arbitrary"

module Pbt
  module Arbitrary
    module ArbitraryMethods
      # For integers between min (included) and max (included).
      #
      # @param min [Integer] Lower limit for the generated integers (included).
      # @param max [Integer] Upper limit for the generated integers (included).
      # @return [Arbitrary<Integer>]
      def integer(min: -1000000, max: 1000000)
        IntegerArbitrary.new(min, max)
      end

      # For natural numbers (non-negative integers) between 0 (included) and max (included).
      #
      # @param max [Integer] Upper limit for the generated integers (included).
      # @return [Arbitrary<Integer>]
      def nat(max: nil)
        integer(min: 0, max: max)
      end

      # For arrays of values generated by `arb`.
      #
      # @param arb [Arbitrary<T>] Arbitrary used to generate the values of the array.
      # @param min [Integer] Lower limit of the generated array size.
      # @param max [Integer] Upper limit of the generated array size.
      # @param empty [Boolean] Whether the array can be empty or not.
      # @return [Arbitrary<T>]
      def array(arb, min: 0, max: 10, empty: true)
        raise ArgumentError if min < 0
        min = 1 if min.zero? && !empty

        ArrayArbitrary.new(arb, min, max)
      end

      # For tuples of values generated by `arbs`.
      #
      # @param arbs [Array<Arbitrary<...T>>] Arbitraries used to generate the values of the tuple.
      # @return [Arbitrary<Array<...T>>]
      def tuple(*arbs)
        TupleArbitrary.new(*arbs)
      end

      # For fixed hashes of values generated by `hash`.
      #
      # @example
      #   arb = Pbt.fixed_hash(x: Pbt.integer, y: Pbt.integer)
      #   arb.generate(Random.new) # => {x: -450108, y: 42}
      #
      # @param hash [Hash<Object, Arbitrary<T>>] Hash with any keys and arbitraries as values.
      # @return [Arbitrary<Hash<Object, T>>]
      def fixed_hash(hash)
        FixedHashArbitrary.new(hash)
      end

      # Picks a random integer in the given range.
      #
      # @see Pbt.one_of
      # @param range [Range<Integer>] Range of integers to choose from.
      # @return [Arbitrary<Integer>]
      def choose(range)
        ChooseArbitrary.new(range)
      end

      # Picks a random element from the given choices.
      # The choices can be of any type.
      #
      # @see Pbt.one_of
      # @param choices [Array<Object>] Array of choices.
      # @return [Arbitrary<Object>]
      def one_of(*choices)
        OneOfArbitrary.new(choices)
      end

      # For a lowercase hexadecimal character.
      #
      # @return [Arbitrary<String>]
      def hexa
        one_of(*HEXA_CHARS)
      end

      # For lowercase hexadecimal strings.
      #
      # @see Pbt.array
      # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
      # @return [Arbitrary<String>]
      def hexa_string(**kwargs)
        array(hexa, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
      end

      # For a single unicode character (including printable and non-printable).
      #
      # @return [Arbitrary<String>]
      def char
        choose(CHAR_RANGE).map(CHAR_MAPPER, CHAR_UNMAPPER)
      end

      # For an alphanumeric character.
      #
      # @return [Arbitrary<String>]
      def alphanumeric_char
        one_of(*ALPHANUMERIC_CHARS)
      end

      # For alphanumeric strings.
      #
      # @see Pbt.array
      # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
      # @return [Arbitrary<String>]
      def alphanumeric_string(**kwargs)
        array(alphanumeric_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
      end

      # For an ascii character.
      #
      # @return [Arbitrary<String>]
      def ascii_char
        one_of(*ASCII_CHARS)
      end

      # For ascii strings.
      #
      # @see Pbt.array
      # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
      # @return [Arbitrary<String>]
      def ascii_string(**kwargs)
        array(ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
      end

      # For a printable ascii character.
      #
      # @return [Arbitrary<String>]
      def printable_ascii_char
        one_of(*PRINTABLE_ASCII_CHARS)
      end

      # For printable ascii strings.
      #
      # @see Pbt.array
      # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
      # @return [Arbitrary<String>]
      def printable_ascii_string(**kwargs)
        array(printable_ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
      end

      # For a printable character.
      #
      # @return [Arbitrary<String>]
      def printable_char
        one_of(*PRINTABLE_CHARS)
      end

      # For printable strings.
      #
      # @see Pbt.array
      # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
      # @return [Arbitrary<String>]
      def printable_string(**kwargs)
        array(printable_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER)
      end

      # For symbols.
      #
      # @see Pbt.array
      # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
      # @return [Arbitrary<Symbol>]
      def symbol(**kwargs)
        array(one_of(*SYMBOL_SAFE_CHARS), empty: false, **kwargs).map(SYMBOL_MAPPER, SYMBOL_UNMAPPER)
      end

      # For floats.
      #
      # @return [Arbitrary<Float>]
      def float
        tuple(integer, integer).map(FLOAT_MAPPER, FLOAT_UNMAPPER)
      end

      # For symbols.
      #
      # @param arb [Arbitrary<T>] Arbitrary used to generate the values of the array.
      # @param min [Integer] Lower limit of the generated set size.
      # @param max [Integer] Upper limit of the generated set size.
      # @param empty [Boolean] Whether the array can be empty or not.
      # @return [Arbitrary<T>]
      def set(arb, min: 0, max: 10, empty: true)
        array(arb, min: min, max: max, empty: empty).map(SET_MAPPER, SET_UNMAPPER)
      end

      # For hashes of any keys and values.
      # If you want to call `Object#hash` for `Pbt`, call this method without arguments.
      #
      # @example
      #   hash_generator = Pbt.hash(Pbt.symbol, Pbt.integer)
      #   hash_generator.generate(Random.new) # => {:buo=>466214, :cwftzvglq=>331431, :wweccnzg=>-848867}
      #
      # @see Pbt.array
      # @param args [Array<Arbitrary<T,U>>] Arbitraries to generate Hash. First one is for key and second is for value.
      # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
      # @return [Arbitrary<Hash<T,U>>]
      def hash(*args, **kwargs)
        if args.size == 2
          key_arbitrary, value_arbitrary = args
          array(tuple(key_arbitrary, value_arbitrary), **kwargs).map(HASH_MAPPER, HASH_UNMAPPER)
        else
          super # call `Object#hash`
        end
      end

      # For booleans.
      #
      # @return [Arbitrary<Boolean>]
      def boolean
        one_of(true, false)
      end

      # For any constant values.
      # It's useful when you want to use a constant value that behaves like an arbitrary.
      #
      # @example
      #   Pbt.constant(42).generate(Random.new) # => 42
      #
      # @param val [Object]
      # @return [Arbitrary<Object>]
      def constant(val)
        ConstantArbitrary.new(val)
      end

      # For nil.
      #
      # @return [Arbitrary<nil>]
      def nil
        constant(nil)
      end

      # For dates between `base_date + past_offset_days` and `base_date + future_offset_days`.
      #
      # @param base_date [Date] Base date for the generated dates.
      # @param past_offset_days [Integer] Offset days for the past. Default is -18250 (about 50 years).
      # @param future_offset_days [Integer] Offset days for the future. Default is 18250 (about 50 years).
      # @return [Arbitrary<Date>]
      def date(base_date: Date.today, past_offset_days: -18250, future_offset_days: 18250)
        offset_arb = integer(min: past_offset_days, max: future_offset_days)
        offset_arb.map(DATE_MAPPER.call(base_date), DATE_UNMAPPER.call(base_date))
      end

      # For past dates between `base_date - past_offset_days` and `base_date`.
      #
      # @param base_date [Date] Base date for the generated dates.
      # @param past_offset_days [Integer] Offset days for the past. Default is -18250 (about 50 years).
      # @return [Arbitrary<Date>]
      def past_date(base_date: Date.today, past_offset_days: -18250)
        date(base_date: base_date, past_offset_days: past_offset_days, future_offset_days: 0)
      end

      # For future dates between `base_date` and `base_date - future_offset_days`.
      #
      # @param base_date [Date] Base date for the generated dates.
      # @param future_offset_days [Integer] Offset days for the future. Default is 18250 (about 50 years).
      # @return [Arbitrary<Date>]
      def future_date(base_date: Date.today, future_offset_days: 18250)
        date(base_date: base_date, past_offset_days: 0, future_offset_days: future_offset_days)
      end

      # For times between `base_time + past_offset_seconds` and `base_time + future_offset_seconds`.
      #
      # @param base_time [Date] Base time for the generated times.
      # @param past_offset_seconds [Integer] Offset seconds for the past. Default is -1576800000 (about 50 years).
      # @param future_offset_seconds [Integer] Offset seconds for the future. Default is 1576800000 (about 50 years).
      # @return [Arbitrary<Time>]
      def time(base_time: Time.now, past_offset_seconds: -1576800000, future_offset_seconds: 1576800000)
        offset_arb = integer(min: past_offset_seconds, max: future_offset_seconds)
        offset_arb.map(TIME_MAPPER.call(base_time), TIME_UNMAPPER.call(base_time))
      end

      # For past times between `base_time + past_offset_seconds` and `base_time`
      #
      # @param base_time [Date] Base time for the generated times.
      # @param past_offset_seconds [Integer] Offset seconds for the past. Default is -1576800000 (about 50 years).
      # @return [Arbitrary<Time>]
      def past_time(base_time: Time.now, past_offset_seconds: -1576800000)
        time(base_time: base_time, past_offset_seconds: past_offset_seconds, future_offset_seconds: 0)
      end

      # For future times between `base_time` and `base_time + future_offset_seconds`.
      #
      # @param base_time [Date] Base time for the generated times.
      # @param future_offset_seconds [Integer] Offset seconds for the future. Default is 1576800000 (about 50 years).
      # @return [Arbitrary<Time>]
      def future_time(base_time: Time.now, future_offset_seconds: 1576800000)
        time(base_time: base_time, past_offset_seconds: 0, future_offset_seconds: future_offset_seconds)
      end
    end
  end
end
