module BBLib
  # Simple timer that can track tasks based on time. Also provides aggregated metrics
  #  and history for each task run. Generally useful for benchmarking or logging.
  #
  # @author Brandon Black
  # @attr [Hash] tasks The information on all running tasks and history of all tasks up to the retention.
  # @attr [Integer] retention The number of runs to collect per task before truncation.
  class TaskTimer
    include Effortless
    include Prototype unless BBLib.in_opal?

    attr_hash :tasks, default: {}, serialize: false
    attr_int_between(-1, nil, :retention, default: 100)

    # Returns an aggregated metric for a given type.
    #
    # @param [Symbol] task The key value of the task to retrieve
    # @param [Symbol] type The metric to return.
    #   Options are :avg, :min, :max, :first, :last, :sum, :all and :count.
    # @return [Float, Integer, Array] Returns either the aggregation (Numeric) or an Array in the case of :all.
    def time(task = :default, type = :current)
      return nil unless tasks.keys.include?(task)
      numbers = tasks[task][:history].map { |v| v[:time] }
      case type
      when :current
        return nil unless tasks[task][:current]
        Time.now.to_f - tasks[task][:current]
      when :min, :max, :first, :last
        numbers.send(type)
      when :avg
        numbers.size.zero? ? nil : numbers.inject { |sum, n| sum + n }.to_f / numbers.size
      when :sum
        numbers.inject { |sum, n| sum + n }
      when :all
        numbers
      when :count
        numbers.size
      end
    end

    # Removes all history for a given task
    #
    # @param [Symbol] task The name of the task to clear history from.
    # @return [NilClass] Returns nil
    def clear(task = :default)
      return nil unless tasks.keys.include?(task)
      stop task
      tasks[task][:history].clear
    end

    # Start a new timer for the referenced task. If a timer is already running for that task it will be stopped first.
    #
    # @param [Symbol] task The name of the task to start.
    # @return [Integer] Returns 0
    def start(task = :default)
      tasks[task] = { history: [], current: nil } unless tasks.keys.include?(task)
      stop task if tasks[task][:current]
      tasks[task][:current] = Time.now.to_f
      0
    end

    # Stop the referenced timer.
    #
    # @param [Symbol] task The name of the task to stop.
    # @return [Float, NilClass] The amount of time the task had been running or nil if no matching task was found.
    def stop(task = :default)
      return nil unless tasks.keys.include?(task) && active?(task)
      time_taken = Time.now.to_f - tasks[task][:current].to_f
      tasks[task][:history] << { start: tasks[task][:current], stop: Time.now.to_f, time: time_taken }
      tasks[task][:current] = nil
      if retention && tasks[task][:history].size > retention then tasks[task][:history].shift end
      time_taken
    end

    def restart(task = :default)
      start(task) unless stop(task).nil?
    end

    def active?(task = :default)
      return false unless tasks.keys.include?(task)
      !tasks[task][:current].nil?
    end

    def stats(task = :default, pretty: false)
      return nil unless tasks.include?(task)
      TIMER_TYPES.map do |k, _v|
        next if STATS_IGNORE.include?(k)
        [k, send(k, task, pretty: pretty)]
      end.compact.to_h
    end

    def method_missing(*args, **named)
      temp   = args.first.to_sym
      type   = TIMER_TYPES.keys.find { |k| k == temp || TIMER_TYPES[k].include?(temp) }
      return super unless type
      t = time(args[1] || :default, type)
      return t if type == :count || !named[:pretty]
      t.is_a?(Array) ? t.map(&:to_duration) : t.to_duration
    end

    def respond_to_missing?(method, include_private = false)
      TIMER_TYPES.keys.find { |k| k == method || TIMER_TYPES[k].include?(method) } || super
    end

    TIMER_TYPES = {
      current: [],
      count:   [:total],
      first:   [:initial],
      last:    [:latest],
      min:     [:minimum, :smallest],
      max:     [:maximum, :largest],
      avg:     [:average, :av],
      sum:     [],
      all:     [:times]
    }.freeze

    protected

    STATS_IGNORE = [:all].freeze

    def simple_init(*args)
      start(args.first) if args.first.is_a?(Symbol)
    end
  end
end