module BBLib
  # Parses known time based patterns out of a string to construct a numeric duration.
  def self.parse_duration(str, output: :sec, min_interval: :sec)
    msecs = 0.0

    # Parse time expressions such as 04:05.
    # The argument min_interval controls what time interval the final number represents
    str.scan(/\d+\:[\d+\:]+\d+/).each do |e|
      keys = TIME_EXPS.keys
      position = keys.index(min_interval)
      e.split(':').reverse.each do |sec|
        key = keys[position]
        msecs+= sec.to_f * TIME_EXPS[key][:mult]
        position+=1
      end
    end

    # Parse expressions such as '1m' or '1 min'
    TIME_EXPS.each do |_k, v|
      v[:exp].each do |e|
        numbers = str.downcase.scan(/(?=\w|\D|\A)\d*\.?\d+[[:space:]]*#{e}(?=\W|\d|\z)/i)
        numbers.each do |n|
          msecs+= n.to_f * v[:mult]
        end
      end
    end
    msecs / TIME_EXPS[output][:mult]
  end

  # Turns a numeric input into a time string.
  def self.to_duration(num, input: :sec, stop: :milli, style: :medium)
    return nil unless num.is_a?(Numeric)
    return '0' if num.zero?
    style = :medium unless [:long, :medium, :short].include?(style)
    expression = []
    n = num * TIME_EXPS[input.to_sym][:mult]
    done = false
    TIME_EXPS.reverse.each do |k, v|
      next if done
      done = true if k == stop
      div = n / v[:mult]
      next unless div >= 1
      val = (done ? div.round : div.floor)
      expression << "#{val}#{v[:styles][style]}#{val > 1 && style != :short ? 's' : nil}"
      n -= val.to_f * v[:mult]
    end
    expression.join(' ')
  end

  def self.to_nearest_duration(num, input: :sec, style: :medium)
    n = num * TIME_EXPS[input.to_sym][:mult]
    stop = nil
    TIME_EXPS.each do |k, v|
      stop = k if v[:mult] <= n
    end
    stop = :year unless stop
    to_duration(num, input: input, style: style, stop: stop)
  end

  TIME_EXPS = {
    yocto: {
      mult: 0.000000000000000000001,
      styles: { long: ' yoctosecond', medium: ' yocto', short: 'ys' },
      exp: %w(yoctosecond yocto yoctoseconds yoctos ys)
    },
    zepto: {
      mult: 0.000000000000000001,
      styles: { long: ' zeptosecond', medium: ' zepto', short: 'zs' },
      exp: %w(zeptosecond zepto zeptoseconds zeptos zs)
    },
    atto: {
      mult: 0.000000000000001,
      styles: { long: ' attosecond', medium: ' atto', short: 'as' },
      exp: %w(attoseconds atto attoseconds attos as)
    },
    femto: {
      mult: 0.000000000001,
      styles: { long: ' femtosecond', medium: ' fempto', short: 'fs' },
      exp: %w(femtosecond fempto femtoseconds femptos fs)
    },
    pico: {
      mult: 0.000000001,
      styles: { long: ' picosecond', medium: ' pico', short: 'ps' },
      exp: %w(picosecond pico picoseconds picos ps)
    },
    nano: {
      mult: 0.000001,
      styles: { long: ' nanosecond', medium: ' nano', short: 'ns' },
      exp: %w(nanosecond nano nanoseconds nanos ns)
    },
    micro: {
      mult: 0.001,
      styles: { long: ' microsecond', medium: ' micro', short: 'μs' },
      exp: %W(microsecond micro microseconds micros \u03BCs)
    },
    milli: {
      mult: 1,
      styles: { long: ' millisecond', medium: ' mil', short: 'ms' },
      exp: %w(ms mil mils milli millis millisecond milliseconds milsec milsecs msec msecs msecond mseconds)
    },
    sec: {
      mult: 1000,
      styles: { long: ' second', medium: ' sec', short: 's' },
      exp: %w(s sec secs second seconds)
    },
    min: {
      mult: 60_000,
      styles: { long: ' minute', medium: ' min', short: 'm' },
      exp: %w(m mn mns min mins minute minutes)
    },
    hour: {
      mult: 3_600_000,
      styles: { long: ' hour', medium: ' hr', short: 'h' },
      exp: %w(h hr hrs hour hours)
    },
    day: {
      mult: 86_400_000,
      styles: { long: ' day', medium: ' day', short: 'd' },
      exp: %w(d day days)
    },
    week: {
      mult: 604_800_000,
      styles: { long: ' week', medium: ' wk', short: 'w' },
      exp: %w(w wk wks week weeks)
    },
    month: {
      mult: 2_592_000_000,
      styles: { long: ' month', medium: ' mo', short: 'mo' },
      exp: %w(mo mon mons month months mnth mnths mth mths)
    },
    year: {
      mult: 31_536_000_000,
      styles: { long: ' year', medium: ' yr', short: 'y' },
      exp: %w(y yr yrs year years)
    }
  }.freeze

  module Durations
    TIME_TABLE = {
      nanosecond:  0.000000001,
      microsecond: 0.000001,
      milisecond:  0.001,
      second:      1,
      minute:      60,
      hour:        3_600,
      day:         86_400,
      week:        604_800_000,
      month:       2_592_000,
      year:        31_536_000
    }.freeze

    TIME_TABLE.each do |name, multiplier|
      define_method(name) do
        self * multiplier
      end

      define_method(name.pluralize) do
        self * multiplier
      end
    end
  end
end

class String
  def parse_duration(output: :sec, min_interval: :sec)
    BBLib.parse_duration self, output: output, min_interval: min_interval
  end
end

class Numeric
  include BBLib::Durations

  def to_duration(input: :sec, stop: :milli, style: :medium)
    BBLib.to_duration self, input: input, stop: stop, style: style
  end

  def to_nearest_duration(*args)
    BBLib.to_nearest_duration(self, *args)
  end
end