# encoding: utf-8

require 'heroku/helpers/env'

module Heroku
  module Helpers

    extend self

    def home_directory
      if running_on_windows?
        # This used to be File.expand_path("~"), which should have worked but there was a bug
        # when a user has a cyrillic character in their username.  Their username gets mangled
        # by a C code operation that does not respect multibyte characters
        #
        # see: https://github.com/ruby/ruby/blob/v2_2_3/win32/file.c#L47
        home = Heroku::Helpers::Env["HOME"]
        homedrive = Heroku::Helpers::Env["HOMEDRIVE"]
        homepath = Heroku::Helpers::Env["HOMEPATH"]
        userprofile = Heroku::Helpers::Env["USERPROFILE"]

        home_dir = if home
                     home
                   elsif homedrive && homepath
                     homedrive + homepath
                   elsif userprofile
                     userprofile
                   else
                     # The expanding `~' error here does not make much sense
                     # just made it match File.expand_path when no env set
                     raise ArgumentError.new("couldn't find HOME environment -- expanding `~'")
                   end
        home_dir.gsub(/\\/, '/')
      else
        Dir.home
      end
    end

    def running_on_windows?
      RUBY_PLATFORM =~ /mswin32|mingw32/
    end

    def running_on_a_mac?
      RUBY_PLATFORM =~ /-darwin\d/
    end

    def display(msg="", new_line=true)
      if new_line
        puts(msg)
      else
        print(msg)
      end
      $stdout.flush
    end

    def redisplay(line, line_break = false)
      display("\r\e[0K#{line}", line_break)
    end

    def deprecate(message)
      display "WARNING: #{message}"
    end

    def debug(*args)
      $stderr.puts(*args) if debugging?
    end

    def stderr_puts(*args)
      $stderr.puts(*args)
    end

    def stderr_print(*args)
      $stderr.print(*args)
    end

    def debugging?
      ENV['HEROKU_DEBUG']
    end

    def confirm(message="Are you sure you wish to continue? (y/n)")
      display("#{message} ", false)
      ['y', 'yes'].include?(ask.downcase)
    end

    def confirm_command(app_to_confirm = app, message=nil)
      if confirmed_app = Heroku::Command.current_options[:confirm]
        unless confirmed_app == app_to_confirm
          raise(Heroku::Command::CommandFailed, "Confirmed app #{confirmed_app} did not match the selected app #{app_to_confirm}.")
        end
        return true
      else
        display
        message ||= "WARNING: Destructive Action\nThis command will affect the app: #{app_to_confirm}"
        message << "\nTo proceed, type \"#{app_to_confirm}\" or re-run this command with --confirm #{app_to_confirm}"
        output_with_bang(message)
        display
        display "> ", false
        if ask.downcase != app_to_confirm
          error("Confirmation did not match #{app_to_confirm}. Aborted.")
        else
          true
        end
      end
    end

    def format_date(date)
      date = Time.parse(date).utc if date.is_a?(String)
      date.strftime("%Y-%m-%d %H:%M %Z").gsub('GMT', 'UTC')
    end

    def ask
      $stdin.gets.to_s.strip
    end

    def shell(cmd)
      FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`}
    end

    def run_command(command, args=[])
      Heroku::Command.run(command, args)
    end

    def retry_on_exception(*exceptions)
      retry_count = 0
      begin
        yield
      rescue *exceptions => ex
        raise ex if retry_count >= 3
        sleep 3
        retry_count += 1
        retry
      end
    end

    def has_git?
      %x{ git --version }
      $?.success?
    end

    def git(args)
      return "" unless has_git?
      flattened_args = [args].flatten.compact.join(" ")
      %x{ git #{flattened_args} 2>&1 }.strip
    end

    def time_ago(since)
      if since.is_a?(String)
        since = Time.parse(since)
      end

      elapsed = Time.now - since

      message = since.strftime("%Y/%m/%d %H:%M:%S")
      if elapsed <= 60
        message << " (~ #{elapsed.floor}s ago)"
      elsif elapsed <= (60 * 60)
        message << " (~ #{(elapsed / 60).floor}m ago)"
      elsif elapsed <= (60 * 60 * 25)
        message << " (~ #{(elapsed / 60 / 60).floor}h ago)"
      end
      message
    end

    def time_remaining(from, to)
      secs = (to - from).to_i
      mins  = secs / 60
      hours = mins / 60
      return "#{hours}h #{mins % 60}m" if hours > 0
      return "#{mins}m #{secs % 60}s" if mins > 0
      return "#{secs}s" if secs >= 0
    end

    def truncate(text, length)
      return "" if text.nil?
      if text.size > length
        text[0, length - 2] + '..'
      else
        text
      end
    end

    @@kb = 1024
    @@mb = 1024 * @@kb
    @@gb = 1024 * @@mb
    def format_bytes(amount)
      amount = amount.to_i
      return '(empty)' if amount == 0
      return amount if amount < @@kb
      return "#{(amount / @@kb).round}k" if amount < @@mb
      return "#{(amount / @@mb).round}M" if amount < @@gb
      return "#{(amount / @@gb).round}G"
    end

    def quantify(string, num)
      "%d %s" % [ num, num.to_i == 1 ? string : "#{string}s" ]
    end

    def has_git_remote?(remote)
      git('remote').split("\n").include?(remote) && $?.success?
    end

    def create_git_remote(remote, url)
      return if has_git_remote? remote
      git "remote add #{remote} #{url}"
      display "Git remote #{remote} added" if $?.success?
    end

    def longest(items)
      items.map { |i| i.to_s.length }.sort.last
    end

    def display_table(objects, columns, headers)
      lengths = []
      columns.each_with_index do |column, index|
        header = headers[index]
        lengths << longest([header].concat(objects.map { |o| o[column].to_s }))
      end
      lines = lengths.map {|length| "-" * length}
      lengths[-1] = 0 # remove padding from last column
      display_row headers, lengths
      display_row lines, lengths
      objects.each do |row|
        display_row columns.map { |column| row[column] }, lengths
      end
    end

    def display_row(row, lengths)
      row_data = []
      row.zip(lengths).each do |column, length|
        format = column.is_a?(Fixnum) ? "%#{length}s" : "%-#{length}s"
        row_data << format % column
      end
      display(row_data.join("  "))
    end

    def json_encode(object)
      JSON.generate(object)
    end

    def json_decode(json)
      JSON.parse(json)
    rescue JSON::ParserError
      nil
    end

    def set_buffer(enable)
      with_tty do
        if enable
          `stty icanon echo`
        else
          `stty -icanon -echo`
        end
      end
    end

    def with_tty(&block)
      return unless $stdin.isatty
      begin
        yield
      rescue
        # fails on windows
      end
    end

    def get_terminal_environment
      { "TERM" => ENV["TERM"], "COLUMNS" => `tput cols`.strip, "LINES" => `tput lines`.strip }
    rescue
      { "TERM" => ENV["TERM"] }
    end

    def fail(message)
      raise Heroku::Command::CommandFailed, message
    end

    ## DISPLAY HELPERS

    def action(message, options={})
      display("#{in_message(message, options)}... ", false)
      Heroku::Helpers.error_with_failure = true
      ret = yield
      Heroku::Helpers.error_with_failure = false
      display((options[:success] || "done"), false)
      if @status
        display(", #{@status}", false)
        @status = nil
      end
      display
      ret
    end

    def in_message(message, options={})
      message = "#{message} in space #{options[:space]}" if options[:space]
      message = "#{message} in organization #{org}" if options[:org]
      message
    end

    def status(message)
      @status = message
    end

    def format_with_bang(message)
      return '' if message.to_s.strip == ""
      " !    " + message.encode('utf-8', 'binary', invalid: :replace, undef: :replace).split("\n").join("\n !    ")
    end

    def output_with_bang(message="", new_line=true)
      return if message.to_s.strip == ""
      display(format_with_bang(message), new_line)
    end

    def error(message, report=false)
      if Heroku::Helpers.error_with_failure
        display("failed")
        Heroku::Helpers.error_with_failure = false
      end
      $stderr.puts(format_with_bang(message))
      rollbar_id = Rollbar.error(message) if report
      $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id
      exit(1)
    end

    def self.error_with_failure
      @@error_with_failure ||= false
    end

    def self.error_with_failure=(new_error_with_failure)
      @@error_with_failure = new_error_with_failure
    end

    def self.included_into
      @@included_into ||= []
    end

    def self.extended_into
      @@extended_into ||= []
    end

    def self.included(base)
      included_into << base
    end

    def self.extended(base)
      extended_into << base
    end

    def display_header(message="", new_line=true)
      return if message.to_s.strip == ""
      display("=== " + message.to_s.split("\n").join("\n=== "), new_line)
    end

    def display_object(object)
      case object
      when Array
        # list of objects
        object.each do |item|
          display_object(item)
        end
      when Hash
        # if all values are arrays, it is a list with headers
        # otherwise it is a single header with pairs of data
        if object.values.all? {|value| value.is_a?(Array)}
          object.keys.sort_by {|key| key.to_s}.each do |key|
            display_header(key)
            display_object(object[key])
            hputs
          end
        end
      else
        hputs(object.to_s)
      end
    end

    def hputs(string='')
      Kernel.puts(string)
    end

    def hprint(string='')
      Kernel.print(string)
      $stdout.flush
    end

    def spinner(ticks)
      %w(/ - \\ |)[ticks % 4]
    end

    def launchy(message, url)
      action(message) do
        require("launchy")
        launchy = Launchy.open(url)
        if launchy.respond_to?(:join)
          launchy.join
        end
      end
    end

    # produces a printf formatter line for an array of items
    # if an individual line item is an array, it will create columns
    # that are lined-up
    #
    # line_formatter(["foo", "barbaz"])                 # => "%-6s"
    # line_formatter(["foo", "barbaz"], ["bar", "qux"]) # => "%-3s   %-6s"
    #
    def line_formatter(array)
      if array.any? {|item| item.is_a?(Array)}
        cols = []
        array.each do |item|
          if item.is_a?(Array)
            item.each_with_index { |val,idx| cols[idx] = [cols[idx]||0, (val || '').length].max }
          end
        end
        cols.map { |col| "%-#{col}s" }.join("  ")
      else
        "%s"
      end
    end

    def styled_array(array, options={})
      fmt = line_formatter(array)
      array = array.sort unless options[:sort] == false
      array.each do |element|
        display((fmt % element).rstrip)
      end
      display
    end

    def format_error(error, message='Heroku client internal error.', rollbar_id=nil)
      formatted_error = []
      formatted_error << " !    #{message}"
      formatted_error << ' !    Search for help at: https://help.heroku.com'
      formatted_error << ' !    Or report a bug at: https://github.com/heroku/heroku/issues/new'
      formatted_error << ''
      formatted_error << "    Error:       #{error.message} (#{error.class})"
      command = ARGV.map do |arg|
        if arg.include?(' ')
          arg = %{"#{arg}"}
        end
        arg.encode('utf-8')
      end.join(' ')
      formatted_error << "    Command:     heroku #{command}"
      require 'heroku/auth'
      unless Heroku::Auth.host == Heroku::Auth.default_host
        formatted_error << "    Host:        #{Heroku::Auth.host}"
      end
      if http_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
        formatted_error << "    HTTP Proxy:  #{http_proxy}"
      end
      if https_proxy = ENV['https_proxy'] || ENV['HTTPS_PROXY']
        formatted_error << "    HTTPS Proxy: #{https_proxy}"
      end
      plugins = Heroku::Plugin.list.sort
      unless plugins.empty?
        formatted_error << "    Plugins:     #{plugins.first}"
        plugins[1..-1].each do |plugin|
          formatted_error << "                 #{plugin}"
        end
        if plugins.length > 1
          formatted_error << ''
          $stderr.puts
        end
      end
      formatted_error << "    Version:     #{Heroku.user_agent}"
      formatted_error << "    Error ID:    #{rollbar_id}" if rollbar_id
      formatted_error << "\n"
      formatted_error << "    More information in #{error_log_path}"
      formatted_error << "\n"
      formatted_error.join("\n")
    end

    def styled_error(error, message='Heroku client internal error.')
      if Heroku::Helpers.error_with_failure
        display("failed")
        Heroku::Helpers.error_with_failure = false
      end
      rollbar_id = Rollbar.error(error)
      $stderr.puts(format_error(error, message, rollbar_id))
      error_log(message, error.message, error.backtrace.join("\n"))
    rescue => e
      $stderr.puts e, e.backtrace, error, error.backtrace
    end

    def error_log(*obj)
      FileUtils.mkdir_p(File.dirname(error_log_path))
      File.open(error_log_path, 'a') do |file|
        file.write(obj.join("\n") + "\n")
      end
    end

    def error_log_path
      File.join(home_directory, '.heroku', 'error.log')
    end

    def styled_header(header)
      display("=== #{header}")
    end

    def styled_hash(hash, keys=nil)
      max_key_length = hash.keys.map {|key| key.to_s.length}.max + 2
      keys ||= hash.keys.sort {|x,y| x.to_s <=> y.to_s}
      keys.each do |key|
        case value = hash[key]
        when Array
          if value.empty?
            next
          else
            elements = value.sort {|x,y| x.to_s <=> y.to_s}
            display("#{key}: ".ljust(max_key_length), false)
            display(elements[0])
            elements[1..-1].each do |element|
              display("#{' ' * max_key_length}#{element}")
            end
            if elements.length > 1
              display
            end
          end
        when nil
          next
        else
          display("#{key}: ".ljust(max_key_length), false)
          display(value)
        end
      end
    end

    def string_distance(first, last)
      distances = [] # 0x0s
      0.upto(first.length) do |index|
        distances << [index] + [0] * last.length
      end
      distances[0] = 0.upto(last.length).to_a
      1.upto(last.length) do |last_index|
        1.upto(first.length) do |first_index|
          first_char = first[first_index - 1, 1]
          last_char = last[last_index - 1, 1]
          if first_char == last_char
            distances[first_index][last_index] = distances[first_index - 1][last_index - 1] # noop
          else
            distances[first_index][last_index] = [
              distances[first_index - 1][last_index],     # deletion
              distances[first_index][last_index - 1],     # insertion
              distances[first_index - 1][last_index - 1]  # substitution
            ].min + 1 # cost
            if first_index > 1 && last_index > 1
              first_previous_char = first[first_index - 2, 1]
              last_previous_char = last[last_index - 2, 1]
              if first_char == last_previous_char && first_previous_char == last_char
                distances[first_index][last_index] = [
                  distances[first_index][last_index],
                  distances[first_index - 2][last_index - 2] + 1 # transposition
                ].min
              end
            end
          end
        end
      end
      distances[first.length][last.length]
    end

    def suggestion(actual, possibilities)
      distances = Hash.new {|hash,key| hash[key] = []}

      possibilities.each do |suggestion|
        distances[string_distance(actual, suggestion)] << suggestion
      end

      minimum_distance = distances.keys.min
      if minimum_distance < 4
        suggestions = distances[minimum_distance].sort
        if suggestions.length == 1
          "Perhaps you meant `#{suggestions.first}`."
        else
          "Perhaps you meant #{suggestions[0...-1].map {|suggestion| "`#{suggestion}`"}.join(', ')} or `#{suggestions.last}`."
        end
      else
        nil
      end
    end

    def org_host
      ENV["HEROKU_ORG_HOST"] || default_org_host
    end

    def default_org_host
      "herokumanager.com"
    end

    def org? email
      email =~ /^.*@#{org_host}$/
    end

    def app_owner email
      org?(email) ? email.gsub(/^(.*)@#{org_host}$/,'\1') : email
    end

    def has_http_git_entry_in_netrc
      Auth.netrc && Auth.netrc[Auth.http_git_host]
    end

    def warn_if_using_jruby
      stderr_puts "WARNING: jruby is known to cause issues when used with the toolbelt." if RUBY_PLATFORM == "java"
    end

    # cheap deep clone
    def deep_clone(obj)
      json_decode(json_encode(obj))
    end
  end
end
