# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require "delegate"
require "io/console"
require "thread"
require "log4r"
require "vagrant/util/platform"
require "vagrant/util/safe_puts"

module Vagrant
  module UI
    # Vagrant UIs handle communication with the outside world (typically
    # through a shell). They must respond to the following methods:
    #
    # * `info`
    # * `warn`
    # * `error`
    # * `success`
    class Interface
      # Opts can be used to set some options. These options are implementation
      # specific. See the implementation for more docs.
      attr_accessor :opts

      # @return [IO] UI input. Defaults to `$stdin`.
      attr_accessor :stdin

      # @return [IO] UI output. Defaults to `$stdout`.
      attr_accessor :stdout

      # @return [IO] UI error output. Defaults to `$stderr`.
      attr_accessor :stderr

      def initialize
        @logger   = Log4r::Logger.new("vagrant::ui::interface")
        @opts     = {}

        @stdin  = $stdin
        @stdout = $stdout
        @stderr = $stderr
      end

      def initialize_copy(original)
        super
        @opts = original.opts.dup
      end

      [:ask, :detail, :warn, :error, :info, :output, :success].each do |method|
        define_method(method) do |message, **opts|
          # Log normal console messages
          begin
            @logger.info { "#{method}: #{message}" }
          rescue ThreadError
            # We're being called in a trap-context. Wrap in a thread.
            Thread.new do
              @logger.info { "#{method}: #{message}" }
            end.join(THREAD_MAX_JOIN_TIMEOUT)
          end
        end
      end

      [:clear_line, :report_progress].each do |method|
        # By default do nothing, these aren't logged
        define_method(method) { |*args| }
      end

      # @return [false]
      def color?
        return false
      end

      # For machine-readable output.
      #
      # @param [String] type The type of the data
      # @param [Array] data The data associated with the type
      def machine(type, *data)
        @logger.info("Machine: #{type} #{data.inspect}")
      end

      # Yields self (UI)
      # Provides a way for selectively displaying or not displaying
      # updating content like download progress.
      def rewriting
        yield self
      end

      def to_proto
        raise NotImplementedError,
          "Vagrant::UI::Interface#to_proto"
      end
    end

    # This is a UI implementation that does nothing.
    class Silent < Interface
      def ask(*args, **opts)
        super

        # Silent can't do this, obviously.
        raise Errors::UIExpectsTTY
      end
    end

    class MachineReadable < Interface
      include Util::SafePuts

      def initialize
        super

        @lock = Mutex.new
      end

      def ask(*args, **opts)
        super

        # Machine-readable can't ask for input
        raise Errors::UIExpectsTTY
      end

      [:detail, :warn, :error, :info, :output, :success].each do |method|
        define_method(method) do |message, **opts|
          machine("ui", method.to_s, message, **opts)
        end
      end

      def machine(type, *data)
        opts = {}
        opts = data.pop if data.last.kind_of?(Hash)

        target = opts[:target] || ""

        # Prepare the data by replacing characters that aren't outputted
        data.each_index do |i|
          data[i] = data[i].to_s.dup
          data[i].gsub!(",", "%!(VAGRANT_COMMA)")
          data[i].gsub!("\n", "\\n")
          data[i].gsub!("\r", "\\r")
        end

        # Avoid locks in a trap context introduced from Ruby 2.0
        Thread.new do
          @lock.synchronize do
            safe_puts("#{Time.now.utc.to_i},#{target},#{type},#{data.join(",")}")
          end
        end.join(THREAD_MAX_JOIN_TIMEOUT)
      end
    end

    # This is a UI implementation that outputs the text as is. It
    # doesn't add any color.
    class Basic < Interface
      include Util::SafePuts

      def initialize
        super

        @lock = Mutex.new
      end

      # Use some light meta-programming to create the various methods to
      # output text to the UI. These all delegate the real functionality
      # to `say`.
      [:detail, :info, :warn, :error, :output, :success].each do |method|
        class_eval <<-CODE
          def #{method}(message, **opts)
            super(message)
            say(#{method.inspect}, message, **opts)
          end
        CODE
      end

      def ask(message, **opts)
        super(message)

        # We can't ask questions when the output isn't a TTY.
        raise Errors::UIExpectsTTY if !@stdin.tty? && !Vagrant::Util::Platform.windows?

        # Setup the options so that the new line is suppressed
        opts ||= {}
        opts[:echo]     = true  if !opts.key?(:echo)
        opts[:new_line] = false if !opts.key?(:new_line)
        opts[:prefix]   = false if !opts.key?(:prefix)

        # Output the data
        say(:info, message, opts)

        input = nil
        if opts[:echo] || !@stdin.respond_to?(:noecho)
          input = @stdin.gets
        else
          begin
            input = @stdin.noecho(&:gets)

            # Output a newline because without echo, the newline isn't
            # echoed either.
            say(:info, "\n", opts)
          rescue Errno::EBADF
            # This means that stdin doesn't support echoless input.
            say(:info, "\n#{I18n.t("vagrant.stdin_cant_hide_input")}\n ", opts)

            # Ask again, with echo enabled
            input = ask(message, **opts.merge(echo: true))
          end
        end

        # Get the results and chomp off the newline. We do a logical OR
        # here because `gets` can return a nil, for example in the case
        # that ctrl-D is pressed on the input.
        (input || "").chomp
      end

      # This is used to output progress reports to the UI.
      # Send this method progress/total and it will output it
      # to the UI. Send `clear_line` to clear the line to show
      # a continuous progress meter.
      def report_progress(progress, total, show_parts=true)
        if total && total > 0
          percent = (progress.to_f / total.to_f) * 100
          line    = "Progress: #{percent.to_i}%"
          line   << " (#{progress} / #{total})" if show_parts
        else
          line    = "Progress: #{progress}"
        end

        info(line, new_line: false)
      end

      def clear_line
        # See: https://en.wikipedia.org/wiki/ANSI_escape_code
        reset = "\r\033[K"

        info(reset, new_line: false)
      end

      # This method handles actually outputting a message of a given type
      # to the console.
      def say(type, message, opts={})
        defaults = { new_line: true, prefix: true }
        opts     = defaults.merge(@opts).merge(opts)

        # Don't output if we're hiding details
        return if type == :detail && opts[:hide_detail]

        # Determine whether we're expecting to output our
        # own new line or not.
        printer = opts[:new_line] ? :puts : :print

        # Determine the proper IO channel to send this message
        # to based on the type of the message
        channel = type == :error || opts[:channel] == :error ? @stderr : @stdout

        # Output! We wrap this in a lock so that it safely outputs only
        # one line at a time. We wrap this in a thread because as of Ruby 2.0
        # we can't acquire locks in a trap context (ctrl-c), so we have to
        # do this.
        Thread.new do
          @lock.synchronize do
            safe_puts(format_message(type, message, **opts),
                      io: channel, printer: printer)
          end
        end.join(THREAD_MAX_JOIN_TIMEOUT)
      end

      def format_message(type, message, **opts)
        Util::CredentialScrubber.desensitize(message)
      end
    end

    class NonInteractive < Basic
      def initialize
        super
      end

      def rewriting
        # no-op
      end

      def report_progress(progress, total, show_parts=true)
        # no-op
      end

      def clear_line
        @logger.warn("Using `clear line` in a non interactive ui")
        say(:info, "\n", opts)
      end

      def ask(*args, **opts)
        # Non interactive can't ask for input
        raise Errors::UIExpectsTTY
      end
    end

    # Prefixed wraps an existing UI and adds a prefix to it.
    class Prefixed < Interface
      # The prefix for `output` messages.
      OUTPUT_PREFIX = "==> "

      def initialize(ui, prefix)
        super()

        @prefix = prefix
        @ui     = ui
      end

      def to_proto
        @ui.to_proto
      end

      def client
        @ui.client
      end

      def initialize_copy(original)
        super
        @ui = original.instance_variable_get(:@ui).dup
      end

      # Use some light meta-programming to create the various methods to
      # output text to the UI. These all delegate the real functionality
      # to `say`.
      [:ask, :detail, :info, :warn, :error, :output, :success].each do |method|
        class_eval <<-CODE
          def #{method}(message, **opts)
            super(message)
            if !@ui.opts.key?(:bold) && !opts.key?(:bold)
              opts[:bold] = #{method.inspect} != :detail && \
                #{method.inspect} != :ask
            end
            if !opts.key?(:target)
              opts[:target] = @prefix
            end
            @ui.#{method}(format_message(#{method.inspect}, message, **opts), **opts)
          end
        CODE
      end

      [:clear_line, :report_progress].each do |method|
        # By default do nothing, these aren't formatted
        define_method(method) do |*args|
          @ui.send(method, *args)
        end
      end

      # For machine-readable output, set the prefix in the
      # options hash and continue it on.
      def machine(type, *data)
        opts = {}
        opts = data.pop if data.last.is_a?(Hash)
        opts[:target] = @prefix
        data << opts
        @ui.machine(type, *data)
      end

      # Return the parent's opts.
      #
      # @return [Hash]
      def opts
        @ui.opts
      end

      def format_message(type, message, **opts)
        opts = self.opts.merge(opts)

        prefix = ""
        if !opts.key?(:prefix) || opts[:prefix]
          prefix = OUTPUT_PREFIX
          prefix = " " * OUTPUT_PREFIX.length if \
            type == :detail || type == :ask || opts[:prefix_spaces]
        end

        message = Util::CredentialScrubber.desensitize(message)

        # Fast-path if there is no prefix
        return message if prefix.empty?

        target = @prefix
        target = opts[:target] if opts.key?(:target)
        target = "#{target}:" if target != ""

        lines = [message]
        if message != ""
          lines = [].tap do |l|
            message.scan(/(.*?)(\n|$)/).each do |m|
              l << m.first if m.first != "" || (m.first == "" && m.last == "\n")
            end
          end
          lines << "" if message.end_with?("\n")
        end

        # Otherwise, make sure to prefix every line properly
        lines.map do |line|
          "#{prefix}#{target} #{line}"
        end.join("\n")
      end

      def rewriting
        @ui.rewriting do |ui|
          yield ui
        end
      end

    end

    # This is a UI implementation that outputs color for various types
    # of messages. This should only be used with a TTY that supports color,
    # but is up to the user of the class to verify this is the case.
    class Colored < Basic
      # Terminal colors
      COLORS = {
        red:     31,
        green:   32,
        yellow:  33,
        blue:    34,
        magenta: 35,
        cyan:    36,
        white:   37,
      }

      # @return [true]
      def color?
        return true
      end

      # This is called by `say` to format the message for output.
      def format_message(type, message, **opts)
        # Get the format of the message before adding color.
        message = super

        opts = @opts.merge(opts)

        # Special case some colors for certain message types
        opts[:color] = :red if type == :error
        opts[:color] = :green if type == :success
        opts[:color] = :yellow if type == :warn

        # If it is a detail, it is not bold. Every other message type
        # is bolded.
        bold  = !!opts[:bold]
        colorseq = "#{bold ? 1 : 0 }"
        if opts[:color] && opts[:color] != :default
          color = COLORS[opts[:color]]
          colorseq += ";#{color}"
        end

        # Color the message and make sure to reset the color at the end
        "\033[#{colorseq}m#{message}\033[0m"
      end
    end
  end
end
