# frozen_string_literal: true

require "options"
require "highline"

class ProgressBar
  Error = Class.new(StandardError)
  ArgumentError = Class.new(Error)

  attr_accessor :count, :max, :meters

  def initialize(*args)
    @count      = 0
    @max        = 100
    @meters     = [:bar, :counter, :percentage, :elapsed, :eta, :rate]

    @max = args.shift if args.first.is_a? Numeric
    raise ArgumentError, "Max must be a positive integer" unless @max >= 0

    @meters = args unless args.empty?

    @last_write = ::Time.at(0)
    @start      = ::Time.now

    @hl = HighLine.new
    @terminal_width = 80
    @last_width_adjustment = ::Time.at(0)
  end

  def increment!(count = 1)
    @count += count
    now = ::Time.now
    return unless (now - @last_write) > 0.2 || @count >= max

    write
    @last_write = now
  end

  def puts(text)
    clear!
    $stderr.write(text)
    $stderr.puts
    write
  end

  def write
    print "\r" + to_s
  end

  def remaining
    max - count
  end

  def ratio
    [count.to_f / max, 1.0].min # never go above 1, even if count > max
  end

  def percentage
    ratio * 100
  end

  def elapsed
    ::Time.now - @start
  end

  def rate
    if count > 0
      count / elapsed
    else
      0
    end
  end

  def eta
    if count > 0
      remaining / rate
    else
      0
    end
  end

  def to_s
    self.count = max if count > max
    meters.inject(String.new) do |text, meter|
      text << "#{render(meter)} "
    end.strip
  end

  protected

  def print(str)
    $stderr.write str
  end

  def clear!
    print "\r" + (" " * terminal_width) + "\r"
  end

  def render(meter)
    send(:"render_#{meter}")
  end

  def width_of(meter)
    send(:"#{meter}_width")
  end

  def render_bar
    return "" if bar_width < 2

    progress_width = (ratio * (bar_width - 2)).floor
    remainder_width = bar_width - 2 - progress_width
    "[#{'#' * progress_width}#{' ' * remainder_width}]"
  end

  def render_counter
    "[%#{max_width}i/%i]" % [count, max]
  end

  def render_percentage
    format = (max == 100 ? "%3i" : "%6.2f")
    "[#{format}%%]" % percentage
  end

  def render_elapsed
    "[#{format_interval(elapsed)}]"
  end

  def render_eta
    "[#{format_interval(eta)}]"
  end

  def render_rate
    "[%#{max_width + 3}.2f/s]" % rate
  end

  def terminal_width
    # HighLine check takes a long time, so only update width every second.
    now = ::Time.now
    if now - @last_width_adjustment > 1
      @last_width_adjustment = now
      new_width = @hl.output_cols.to_i
      new_width = 80 if new_width < 1
      @terminal_width = new_width
    else
      @terminal_width
    end
  end

  def bar_width
    terminal_width - non_bar_width
  end

  def non_bar_width
    meters.reject { |meter| meter == :bar }.inject(0) do |width, meter|
      width + width_of(meter) + 1
    end
  end

  # [  1/100]
  def counter_width
    (max_width * 2) + 3
  end

  def percentage_width
    if max == 100      # [ 24%]
      6
    else               # [ 24.00%]
      9
    end
  end

  def elapsed_width
    format_interval(elapsed).length + 2
  end

  def eta_width
    format_interval(eta).length + 2
  end

  # [ 23.45/s]
  def rate_width
    render_rate.length
  end

  def max_width
    max.to_s.length
  end

  HOUR = 3600
  MINUTE = 60
  def format_interval(interval)
    if interval > HOUR
      "%02i:%02i:%02i" % [interval / HOUR, interval % HOUR / MINUTE, interval % MINUTE]
    else
      "%02i:%02i" % [interval / MINUTE, interval % MINUTE]
    end
  end
end

require_relative "progress_bar/with_progress"
