require 'fileutils'
require 'reap/extensions'
require 'facets/ziputils'

module Reap

  module Utilities

    attr_accessor :dryrun
    attr_accessor :trace
    attr_accessor :force
    attr_accessor :verbose

    def dryrun?  ; @dryrun  ; end
    def trace?   ; @trace   ; end
    def force?   ; @force   ; end
    def verbose? ; @verbose ; end

    # Internal status report.
    # Only output if dryrun or trace mode.

    def status(message)
      if dryrun? or trace?
        puts message
      end
    end

    # Shell runner.

    def sh(cmd)
      if dryrun?
        puts cmd
        true
      else
        puts "--> system call: #{cmd}" if trace?
        system(cmd)
      end
    end

    # Convenient method to get simple console reply.

    def ask(question, answers=nil)
      print "#{question}"
      print " [#{answers}] " if answers
      until inp = $stdin.gets ; sleep 1 ; end
      inp.strip
    end

    # Ask for a password. (FIXME: only for unix so far)

    def password(prompt=nil)
      msg ||= "Enter Password: "
      inp = ''

      print "#{prompt} "

      begin
        #system "stty -echo"
        #inp = gets.chomp
        until inp = $stdin.gets
          sleep 1
        end
      ensure
        #system "stty echo"
      end

      return inp.chomp
    end

    # Delegate access to FileUtils.

    def fileutils
      dryrun? ? ::FileUtils::DryRun : ::FileUtils
    end

    # Add FileUtils Features

    ::FileUtils.private_instance_methods(false).each do |meth|
      next if meth =~ /^fu_/
      module_eval %{
        def #{meth}(*a,&b)
          fileutils.#{meth}(*a,&b)
        end
      }
    end

    # Add FileTest Features

    ::FileTest.private_instance_methods(false).each do |meth|
      next if meth =~ /^fu_/
      module_eval %{
        def #{meth}(*a,&b)
          FileTest.#{meth}(*a,&b)
        end
      }
    end

    # Specific.

    def rm_r(*a)
      if dryrun?
        puts "rm_r #{a.join(' ')}"
      else
        fileutils.rm_r(*a)
      end
    end

    # Bonus FileUtils features.

    def cd(*a,&b)
      puts "cd #{a}" if dryrun? or trace?
      fileutils.chdir(*a,&b)
    end

    # Read file.

    def read(path)
      File.read(path)
    end

    # Write file.

    def write(path, text)
      if dryrun?
        puts "write #{path}"
      else
        File.open(path, 'w'){ |f| f << text }
      end
    end

    # Assert that a path exists.

    def exists?(path)
      paths = Dir.glob(path)
      paths.not_empty?
    end
    alias_method :exist?, :exists? ; module_function :exist?
    alias_method :path?,  :exists? ; module_function :path?

    # Assert that a path exists.

    def exists!(*paths)
      abort "path not found #{path}" unless paths.any?{|path| exists?(path)}
    end
    alias_method :exist!, :exists! ; module_function :exist!
    alias_method :path!,  :exists! ; module_function :path!

    # Is a given path a regular file? If +path+ is a glob
    # then checks to see if all matches are refular files.

    def file?(path)
      paths = Dir.glob(path)
      paths.not_empty? && paths.all?{ |f| FileTest.file?(f) }
    end

    # Assert that a given path is a file.

    def file!(*paths)
      abort "file not found #{path}" unless paths.any?{|path| file?(path)}
    end

    # Is a given path a directory? If +path+ is a glob
    # checks to see if all matches are directories.

    def dir?(path)
      paths = Dir.glob(path)
      paths.not_empty? && paths.all?{ |f| FileTest.directory?(f) }
    end
    alias_method :directory?, :dir? ; module_function :directory?

    # Assert that a given path is a directory.

    def dir!(*paths)
      paths.each do |path|
        abort "Directory not found: '#{path}'." unless  dir?(path)
      end
    end
    alias_method :directory!, :dir! ; module_function :directory!

    # Return a cached list of the PATH environment variable.
    # This is a support method used by #bin?

    def command_paths
      @command_paths ||= ENV['PATH'].split(/[:;]/)
    end

    # Is a file a command executable?
    #
    # TODO: Make more robust. Probably needs to be fixed for Windows.

    def bin?(fname)
      #@command_paths ||= ENV['PATH'].split(/[:;]/)
      is_bin = command_paths.any? do |f|
        FileTest.exist?(File.join(f, fname))
      end
      #is_bin ? File.basename(fname) : false
      is_bin ? fname : false
    end

    # Is a path considered reasonably "safe"?
    #
    # TODO: Make more robust.

    def safe?(path)
      case path
      when *[ '/', '/*', '/**/*' ]
        return false
      end
      true
    end

    # Does a path need updating, based on given +sources+?
    # This compares mtimes of give paths. Returns false
    # if the path needs to be updated.

    def out_of_date?(path, *sources)
      return true unless File.exist?(path)

      sources = sources.collect{ |source| Dir.glob(source) }.flatten
      mtimes  = sources.collect{ |file| File.mtime(file) }

      return true if mtimes.empty?  # TODO: This the way to go here?

      File.mtime(path) < mtimes.max
    end

    # Glob files.

    def glob(*args, &blk)
      Dir.glob(*args, &blk)
    end

    def multiglob(*args, &blk)
      Dir.multiglob(*args, &blk)
    end

    def multiglob_r(*args, &blk)
      Dir.multiglob_r(*args, &blk)
    end

    # Stage package by hard linking included files to a stage directory.
    # Stage files in a directory.
    #
    #   stage_directory       Stage directory.
    #   files                 Files to link to stage.

    def stage(stage_directory, files)
      return stage_directory if dryrun?           # Don't link to stage if dryrun.

      if File.directory?(stage_directory)         # Ensure existance of staging area.
        #raise(???Error, stage_directory) unless force?
        rm_r(stage_directory)
      end

      mkdir_p(stage_directory)                    #dir = File.expand_path(stage)

      #files = package.filelist  #+ [package.manifest_file]

      # TODO Dryrun test here or before folder creation?
      files.each do |f|                 # Link files into staging area.
        file = File.join(stage_directory, f)
        if File.directory?(f)
          mkdir_p(file)
        else
          unless File.exist?(file) and File.mtime(file) >= File.mtime(f)
            ln(f, file) #safe_ln ?
          end
        end
      end

      # stage manifest ?

      return stage_directory
    end

    # Create manifest for a directory.

    def stage_manifest(directory)
      cd(directory) do
        #sh 'manifest up'
        files = Dir['**/*']
        File.open('MANIFEST', 'w'){|f| f << file.join("\n") }
      end
    end

    # Delegate access to ZipUtils.

    def ziputils
      dryrun? ? ::ZipUtils::DryRun : ::ZipUtils
    end

    # Zip folder into file.

    def zip(folder, file=nil, options={})
      ziputils.zip(folder, file, options)
    end

    # BZip and tarball folder into file.

    def tar_bzip(folder, file=nil, options={})
      ziputils.tar_bzip(folder, file, options)
    end

    # GZip and tarball folder into file. Shortcut for ziputils.tgz.

    def tgz(folder, file=nil, options={})
      ziputils.tgz(folder, file, options)
    end


    # Email function to easily send out an email.
    #
    # Settings:
    #
    #     subject      Subject of email message.
    #     from         Message FROM address [email].
    #     to           Email address to send announcemnt.
    #     server       Email server to route message.
    #     port         Email server's port.
    #     port_secure  Email server's port.
    #     domain       Email server's domain name.
    #     account      Email account name if needed.
    #     password     Password for login..
    #     login        Login type: plain, cram_md5 or login [plain].
    #     secure       Uses TLS security, true or false? [false]
    #     message      Mesage to send -or-
    #     file         File that contains message.

    def email(message, settings)
      settings ||= {}
      settings.rekey

      server    = settings[:server]
      account   = settings[:account]  || ENV['EMAIL_ACCOUNT']
      passwd    = settings[:password] || ENV['EMAIL_PASSWORD']
      login     = settings[:login]
      subject   = settings[:subject]
      mail_to   = settings[:to]     || settings[:mail_to]
      mail_from = settings[:from]   || settings[:mail_from]
      secure    = settings[:secure]
      domain    = settings[:domain] || server

      port      = (settings[:port_secure] || 465) if secure
      port      = (settings[:port] || 25) unless secure

      account ||= mail_from
      login   ||= :plain

      login = login.to_sym

      #mail_to = nil if mail_to.empty?

      raise ArgumentError, "missing email field -- server"  unless server
      raise ArgumentError, "missing email field -- account" unless account
      raise ArgumentError, "missing email field -- subject" unless subject
      raise ArgumentError, "missing email field -- to"      unless mail_to
      raise ArgumentError, "missing email field -- from"    unless mail_from

      passwd ||= password("#{account} password:")

      mail_to = [mail_to].flatten.compact

      msg = ""
      msg << "From: #{mail_from}\n"
      msg << "To: #{mail_to.join(';')}\n"
      msg << "Subject: #{subject}\n"
      msg << ""
      msg << message

      if secure
        Net::SMTP.send(:include, Net::SMTP::TLS)
        Net::SMTP.enable_tls #if secure #if Net::SMTP.respond_to?(:enable_tls) and secure
      end

      begin
        Net::SMTP.start(server, port, domain, account, passwd, login) do |smtp|
          smtp.send_message(msg, mail_from, mail_to)
        end
        puts "Email sent successfully to #{mail_to.join(';')}."
        return true
      rescue => e
        if trace?
          raise e
        else
          abort "Email delivery failed."
        end
      end
    end

  end

end