require 'epitools'

class Path
  
  ## initializers

  def initialize(newpath)
    self.path = newpath
  end

  def self.glob(str)
    Dir[str].map { |entry| new(entry) }
  end
  
  def self.[](path)
    case path
    when Path
      path
    when String
      if path =~ %r{^[a-z\-]+://}i # URL?
        Path::URL.new(path)
      elsif path =~ /[\?\*]/ and not path =~ /\\[\?\*]/  # contains glob chars? (unescaped) 
        glob(path)
      else
        new(path)
      end
    end      
  end

  ## setters
  
  attr_writer :base
  attr_writer :dirs
  
  def path=(newpath)
    if File.exists? newpath
      if File.directory? newpath
        self.dir = newpath
      else
        self.dir, self.filename = File.split(newpath)
      end
    else
      if newpath.endswith(File::SEPARATOR) # ends in '/'
        self.dir = newpath
      else 
        self.dir, self.filename = File.split(newpath)
      end
    end
  end
  
  def filename=(newfilename)
    if newfilename.nil?
      @ext, @base = nil, nil
    else
      ext = File.extname(newfilename)
      
      if ext.blank?
        @ext = nil
        @base = newfilename
      else
        self.ext = ext
        if pos = newfilename.rindex(ext)
          @base = newfilename[0...pos]
        end
      end
    end
  end
   
  def dir=(newdir)
    @dirs = File.expand_path(newdir).split(File::SEPARATOR)[1..-1]
  end
  
  def ext=(newext)
    if newext.blank?
      @ext = nil
    elsif newext.startswith('.')
      @ext = newext[1..-1]
    else
      @ext = newext
    end
  end

  ## getters
   
  # The directories in the path, split into an array. (eg: ['usr', 'src', 'linux'])
  attr_reader :dirs   
  
  # The filename without an extension 
  attr_reader :base
  
  # The file extension, including the . (eg: ".mp3") 
  attr_reader :ext

  # Joins and returns the full path  
  def path
    if d = dir
      File.join(d, (filename || "") )
    else
      ""
    end
  end
  
  # The current directory (with a trailing /)
  def dir
    if dirs 
      File::SEPARATOR + File.join(*dirs)
    else
      nil
    end
  end
  
  def filename
    if base
      if ext
        base + "." + ext
      else
        base
      end
    else
      nil
    end
  end

  def exts
    extensions = basename.split('.')[1..-1]
    extensions += [@ext] if @ext
    extensions
  end
  
  ## fstat info
  
  def exists?
    File.exists? path
  end

  def size
    File.size path
  end
  
  def mtime
    File.mtime path
  end
  
  def ctime
    File.ctime path
  end
  
  def atime
    File.atime path
  end
  
  def dir?
    File.directory? path
  end
  
  def file?
    File.file? path
  end
  
  def symlink?
    File.symlink? path
  end
  
  def uri?
    false
  end
  
  def url?
    uri?
  end
  
  
  ## aliases
  
  alias_method :to_path,    :path
  alias_method :to_str,     :path
  alias_method :to_s,       :path

  alias_method :pathname,   :path
  alias_method :basename,   :base
  alias_method :basename=,  :base=
  alias_method :extname,    :ext
  alias_method :extname=,   :ext=
  alias_method :dirname,    :dir
  alias_method :dirname=,   :dir=
  alias_method :extension,  :ext
  alias_method :extension=, :ext=
  alias_method :directory,  :dir
  alias_method :directory=, :dir=

  alias_method :directory?, :dir?
  
  ## comparisons

  include Comparable
  
  def <=>(other)
    self.path <=> other.path
  end

  
  ## appending
  
  #
  # Path["/etc"]/"passwd" == Path["/etc/passwd"]
  #
  def /(other)
    Path.new( File.join(self, other) )
  end  
  
  ## opening/reading files
  
  def open(mode="rb", &block)
    if block_given?
      File.open(path, mode, &block)
    else
      File.open(path, mode)
    end
  end
  alias_method :io, :open
  alias_method :stream, :open
  
  def read(length=nil, offset=nil)
    File.read(path, length, offset)
  end
  
  def ls; Path[File.join(path, "*")]; end

  def ls_r; Path[File.join(path, "**/*")]; end

  
  ## modifying files

  #
  # Append
  #
  def append(data=nil)
    self.open("ab") do |f|
      if data and not block_given?
        f.write(data)
      else
        yield f
      end
    end    
  end
  alias_method :<<, :append
  
  #
  # Write a string, truncating the file
  #
  def write(data=nil)
    self.open("wb") do |f|
      if data and not block_given?
        f.write(data)
      else
        yield f
      end
    end    
  end
  
  #
  # Examples:
  #   Path["SongySong.mp3"].rename(:basename=>"Songy Song")
  #   Path["Songy Song.mp3"].rename(:ext=>"aac")
  #   Path["Songy Song.aac"].rename(:dir=>"/music2")
  #   Path["/music2/Songy Song.aac"].exists? #=> true
  #  
  def rename(options)
    raise "Options must be a Hash" unless options.is_a? Hash
    dest = self.with(options)
    
    raise "Error: destination (#{dest.inspect}) already exists" if dest.exists?
    File.rename(path, dest)
    
    self.path = dest.path # become dest
  end

  #
  # Renames the file the specified full path (like Dir.rename.)
  #  
  def rename_to(path)
    rename :path=>path
  end
  alias_method :mv,       :rename_to
  
  {
    :mkdir => "Dir.mkdir", 
    :mkdir_p =>"FileUtils.mkdir_p"
  }.each do |method, expression|
    class_eval %{
      def #{method}
        if exists?
          if directory?
            false
          else
            raise "Error: Tried to make a directory over top of an existing file."
          end
        else
          #{expression}(path)
          true
        end
      end
    }
  end

  def cp_r(dest)
    FileUtils.cp_r(path, dest) #if Path[dest].exists?
  end
  
  def join(other)
    if uri?
      Path[URI.join(path, other).to_s]
    else
      Path[File.join(path, other)]
    end
  end

  def ln_s(dest)
    dest = Path[dest]
    FileUtils.ln_s self, dest 
  end

  ## Dangerous methods.
  
  def rm
    if directory? and not symlink?
      Dir.rmdir(self) == 0
    else
      File.unlink(self) == 1
    end
  end
  alias_method :"delete!", :rm
  alias_method :"unlink!", :rm
  alias_method :"remove!", :rm
  
  def truncate(offset=0)
    File.truncate(self, offset)
  end

  
  ## Checksums
  
  def sha1
    Digest::SHA1.file(self).hexdigest
  end
  
  def sha2
    Digest::SHA2.file(self).hexdigest
  end
  
  def md5
    Digest::MD5.file(self).hexdigest
  end
  alias_method :md5sum, :md5

  
  # http://ruby-doc.org/stdlib/libdoc/zlib/rdoc/index.html
  
  def gzip
    gz_filename = self.with(:filename=>filename+".gz")
    
    raise "#{gz_filename} already exists" if gz_filename.exists? 
  
    open("rb") do |input|    
      Zlib::GzipWriter.open(gz_filename) do |gzip|
        IO.copy_stream(input, gzip)
      end
    end
    
    gz_filename
  end
  
  def gzip!
    gzipped = self.gzip
    self.rm
    self.path = gzipped.path
  end
  
  def gunzip
    raise "Not a .gz file" unless ext == "gz"

    gunzipped = self.with(:ext=>nil)
    
    gunzipped.open("wb") do |out|
      Zlib::GzipReader.open(self) do |gunzip|
        IO.copy_stream(gunzip, out)
      end
    end
    
    gunzipped
  end

  def gunzip!
    gunzipped = self.gunzip
    self.rm
    self.path = gunzipped.path
  end

  #
  # Return the IO object for this file.
  #
  def io
    open
  end
  alias_method :stream, :io
  
  def =~(pattern)
    path =~ pattern
  end
  
  ## Class method versions of FileUtils-like things
  
  %w[
    mkdir
    mkdir_p 
    sha1 
    sha2 
    md5
    rm
    truncate
  ].each do |method|
    class_eval %{
      def self.#{method}(path)
        Path[path].#{method}
      end
    }
  end

  
  # Mimetype finding and magic (requires 'mimemagic' gem)

  #
  # Find the file's mimetype (first from file extension, then by magic)
  #  
  def mimetype
    mimetype_from_ext || magic
  end
  alias_method :identify, :mimetype
    
  #
  # Find the file's mimetype (only using the file extension)
  #  
  def mimetype_from_ext
    MimeMagic.by_extension(ext)
  end
  
  #
  # Find the file's mimetype (by magic)
  #  
  def magic
    open { |io| MimeMagic.by_magic(io) }
  end
  
  def ext_by_magic
    # TODO: return the extension for the mime type.
    raise NotImplementedError
  end

  ############################################################################
  ## Class Methods

  #
  # TODO: Remove the tempfile when the Path object is garbage collected or freed.
  #
  def self.tmpfile(prefix="tmp")
    path = Path[ Tempfile.new(prefix).path ]
    yield path if block_given?
    path
  end
  alias_class_method :tempfile, :tmpfile  
  
  def self.home
    Path[ENV['HOME']]
  end
  
  def self.pwd
    File.expand_path Dir.pwd
  end
  
  def self.pushd
    @@dir_stack ||= []
    @@dir_stack.push pwd
  end
  
  def self.popd
    @@dir_stack ||= [pwd]
    @@dir_stack.pop
  end
  
  def self.cd(dest); Dir.chdir(dest); end
  
  def self.ls(path); Path[path].ls  end
  
  def self.ls_r(path); Path[path].ls_r; end
  
  def self.ln_s(src, dest); Path[src].ln_s(dest); end

  ## TODO: Verbose mode
  #def self.verbose=(value); @@verbose = value; end
  #def self.verbose; @@verbose ||= false; end
  
  if Sys.windows?
    PATH_SEPARATOR    = ";"
    BINARY_EXTENSION  = ".exe"
  else
    PATH_SEPARATOR    = ":"
    BINARY_EXTENSION  = ""
  end
  
  def self.which(bin)
    ENV["PATH"].split(PATH_SEPARATOR).find do |path|
      result = (Path[path] / (bin + BINARY_EXTENSION))
      return result if result.exists?
    end
    nil
  end  
  
end


#
# A wrapper for URL objects.
#
class Path::URL < Path

  attr_reader :uri
  
  def initialize(uri)
    @uri = URI.parse(uri)
    self.path = @uri.path
  end
  
  def uri?
    true
  end
  
  def host
    uri.host
  end
  
  def query
    if query = uri.query
      query.to_params
    else
      nil
    end
  end
  
  def to_s
    uri.to_s
  end
  
end


#
# Path("/some/path") is an alias for Path["/some/path"]
#
def Path(*args)
  Path[*args]
end

if $0 == __FILE__
  require 'ruby-debug'
  #Path.pry
  #Path["http://google.com/"].pry
  debugger
  Path["?"]
end