# frozen_string_literal: true

module Doing
  # Chronify methods for strings
  module ChronifyString
    ##
    ## Converts input string into a Time object when input
    ## takes on the following formats:
    ##             - interval format e.g. '1d2h30m', '45m'
    ##               etc.
    ##             - a semantic phrase e.g. 'yesterday
    ##               5:30pm'
    ##             - a strftime e.g. '2016-03-15 15:32:04
    ##               PDT'
    ##
    ## @param      options  Additional options
    ##
    ## @option options :future [Boolean] assume future date
    ##                                   (default: false)
    ##
    ## @option options :guess  [Symbol] :begin or :end to
    ##                                   assume beginning or end of
    ##                                   arbitrary time range
    ##
    ## @return     [DateTime] result
    ##
    def chronify(**options)
      now = Time.now
      raise Errors::InvalidTimeExpression, "Invalid time expression #{inspect}" if to_s.strip == ''

      secs_ago = if match(/^(\d+)$/)
                   # plain number, assume minutes
                   Regexp.last_match(1).to_i * 60
                 elsif (m = match(/^(?:(?<day>\d+)d)? *(?:(?<hour>\d+)h)? *(?:(?<min>\d+)m)?$/i))
                   # day/hour/minute format e.g. 1d2h30m
                   [[m['day'], 24 * 3600],
                    [m['hour'], 3600],
                    [m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+)
                 end

      if secs_ago
        res = now - secs_ago
        Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res} (#{secs_ago} seconds ago)))
      else
        date_string = dup
        date_string = 'today' if date_string.match(Types::REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
        date_string = "#{options[:context].to_s} #{date_string}" if date_string =~ Types::REGEX_TIME && options[:context]

        res = Chronic.parse(date_string, {
                              guess: options.fetch(:guess, :begin),
                              context: options.fetch(:future, false) ? :future : :past,
                              ambiguous_time_range: 8
                            })

        Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res}))
      end

      res
    end

    ##
    ## Converts simple strings into seconds that can be
    ## added to a Time object
    ##
    ## Input string can be HH:MM or XX[dhm][[XXhm][XXm]]
    ## (1d2h30m, 45m, 1.5d, 1h20m, etc.)
    ##
    ## @return     [Integer] seconds
    ##
    def chronify_qty
      minutes = 0
      case self.strip
      when /^(\d+):(\d\d)$/
        minutes += Regexp.last_match(1).to_i * 60
        minutes += Regexp.last_match(2).to_i
      when /^(\d+(?:\.\d+)?)([hmd])?/
        scan(/(\d+(?:\.\d+)?)([hmd])?/).each do |m|
          amt = m[0]
          type = m[1].nil? ? 'm' : m[1]

          minutes += case type.downcase
                     when 'm'
                       amt.to_i
                     when 'h'
                       (amt.to_f * 60).round
                     when 'd'
                       (amt.to_f * 60 * 24).round
                     else
                       0
                     end
        end
      end
      minutes * 60
    end

    ##
    ## Convert DD:HH:MM to seconds
    ##
    ## @return     [Integer] rounded number of seconds
    ##
    def to_seconds
      mtch = match(/(\d+):(\d+):(\d+)/)

      raise Errors::DoingRuntimeError, "Invalid time string: #{self}" unless mtch

      h = mtch[1]
      m = mtch[2]
      s = mtch[3]
      (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
    end

    ##
    ## Convert DD:HH:MM to a natural language string
    ##
    ## @param      format  [Symbol] The format to output (:dhm, :hm, :m, :clock, :natural)
    ##
    def time_string(format: :dhm)
      to_seconds.time_string(format: format)
    end

    ##
    ## Convert (chronify) natural language dates
    ## within configured date tags (tags whose value is
    ## expected to be a date). Modifies string in place.
    ##
    ## @param      additional_tags  [Array] An array of
    ##                              additional tags to
    ##                              consider date_tags
    ##
    def expand_date_tags(additional_tags = nil)
      iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/

      watch_tags = [
        'start(?:ed)?',
        'beg[ia]n',
        'done',
        'finished',
        'completed?',
        'waiting',
        'defer(?:red)?'
      ]

      if additional_tags
        date_tags = additional_tags
        date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
        date_tags.map! do |tag|
          tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
        end
        watch_tags.concat(date_tags).uniq!
      end

      done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i

      gsub!(done_rx) do
        m = Regexp.last_match
        t = m['tag']
        d = m['date']
        future = t =~ /^(done|complete)/ ? false : true
        parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
        parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
      end
    end

    def is_range?
      self =~ / (to|through|thru|(un)?til|-+) /
    end

    ##
    ## Splits a range string and returns an array of
    ## DateTime objects as [start, end]. If only one date is
    ## given, end time is nil.
    ##
    ## @return     [Array<DateTime>] Start and end dates as
    ##             array
    ## @example    Process a natural language date range
    ##   "mon 3pm to mon 5pm".split_date_range
    ##
    def split_date_range
      time_rx = /^(\d{1,2}(:\d{1,2})?( *(am|pm))?|midnight|noon)$/
      range_rx = / (to|through|thru|(?:un)?til|-+) /

      date_string = dup

      if date_string.is_range?
        # Do we want to differentiate between "to" and "through"?
        # inclusive = date_string =~ / (through|thru|-+) / ? true : false
        inclusive = true

        dates = date_string.split(range_rx)

        if dates[0].strip =~ time_rx && dates[-1].strip =~ time_rx
          start = dates[0].strip
          finish = dates[-1].strip
        else
          start = dates[0].chronify(guess: :begin, future: false)
          finish = dates[-1].chronify(guess: inclusive ? :end : :begin, future: true)
        end

        raise Errors::InvalidTimeExpression, "Unrecognized date string (#{dates[0]})" if start.nil?

        raise Errors::InvalidTimeExpression, "Unrecognized date string (#{dates[-1]})" if finish.nil?

      else
        if date_string.strip =~ time_rx
          start = date_string.strip
          finish = '11:59pm'
        else
          start = date_string.strip.chronify(guess: :begin, future: false)
          finish = date_string.strip.chronify(guess: :end)
        end
        raise Errors::InvalidTimeExpression, 'Unrecognized date string' unless start

      end


      if start.is_a? String
        Doing.logger.debug('Parser:', "--from string interpreted as time span, from #{start || '12am'} to #{finish || '11:59pm'}")
      else
        Doing.logger.debug('Parser:', "date range interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
      end
      [start, finish]
    end
  end
end