/////////////////////////////////////////////////////////////////////////////
//                   SOFTWARE COPYRIGHT NOTICE AGREEMENT                   //
//       This software and its documentation are copyright (2008) by the   //
//   Broad Institute/Massachusetts Institute of Technology.  All rights    //
//   are reserved.  This software is supplied without any warranty or      //
//   guaranteed support whatsoever. Neither the Broad Institute nor MIT    //
//   can be responsible for its use, misuse, or functionality.             //
/////////////////////////////////////////////////////////////////////////////
/**
 * \file File.h
 * \author aaron
 * \date Wednesday, December 03, 2008
 *
 * \brief A file, as described by a path.
 */
#ifndef SYSTEM_FILE_FILE_H_
#define SYSTEM_FILE_FILE_H_

#include "system/Assert.h"
#include <cstddef>
#include <ostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>

class Directory;
class SymLink;

/**
 * \class File
 * \brief A file, as described by a path.
 *
 * A stat on the file is done on-demand, only when this information is needed.
 */
class File
{
public:
    // make it look container-ish
    typedef std::string::value_type value_type;
    typedef std::string::const_iterator const_iterator;
    typedef std::string::const_reverse_iterator const_reverse_iterator;

    /// Default constructor creates an empty path.
    File() : mType(UNKNOWN), mpStat(0) {}

    /// Construct from a path specified as a c-style string.
    File( char const* path ) : mPath(path), mType(UNKNOWN), mpStat(0)
    { patchPath(); }

    /// Construct from something with a c_str() member (like string or String)
    template <class C>
    explicit File( C const& path, char const*(C::*)() const = &C::c_str )
    : mPath(path.c_str()), mType(UNKNOWN), mpStat(0)
    { patchPath(); }

    /// copy constructor
    File( File const& file ) : mType(UNKNOWN), mpStat(0) { *this = file; }

    /// destructor
    ~File()
    { delete mpStat; }

    /// copy-by-assignment operator
    File& operator=( File const& that )
    { if ( this != &that )
      { mPath = that.mPath; mType = that.mType;
        if ( !that.mpStat ) { delete mpStat; mpStat = 0; }
        else if ( !mpStat ) mpStat = new struct stat(*that.mpStat);
        else *mpStat = *that.mpStat; }
      return *this; }

    /**
     ** Standard container iterators
     **/
    const_iterator begin() const { return mPath.begin(); }
    const_iterator cbegin() const { return mPath.begin(); }
    const_iterator end() const { return mPath.end(); }
    const_iterator cend() const { return mPath.end(); }
    const_reverse_iterator rbegin() const { return mPath.rbegin(); }
    const_reverse_iterator crbegin() const { return mPath.rbegin(); }
    const_reverse_iterator rend() const { return mPath.rend(); }
    const_reverse_iterator crend() const { return mPath.rend(); }

    /// complete path as a C-string
    char const* c_str() const { return mPath.c_str(); }

    /**
     ** Operations that have to do with the File's status
     **/

    /// Test to see if the file exists (i.e., it can be stat'd).
    /// Call with freshen==true to reexecute the call to stat rather than
    /// relying on previously-retrieved values.
    bool exists( bool freshen = false ) const
    { if ( mType==UNKNOWN || freshen ) setStat();
      return mpStat; }

    /// Evaluate the file's status.
    /// Returns null if the file doesn't exist.
    /// Call with freshen==true to reexecute the call to stat rather than
    /// relying on previously-retrieved values.
    struct stat const* stat( bool freshen = false ) const
    { if ( mType==UNKNOWN || freshen ) setStat();
      return mpStat; }

    /// File types enumerated.
    enum FILETYPE
    {
        NOT_A_FILE = 0,
        UNKNOWN = 1,
        FIFO = S_IFIFO,
        CHARACTER_DEVICE = S_IFCHR,
        DIRECTORY = S_IFDIR,
        BLOCK_DEVICE = S_IFBLK,
        REGULAR = S_IFREG,
        SYM_LINK = S_IFLNK,
        SOCKET = S_IFSOCK
    };

    /// Return the filetype.
    /// Returns NOT_A_FILE if stat fails.  No freshen option, because files
    /// don't change type very often.  (You can always overwrite with a copy.)
    FILETYPE type() const
    { if ( mType == UNKNOWN ) setStat();
      return mType; }

    /// Get the type of the file, resolving symbolic links.
    FILETYPE resolvedType() const
    { if ( mType == UNKNOWN ) setStat();
      return mpStat ?
              static_cast<FILETYPE>(mpStat->st_mode & S_IFMT) :
              NOT_A_FILE; }

    /// Is it a directory?
    bool isDir() const { return resolvedType() == DIRECTORY; }

    /// Get a directory object with the same path.
    Directory asDir() const;

    /// Is it a symbolic link?
    bool isLink() const { return type() == SYM_LINK; }

    /// Get a symbolic link object with the same path.
    SymLink asLink() const;

    /// Is it a regular file?
    bool isRegular() const { return resolvedType() == REGULAR; }

    /// Return the size of the file.
    /// Returns -1 if stat fails.
    off_t filesize( bool freshen=false ) const
    { if ( mType==UNKNOWN || freshen ) setStat();
      return mpStat ? mpStat->st_size : -1L; }

    /// Last access time.
    /// Returns 0 if stat fails.
    time_t accessTime( bool freshen=false ) const
    { if ( mType==UNKNOWN || freshen ) setStat();
      return mpStat ? mpStat->st_atime : 0; }

    /// Last modification time.
    /// Returns 0 if stat fails.
    time_t modifyTime( bool freshen=false ) const
    { if ( mType==UNKNOWN || freshen ) setStat();
      return mpStat ? mpStat->st_mtime : 0; }

    /// Last status change time.
    time_t createTime( bool freshen=false ) const
    { if ( mType==UNKNOWN || freshen ) setStat();
      return mpStat ? mpStat->st_ctime : 0; }

    /// Checks to see if two (possibly different) paths refer to same the file.
    bool isSameFile( File const& file ) const
    { struct stat const* pStat1 = stat();
      struct stat const* pStat2 = file.stat();
      return pStat1 && pStat2 &&
              pStat1->st_ino == pStat2->st_ino &&
              pStat1->st_dev == pStat2->st_dev; }

    /**
     ** Operations on the File's path
     **/

    bool isAbsolute() const
    { return mPath.size() && mPath[0]=='/'; }

    bool isExplicitlyRelative() const
    { return mPath.size()>1 && mPath[0]=='.' && mPath[1]=='/'; }

    /// Return the file's path as a string.
    std::string const& toString() const { return mPath; }

    /// Return the filename portion of the path.
    /// E.g., "/home/user/boat/file.txt" would return "file.txt".
    /// Anomaly:  The filename of the root directory is empty.
    std::string filename() const
    { return mPath.substr(filenameOffset()); }

    /// Return the directory portion of the path.
    /// E.g., "/home/user/boat/file.txt" would return "/home/user/boat/",
    /// and barefile.txt would return "./".
    /// Anomaly:  The dirname of the root directory is empty.
    std::string dirname() const
    { size_t pos = filenameOffset();
      return pos ? mPath.substr(0,pos-1) : CWD; }

    /// The directory that contains this file.
    /// I.e., the directory described by dirname().
    Directory directory() const;

    /// Return the final extension (or an empty string, if there is none).
    /// E.g., "/home/user/boat/file.txt" would return ".txt".
    std::string extension() const
    { std::string result;
      size_t start = mPath.find_last_of('.');
      if ( start != std::string::npos && start > filenameOffset() )
          result = mPath.substr(start);
      return result; }

    /// Return a new File with its final extension (if any) removed.
    /// E.g., "/home/user/boat/file.txt" would return "/home/user/boat/file".
    File removeExtension() const
    { size_t end = mPath.find_last_of('.');
      return end != std::string::npos && end > filenameOffset() ?
                      File(mPath.substr(0,end)) : *this; }

    /// Return a new File with a path obtained by appending a new extension.
    /// If the new extension doesn't start with a '.', one is added, unless
    /// the new extension is empty, in which case the path is unaltered.
    File addExtension( std::string newExtension ) const
    { AssertEq(newExtension.find_first_of('/'),std::string::npos);
      size_t xSize = newExtension.size();
      std::string newPath; newPath.reserve(mPath.size()+1+xSize);
      newPath = mPath;
      if ( xSize )
      { if ( newExtension[0] != '.' ) newPath += '.';
        newPath += newExtension; }
      return File(newPath); }

    /// Return a new File with the final extension changed.
    /// Equivalent to removeExtension + addExtension.
    /// E.g., "/user/boat/file.txt" would return "/user/boat/file.newExtension".
    File changeExtension( std::string newExtension ) const
    { AssertEq(newExtension.find_first_of('/'),std::string::npos);
      size_t end = mPath.find_last_of('.');
      if ( end == std::string::npos || end <= filenameOffset() )
          end = mPath.size();
      size_t xSize = newExtension.size();
      std::string newPath; newPath.reserve(end+1+xSize);
      newPath = mPath.substr(0,end);
      if ( xSize )
      { if ( newExtension[0] != '.' ) newPath += '.';
        newPath += newExtension; }
      return File(newPath); }

    /**
     ** Operations on the File itself.
     **/

    /// Remove the file from its directory.
    /// It's a fatal error if it doesn't happen (so consider calling the
    /// exists() method first).
    void remove() const;

    /// Give the file a new name.  If you're crossing filesystems, this might
    /// actually cause the file to be copied (which might be expensive).
    void rename( File const& file ) const;

    /**
     *** Some friends.
     **/

    /// Tests whether the paths are identical.
    friend bool operator==( File const& file1, File const& file2 )
    { return file1.toString() == file2.toString(); }

    /// Tests whether the paths are different.
    friend bool operator!=( File const& file1, File const& file2 )
    { return !(file1 == file2); }

    /// Compares paths lexicographically.
    friend bool operator<( File const& file1, File const& file2 )
    { return file1.toString() < file2.toString(); }

    friend std::ostream& operator<<( std::ostream& s, File const& file )
    { s << file.toString(); return s; }

    static std::string CWD; // i.e., "."
    static std::string ROOT;

private:
    friend class Directory;
    friend class SymLink;

    void patchPath();
    size_t filenameOffset() const
    { return mPath.find_last_of('/')+1; } // trick: +1 turns npos into 0

    void clearStat() const { mType = UNKNOWN; delete mpStat; mpStat = 0; }
    void setStat() const;

    std::string mPath;
    mutable FILETYPE mType;
    mutable struct stat* mpStat;
};

#endif /* SYSTEM_FILE_FILE_H_ */
