
module Sprout::System

  # The abstract base class for all supported system/platform types.
  # In general, users are created by calling the +create+ factory method
  # on the +System+ module.
  #     
  #     System.create
  #
  # Assuming you call the create method, you should wind up with
  # a concrete system that matches your system, and these concrete
  # users will generally be derived from this base class.
  #
  class BaseSystem

    ##
    # Get the home path for a system on a particular operating system.
    #
    # This path will be different, depending on which system owns
    # the curren process, and which operating system they are on.
    #
    def home
      @home ||= find_home
    end

    ##
    # Set the home path for a system on a particular operating system.
    # 
    # If you request the home path before setting it, we will
    # attempt to determine the home path of the current system for
    # the current operating system.
    #
    # This is just a simple way to override the default behavior.
    #
    def home=(home)
      @home = home
    end

    ##
    # Some operating systems (like OS X and Windows) have a 
    # specific location where applications are expected to store
    # files for a particular system. This location is generally
    # a subdirectory of +home+.
    # 
    # The value of this location will usually be overridden in
    # concrete System classes.
    #
    def library
      return home
    end

    ##
    # Instantiate and return a new Sprout::ProcessRunner so
    # that we can execute it.
    #
    def get_process_runner
      Sprout::ProcessRunner.new
    end

    def can_execute? platform
      platform == :universal
    end

    ##
    # Creates a new process, executes the command
    # and returns whatever the process wrote to stdout, or stderr.
    #
    # Raises a +Sprout::Errors::ExecutionError+ if the process writes to stderr
    #
    def execute(tool, options='')
      Sprout.stdout.puts("#{tool} #{options}")
      runner = get_and_execute_process_runner(tool, options)
      error  = runner.read_err
      result = runner.read

      if(result.size > 0)
        Sprout.stdout.puts result
      end

      if(error.size > 0)
        raise Sprout::Errors::ExecutionError.new("[ERROR] #{error}")
      end

      result || error
    end

    ##
    # Creates and returns the process without
    # attempting to read or write to the stream.
    # This is useful for interacting with
    # long-lived CLI processes like FCSH or FDB.
    #
    def execute_silent(tool, options='')
      get_and_execute_process_runner(tool, options)
    end

    ##
    # Execute a new process in a separate thread and yield whatever output
    # is written to its stderr and stdout.
    #
    # @return [Sprout::ProcessRunner]
    # @param tool [File] Path to the executable.
    # @param options [String] The command line options that the executable accepts.
    # @param prompt [Regex] The prompt that will trigger the listener block to be called.
    # @yield [String] Message that was received from the process, called when #prompt is encountered.
    #
    def execute_thread tool, options='', prompt=nil, &block
      t = Thread.new do
        Thread.current.abort_on_exception = true
        runner = execute_silent(tool, options)
        Thread.current['runner'] = runner
        out = read_from runner.r, prompt, &block
        err = read_from runner.e, prompt, &block
        out.join && err.kill
      end

      # Wait for the runner to be created
      # before returning a nil reference
      # that never gets populated...
      while t['runner'].nil? do
        sleep(0.1)
      end

      if !t.alive?
        raise Sprout::Errors::UsageError.new(t['runner'].read_err)
      end

      t
    end

    def read_from pipe, prompt, &block
      Thread.new do
        Thread.current.abort_on_exception = true
        lines = ''
        line = ''
        pipe.sync = true
        pipe.each_char do |char|
          break if pipe.closed?
          line << char

          if line.match prompt
            yield line if block_given?
            lines << line
            lines = ''
            line = ''
            next
          end

          if char == "\n"
            lines << line
            yield line if block_given?
            line = ''
          end
        end
      end
    end

    ##
    # Clean the provided +path+ String for the current
    # operating system.
    #
    # Each operating system behaves differently when we
    # attempt to execute a file with spaces in the +path+
    # to the file.
    # 
    # Subclasses will generally override this method and
    # clean the path appropriately for their operating 
    # system.
    #
    def clean_path(path)
    end

    ##
    # Different operating systems will store Application data
    # different default locations.
    #
    # Subclasses will generally override this method and 
    # return the appropriate location for their operating system.
    #
    # +name+ String value of the Application name for which we'd
    # like to store data.
    #
    def application_home(name)
      return File.join(library, format_application_name(name.to_s));
    end

    ##
    # Template method that should be overridden by
    # subclasses.
    #
    def format_application_name(name)
      name
    end

    protected

    def env_homedrive
      ENV['HOMEDRIVE']
    end

    def env_homepath
      ENV['HOMEPATH']
    end

    def env_homedrive_and_homepath
      drive = env_homedrive
      path = env_homepath
      "#{drive}:#{path}" if drive && path
    end

    def env_userprofile
      ENV['USERPROFILE']
    end

    def env_home
      ENV['HOME']
    end

    def tilde_home
      File.expand_path("~")
    end

    def alt_separator?
      File::ALT_SEPARATOR
    end

    def worst_case_home
      return "C:\\" if alt_separator?
      return "/"
    end

    def find_home
      [:env_userprofile, :env_home, :env_homedrive_and_homepath].each do |key|
        value = self.send(key)
        return value unless value.nil?
      end

      begin
        return tilde_home
      rescue StandardError
        worst_case_home
      end
    end

    protected

    ##
    # Get a process runner and execute the provided +executable+,
    # with the provided +options+.
    #
    # +executable+ String path to the external executable file.
    #
    # +options+ String commandline options to send to the +executable+.
    #
    def get_and_execute_process_runner tool, options=nil
      runner = get_process_runner
      runner.execute_open4 clean_path(tool), options
      runner
    end

  end

  class ThreadMock
    def alive?
      return false
    end
  end
end

