require 'active_support/core_ext/object/try'
require 'nokogiri'

require_relative 'ruby_extensions/thread'
require_relative 'command'


module Epuber
  class UserInterface
    Location = Struct.new(:path, :lineno)

    class << self
      # @return [Epuber::Command]
      #
      attr_accessor :current_command
    end

    # Fatal error, prints message and exit with return code 1
    #
    # @param [Exception, String] message message of the error
    # @param [Thread::Backtrace::Location] location location of the error
    #
    def self.error!(message, location: nil)
      error(message, location: location)
      exit(1)
    end

    # Fatal error, prints message and exit with return code 1
    #
    # @param [String] message message of the error
    # @param [Thread::Backtrace::Location] location location of the error
    # @param [Bool] backtrace  output backtrace locations, nil == automatic, true == always and false == never
    #
    def self.error(message, location: nil)
      $stdout.puts unless @last_processing_file_line.nil?

      $stdout.puts(_format_message(:error, message, location: location))
      _print_backtrace(location.try(:backtrace_locations) || message.try(:backtrace_locations) || caller_locations, location: location) if current_command && current_command.verbose?
    end

    # @param [String] message message of the error
    # @param [Thread::Backtrace::Location] location location of the error
    #
    def self.warning(message, location: nil)
      _clear_processing_line_for_new_output do
        $stdout.puts(_format_message(:warning, message, location: location))
      end
    end





    # @param [#to_s] problem some problem, object just have to know to convert self into string with method #to_s
    #
    def self.print_processing_problem(problem)
      _clear_processing_line_for_new_output do
        $stdout.puts(problem.to_s.ansi.send(_color_from_level(:warning)))
      end
    end

    # @param [String] info_text
    #
    def self.print_processing_debug_info(info_text)
      if current_command && current_command.verbose?
        _clear_processing_line_for_new_output do
          message = if @current_file.nil?
                      "▸ #{info_text}"
                    else
                      "▸ #{@current_file.source_path}: #{info_text}"
                    end

          $stdout.puts(message.ansi.send(_color_from_level(:debug)))
        end
      end
    end

    # @param [Compiler::FileTypes::AbstractFile] file
    #
    # @return nil
    #
    def self.print_processing_file(file, index, count)
      remove_processing_file_line

      @current_file = file

      @last_processing_file_line = "▸ Processing #{file.source_path} (#{index + 1} of #{count})"
      $stdout.print(@last_processing_file_line)
    end

    def self.remove_processing_file_line
      last_line = @last_processing_file_line

      unless @last_processing_file_line.nil?
        $stdout.print("\033[2K") # remove line, but without moving cursor
        $stdout.print("\r") # go to beginning of line
        @last_processing_file_line = nil
      end

      last_line
    end

    def self.processing_files_done
      remove_processing_file_line

      @current_file = nil
    end

    private

    def self._clear_processing_line_for_new_output
      last_line = remove_processing_file_line

      yield

      @last_processing_file_line = last_line
      $stdout.print(last_line)
    end

    # @param [Symbol] level color of the output
    #
    # @return [Symbol] color
    #
    def self._color_from_level(level)
      case level
        when :error;   :red
        when :warning; :yellow
        when :normal;  :white
        when :debug;   :blue
        else
          raise "Unknown output level #{level}"
      end
    end

    # @param [Thread::Backtrace::Location, Nokogiri::XML::Node] obj
    #
    # @return [Location]
    #
    def self._location_from_obj(obj)
      case obj
      when ::Thread::Backtrace::Location
        Location.new(obj.path, obj.lineno)
      when ::Nokogiri::XML::Node
        Location.new(obj.document.file_path, obj.line)
      when Location
        obj
      end
    end

    # @param [Symbol] level color of the output
    # @param [String] message message of the error
    # @param [Thread::Backtrace::Location] location location of the error
    #
    # @return [String] formatted message
    #
    def self._format_message(level, message, location: nil)
      location = _location_from_obj(location)

      comps = []
      comps << message.to_s
      comps << "  (in file #{location.path} line #{location.lineno}" unless location.nil?

      comps.join("\n").ansi.send(_color_from_level(level))
    end

    # @param [Array<Thread::Backtrace::Location>] locations locations of the error (only for verbose output)
    # @param [Thread::Backtrace::Location] location location of the error
    #
    # @return [String] formatted message
    #
    def self._format_backtrace(locations, location: nil)
      index = locations.index(location) || 0
      locations[index, locations.size].map { |loc| loc.to_s }
    end

    # @param [Thread::Backtrace::Location] location location of the error
    #
    def self._print_backtrace(locations, location: nil)
      puts(_format_backtrace(locations, location: location)) if current_command.verbose?
    end
  end

  # shortcut
  UI = UserInterface
end