# A class like tee(1)
class Tee
  # @return [String]
  VERSION = '0.0.2'

  class << self
    # @macro new
    #   @param ios [Array<IO, String>]
    #   @param options [Hash]
    #   @option options [String, Fixnum] :mode ('w')
    #   @option options [Fixnum] :perm (0666)
    #   @option options [IO, nil] :stdout ($stdout)
    #
    # @overload open(*ios, options = {})
    #   A synonym for Tee.new
    #   @macro new
    #   @return [Tee]
    #
    # @overload open(*ios, options = {}, &block)
    #   It will be passed the Tee as an argument,
    #   and the Tee will automatically be closed when the block terminates.
    #   @macro new
    #   @yieldparam tee [Tee]
    #   @return the value of the block
    def open(*args, &block)
      if block_given?
        tee = new(*args)
        begin
          yield tee
        ensure
          tee.send(:close_ios_opened_by_self)
        end
      else
        new(*args)
      end
    end
  end

  # @param value [IO, nil] Sets the attribute stdout
  # @return [IO, nil]      Returns the value of attribute stdout
  attr_accessor :stdout

  # @overload initialize(*ios, options = {})
  #   @macro new
  def initialize(*ios)
    @options = { mode: 'w' }
    @options.update(ios.pop) if ios.last.is_a?(Hash)

    @stdout = @options.key?(:stdout) ? @options.delete(:stdout) : $stdout

    @ios = []
    add(*ios)
  end

  # Add ios
  #
  # @param ios [Array<IO, String>]
  # @return [self]
  def add(*ios)
    open_args = [@options[:mode]]
    open_args << @options[:perm] if @options[:perm]

    _ios = []
    begin
      ios.each do |io|
        _ios << (
          io.respond_to?(:write) ?
            [io, false] :
            [File.open(io, *open_args), true]
        )
      end
    rescue => e
      close_ios_opened_by_self(_ios) rescue nil
      raise e
    end
    @ios.concat(_ios)

    self
  end

  # Delegates #<< to ios
  #
  # @param obj [Object]
  # @return [self]
  def <<(obj)
    each_ios_and_stdout { |io| io << obj }
    self
  end

  # Closes all ios except stdout
  #
  # @return [nil]
  def close
    each_ios(&:close)
    nil
  end

  # Returns true if all ios except stdout is closed, false otherwise.
  #
  # @return [Boolean]
  def closed?
    each_ios.all?(&:closed?)
  end

  # Delegates #flush to ios
  #
  # @return [self]
  def flush
    each_ios_and_stdout(&:flush)
    self
  end

  # Delegates #putc to ios
  #
  # @param char [Fixnum, String]
  # @return [Fixnum]
  # @return [String]
  def putc(char)
    each_ios_and_stdout { |io| io.putc(char) }
    char
  end

  # Returns self
  #
  # @return [self]
  def to_io
    self
  end

  # Delegates #tty? to stdout
  #
  # @return [Boolean]
  def tty?
    @stdout ? @stdout.tty? : false
  end
  alias isatty tty?

  # @method print(obj, ...)
  # Delegates #print to ios
  # @param obj [Object]
  # @return [nil]

  # @method printf(format[, obj, ...])
  # Delegates #printf to ios
  # @param format [String]
  # @param obj [Object]
  # @return [nil]

  # @method puts(obj, ...)
  # Delegates #puts to ios
  # @param obj [Object]
  # @return [nil]
  %w( print printf puts ).each do |method|
    class_eval(<<-EOS, __FILE__, __LINE__ + 1)
      def #{method}(*args)
        each_ios_and_stdout { |io| io.#{method}(*args) }
        nil
      end
    EOS
  end

  # @method syswrite(string)
  # Delegates #syswrite to ios
  # @param string [String]
  # @return [Array<Integer>]

  # @method write(string)
  # Delegates #write to ios
  # @param string [String]
  # @return [Array<Integer>]

  # @method write_nonblock(string)
  # Delegates #write_nonblock to ios
  # @param string [String]
  # @return [Array<Integer>]
  %w( syswrite write write_nonblock ).each do |method|
    class_eval(<<-EOS, __FILE__, __LINE__ + 1)
      def #{method}(string)
        each_ios_and_stdout.map { |io| io.#{method}(string) }
      end
    EOS
  end

  private

  def each_ios(&block)
    return to_enum(:each_ios) unless block_given?
    @ios.each do |io,|
      yield io
    end
    self
  end

  def each_ios_and_stdout(&block)
    return to_enum(:each_ios_and_stdout) unless block_given?
    each_ios(&block)
    yield @stdout if @stdout
    self
  end

  def close_ios_opened_by_self(ios = @ios)
    ios.each { |io, opened| io.close if opened && !io.closed? }
    nil
  end
end