# frozen_string_literal: true

module Puppet::Pops
module Types
  class PAbstractTimeDataType < PScalarType
    # @param from [AbstractTime] lower bound for this type. Nil or :default means unbounded
    # @param to [AbstractTime] upper bound for this type. Nil or :default means unbounded
    def initialize(from, to = nil)
      @from = convert_arg(from, true)
      @to = convert_arg(to, false)
      raise ArgumentError, "'from' must be less or equal to 'to'. Got (#{@from}, #{@to}" unless @from <= @to
    end

    # Checks if this numeric range intersects with another
    #
    # @param o [PNumericType] the range to compare with
    # @return [Boolean] `true` if this range intersects with the other range
    # @api public
    def intersect?(o)
      instance_of?(o.class) && !(@to < o.numeric_from || o.numeric_to < @from)
    end

    # Returns the lower bound of the numeric range or `nil` if no lower bound is set.
    # @return [Float,Integer]
    def from
      @from == -Float::INFINITY ? nil : @from
    end

    # Returns the upper bound of the numeric range or `nil` if no upper bound is set.
    # @return [Float,Integer]
    def to
      @to == Float::INFINITY ? nil : @to
    end

    # Same as #from but will return `-Float::Infinity` instead of `nil` if no lower bound is set.
    # @return [Float,Integer]
    def numeric_from
      @from
    end

    # Same as #to but will return `Float::Infinity` instead of `nil` if no lower bound is set.
    # @return [Float,Integer]
    def numeric_to
      @to
    end

    def hash
      @from.hash ^ @to.hash
    end

    def eql?(o)
      self.class == o.class && @from == o.numeric_from && @to == o.numeric_to
    end

    def unbounded?
      @from == -Float::INFINITY && @to == Float::INFINITY
    end

    def convert_arg(arg, min)
      case arg
      when impl_class
        arg
      when Hash
        impl_class.from_hash(arg)
      when nil, :default
        min ? -Float::INFINITY : Float::INFINITY
      when String
        impl_class.parse(arg)
      when Integer
        impl_class.new(arg * Time::NSECS_PER_SEC)
      when Float
        impl_class.new(arg * Time::NSECS_PER_SEC)
      else
        raise ArgumentError, "Unable to create a #{impl_class.name} from a #{arg.class.name}" unless arg.nil? || arg == :default

        nil
      end
    end

    # Concatenates this range with another range provided that the ranges intersect or
    # are adjacent. When that's not the case, this method will return `nil`
    #
    # @param o [PAbstractTimeDataType] the range to concatenate with this range
    # @return [PAbstractTimeDataType,nil] the concatenated range or `nil` when the ranges were apart
    # @api public
    def merge(o)
      if intersect?(o) || adjacent?(o)
        new_min = numeric_from <= o.numeric_from ? numeric_from : o.numeric_from
        new_max = numeric_to >= o.numeric_to ? numeric_to : o.numeric_to
        self.class.new(new_min, new_max)
      else
        nil
      end
    end

    def _assignable?(o, guard)
      instance_of?(o.class) && numeric_from <= o.numeric_from && numeric_to >= o.numeric_to
    end
  end

  class PTimespanType < PAbstractTimeDataType
    def self.register_ptype(loader, ir)
      create_ptype(loader, ir, 'ScalarType',
                   'from' => { KEY_TYPE => POptionalType.new(PTimespanType::DEFAULT), KEY_VALUE => nil },
                   'to' => { KEY_TYPE => POptionalType.new(PTimespanType::DEFAULT), KEY_VALUE => nil })
    end

    def self.new_function(type)
      @new_function ||= Puppet::Functions.create_loaded_function(:new_timespan, type.loader) do
        local_types do
          type 'Formats = Variant[String[2],Array[String[2], 1]]'
        end

        dispatch :from_seconds do
          param           'Variant[Integer,Float]', :seconds
        end

        dispatch :from_string do
          param           'String[1]', :string
          optional_param  'Formats', :format
        end

        dispatch :from_fields do
          param          'Integer', :days
          param          'Integer', :hours
          param          'Integer', :minutes
          param          'Integer', :seconds
          optional_param 'Integer', :milliseconds
          optional_param 'Integer', :microseconds
          optional_param 'Integer', :nanoseconds
        end

        dispatch :from_string_hash do
          param <<-TYPE, :hash_arg
            Struct[{
              string => String[1],
              Optional[format] => Formats
            }]
          TYPE
        end

        dispatch :from_fields_hash do
          param <<-TYPE, :hash_arg
            Struct[{
              Optional[negative] => Boolean,
              Optional[days] => Integer,
              Optional[hours] => Integer,
              Optional[minutes] => Integer,
              Optional[seconds] => Integer,
              Optional[milliseconds] => Integer,
              Optional[microseconds] => Integer,
              Optional[nanoseconds] => Integer
            }]
          TYPE
        end

        def from_seconds(seconds)
          Time::Timespan.new((seconds * Time::NSECS_PER_SEC).to_i)
        end

        def from_string(string, format = Time::Timespan::Format::DEFAULTS)
          Time::Timespan.parse(string, format)
        end

        def from_fields(days, hours, minutes, seconds, milliseconds = 0, microseconds = 0, nanoseconds = 0)
          Time::Timespan.from_fields(false, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds)
        end

        def from_string_hash(args_hash)
          Time::Timespan.from_string_hash(args_hash)
        end

        def from_fields_hash(args_hash)
          Time::Timespan.from_fields_hash(args_hash)
        end
      end
    end

    def generalize
      DEFAULT
    end

    def impl_class
      Time::Timespan
    end

    def instance?(o, guard = nil)
      o.is_a?(Time::Timespan) && o >= @from && o <= @to
    end

    DEFAULT = PTimespanType.new(nil, nil)
  end
end
end
