# frozen_string_literal: true

# Inspired by command-builder by Martin Poljak and Jacob Evans, originally licensed MIT

require 'childprocess'
require 'shellwords'

module Tueor

  # Represents one command line command with arguments and parameters.
  class AsyncCommand

    # Channel for communicating with the subprocess's stdout/stderr
    class Receiver < EventMachine::Connection
      def initialize(callback = nil)
        @callback = callback
        @buffer = []
      end

      def receive_data(data)
        @buffer << data
      end

      def unbind
        @callback&.call(@buffer.join(''))
      end
    end

    attr_reader :separators

    def initialize(command, separators: nil, shell: false, &block)
      @command = [command.to_s]
      @separators = separators || (ChildProcess.windows? ? ['/', ':', '-', ' '].freeze : ['-', ' ', '--', '='].freeze)
      @shell = shell
      execute(&block) unless block.nil?
    end

    ##
    # Adds argument to command.
    #
    # One-letter arguments will be treated as "short" arguments for the
    # syntax purposes, other as "long". See {#separators}.
    #

    def argument(name, value = nil, short: nil, &block)
      @command << format_argument(name, value, short: short)

      execute(&block) unless block.nil?
      self
    end

    alias_method :arg, :argument

    ##
    # Adds parameters to command.

    def parameter(*values, raw: false, &block)
      values.each { |value| @command << (raw ? value : escape(value)) }

      execute(&block) unless block.nil?
      self
    end

    alias_method :param, :parameter
    alias_method :params, :parameter

    ##
    # Adds an item to command. If option is Symbol or value isn't +nil+,
    # it will apply the item as an argument, in otherwise, it will treat
    # as a parameter.

    def add(option, value = nil, &block)
      if option.is_a?(Symbol) || !value.nil?
        argument(option, value)
      else
        parameter(option)
      end

      execute(&block) unless block.nil?
      self
    end

    ##
    # Asynchronously executes the command in context of EventMachine
    #

    def execute
      stdout_r, stdout_w = IO.pipe
      stderr_r, stderr_w = IO.pipe

      child = ChildProcess.build(*final_command)
      child.leader = true
      child.io.stdout = stdout_w
      child.io.stderr = stderr_w

      output = nil
      errors = nil

      final_callback = proc { yield(output, errors, child.wait) unless output.nil? || errors.nil? }
      stdout_callback = proc { |data| output = data; final_callback.call }
      stderr_callback = proc { |data| errors = data; final_callback.call }

      EventMachine.attach(stdout_r, Receiver, stdout_callback)
      EventMachine.attach(stderr_r, Receiver, stderr_callback)

      child.start
      stderr_w.close
      stdout_w.close
    end

    ##
    # Converts command to string.
    # @return [String] command in string form
    #

    def use_shell?
      @shell
    end

    def to_s
      final_command(pretty: true).join(' ')
    end

    alias_method :command, :to_s

    private

    ##
    # Adds argument to command.
    #

    def final_command(pretty: false)
      return @command unless use_shell?

      # This mode is interesting.  ChildProcess seems to require that we have the entire shell command a single parameter.
      # However, if we wrap it in "
      cmd = @command.join(' ')
      cmd = '"' + cmd + '"' if pretty

      shell_command << cmd
    end

    def escape(value)
      Shellwords.escape(value.to_s)
    end

    def format_argument(name, value, short: nil)
      short = short.nil? ? name.length == 1 : short

      argument = "#{short ? separators[0] : separators[2]}#{name}"
      argument += "#{short ? separators[1] : separators[3]}#{escape(value)}" unless value.nil?

      argument
    end

    def shell_command
      ChildProcess.windows? ? ['cmd', '/c'] : ['sh', '-c']
    end
  end
end