require_relative "column_printer"
require_relative "table_printer"
require_relative "wrapped_printer"

class Thor
  module Shell
    class Basic
      attr_accessor :base
      attr_reader   :padding

      # Initialize base, mute and padding to nil.
      #
      def initialize #:nodoc:
        @base = nil
        @mute = false
        @padding = 0
        @always_force = false
      end

      # Mute everything that's inside given block
      #
      def mute
        @mute = true
        yield
      ensure
        @mute = false
      end

      # Check if base is muted
      #
      def mute?
        @mute
      end

      # Sets the output padding, not allowing less than zero values.
      #
      def padding=(value)
        @padding = [0, value].max
      end

      # Sets the output padding while executing a block and resets it.
      #
      def indent(count = 1)
        orig_padding = padding
        self.padding = padding + count
        yield
        self.padding = orig_padding
      end

      # Asks something to the user and receives a response.
      #
      # If a default value is specified it will be presented to the user
      # and allows them to select that value with an empty response. This
      # option is ignored when limited answers are supplied.
      #
      # If asked to limit the correct responses, you can pass in an
      # array of acceptable answers.  If one of those is not supplied,
      # they will be shown a message stating that one of those answers
      # must be given and re-asked the question.
      #
      # If asking for sensitive information, the :echo option can be set
      # to false to mask user input from $stdin.
      #
      # If the required input is a path, then set the path option to
      # true. This will enable tab completion for file paths relative
      # to the current working directory on systems that support
      # Readline.
      #
      # ==== Example
      #   ask("What is your name?")
      #
      #   ask("What is the planet furthest from the sun?", :default => "Neptune")
      #
      #   ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"])
      #
      #   ask("What is your password?", :echo => false)
      #
      #   ask("Where should the file be saved?", :path => true)
      #
      def ask(statement, *args)
        options = args.last.is_a?(Hash) ? args.pop : {}
        color = args.first

        if options[:limited_to]
          ask_filtered(statement, color, options)
        else
          ask_simply(statement, color, options)
        end
      end

      # Say (print) something to the user. If the sentence ends with a whitespace
      # or tab character, a new line is not appended (print + flush). Otherwise
      # are passed straight to puts (behavior got from Highline).
      #
      # ==== Example
      #   say("I know you knew that.")
      #
      def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
        return if quiet?

        buffer = prepare_message(message, *color)
        buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")

        stdout.print(buffer)
        stdout.flush
      end

      # Say (print) an error to the user. If the sentence ends with a whitespace
      # or tab character, a new line is not appended (print + flush). Otherwise
      # are passed straight to puts (behavior got from Highline).
      #
      # ==== Example
      #   say_error("error: something went wrong")
      #
      def say_error(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
        return if quiet?

        buffer = prepare_message(message, *color)
        buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")

        stderr.print(buffer)
        stderr.flush
      end

      # Say a status with the given color and appends the message. Since this
      # method is used frequently by actions, it allows nil or false to be given
      # in log_status, avoiding the message from being shown. If a Symbol is
      # given in log_status, it's used as the color.
      #
      def say_status(status, message, log_status = true)
        return if quiet? || log_status == false
        spaces = "  " * (padding + 1)
        status = status.to_s.rjust(12)
        margin = " " * status.length + spaces

        color  = log_status.is_a?(Symbol) ? log_status : :green
        status = set_color status, color, true if color

        message = message.to_s.chomp.gsub(/(?<!\A)^/, margin)
        buffer = "#{status}#{spaces}#{message}\n"

        stdout.print(buffer)
        stdout.flush
      end

      # Asks the user a question and returns true if the user replies "y" or
      # "yes".
      #
      def yes?(statement, color = nil)
        !!(ask(statement, color, add_to_history: false) =~ is?(:yes))
      end

      # Asks the user a question and returns true if the user replies "n" or
      # "no".
      #
      def no?(statement, color = nil)
        !!(ask(statement, color, add_to_history: false) =~ is?(:no))
      end

      # Prints values in columns
      #
      # ==== Parameters
      # Array[String, String, ...]
      #
      def print_in_columns(array)
        printer = ColumnPrinter.new(stdout)
        printer.print(array)
      end

      # Prints a table.
      #
      # ==== Parameters
      # Array[Array[String, String, ...]]
      #
      # ==== Options
      # indent<Integer>:: Indent the first column by indent value.
      # colwidth<Integer>:: Force the first column to colwidth spaces wide.
      # borders<Boolean>:: Adds ascii borders.
      #
      def print_table(array, options = {}) # rubocop:disable Metrics/MethodLength
        printer = TablePrinter.new(stdout, options)
        printer.print(array)
      end

      # Prints a long string, word-wrapping the text to the current width of the
      # terminal display. Ideal for printing heredocs.
      #
      # ==== Parameters
      # String
      #
      # ==== Options
      # indent<Integer>:: Indent each line of the printed paragraph by indent value.
      #
      def print_wrapped(message, options = {})
        printer = WrappedPrinter.new(stdout, options)
        printer.print(message)
      end

      # Deals with file collision and returns true if the file should be
      # overwritten and false otherwise. If a block is given, it uses the block
      # response as the content for the diff.
      #
      # ==== Parameters
      # destination<String>:: the destination file to solve conflicts
      # block<Proc>:: an optional block that returns the value to be used in diff and merge
      #
      def file_collision(destination)
        return true if @always_force
        options = block_given? ? "[Ynaqdhm]" : "[Ynaqh]"

        loop do
          answer = ask(
            %[Overwrite #{destination}? (enter "h" for help) #{options}],
            add_to_history: false
          )

          case answer
          when nil
            say ""
            return true
          when is?(:yes), is?(:force), ""
            return true
          when is?(:no), is?(:skip)
            return false
          when is?(:always)
            return @always_force = true
          when is?(:quit)
            say "Aborting..."
            raise SystemExit
          when is?(:diff)
            show_diff(destination, yield) if block_given?
            say "Retrying..."
          when is?(:merge)
            if block_given? && !merge_tool.empty?
              merge(destination, yield)
              return nil
            end

            say "Please specify merge tool to `THOR_MERGE` env."
          else
            say file_collision_help(block_given?)
          end
        end
      end

      # Called if something goes wrong during the execution. This is used by Thor
      # internally and should not be used inside your scripts. If something went
      # wrong, you can always raise an exception. If you raise a Thor::Error, it
      # will be rescued and wrapped in the method below.
      #
      def error(statement)
        stderr.puts statement
      end

      # Apply color to the given string with optional bold. Disabled in the
      # Thor::Shell::Basic class.
      #
      def set_color(string, *) #:nodoc:
        string
      end

    protected

      def prepare_message(message, *color)
        spaces = "  " * padding
        spaces + set_color(message.to_s, *color)
      end

      def can_display_colors?
        false
      end

      def lookup_color(color)
        return color unless color.is_a?(Symbol)
        self.class.const_get(color.to_s.upcase)
      end

      def stdout
        $stdout
      end

      def stderr
        $stderr
      end

      def is?(value) #:nodoc:
        value = value.to_s

        if value.size == 1
          /\A#{value}\z/i
        else
          /\A(#{value}|#{value[0, 1]})\z/i
        end
      end

      def file_collision_help(block_given) #:nodoc:
        help = <<-HELP
        Y - yes, overwrite
        n - no, do not overwrite
        a - all, overwrite this and all others
        q - quit, abort
        h - help, show this help
        HELP
        if block_given
          help << <<-HELP
        d - diff, show the differences between the old and the new
        m - merge, run merge tool
          HELP
        end
        help
      end

      def show_diff(destination, content) #:nodoc:
        require "tempfile"
        Tempfile.open(File.basename(destination), File.dirname(destination), binmode: true) do |temp|
          temp.write content
          temp.rewind
          system(*diff_tool, destination, temp.path)
        end
      end

      def quiet? #:nodoc:
        mute? || (base && base.options[:quiet])
      end

      def unix?
        Terminal.unix?
      end

      def ask_simply(statement, color, options)
        default = options[:default]
        message = [statement, ("(#{default})" if default), nil].uniq.join(" ")
        message = prepare_message(message, *color)
        result = Thor::LineEditor.readline(message, options)

        return unless result

        result = result.strip

        if default && result == ""
          default
        else
          result
        end
      end

      def ask_filtered(statement, color, options)
        answer_set = options[:limited_to]
        case_insensitive = options.fetch(:case_insensitive, false)
        correct_answer = nil
        until correct_answer
          answers = answer_set.join(", ")
          answer = ask_simply("#{statement} [#{answers}]", color, options)
          correct_answer = answer_match(answer_set, answer, case_insensitive)
          say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer
        end
        correct_answer
      end

      def answer_match(possibilities, answer, case_insensitive)
        if case_insensitive
          possibilities.detect{ |possibility| possibility.downcase == answer.downcase }
        else
          possibilities.detect{ |possibility| possibility == answer }
        end
      end

      def merge(destination, content) #:nodoc:
        require "tempfile"
        Tempfile.open([File.basename(destination), File.extname(destination)], File.dirname(destination), binmode: true) do |temp|
          temp.write content
          temp.rewind
          system(*merge_tool, temp.path, destination)
        end
      end

      def merge_tool #:nodoc:
        @merge_tool ||= begin
          require "shellwords"
          Shellwords.split(ENV["THOR_MERGE"] || "git difftool --no-index")
        end
      end

      def diff_tool #:nodoc:
        @diff_cmd ||= begin
          require "shellwords"
          Shellwords.split(ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u")
        end
      end
    end
  end
end
