//
// Mono.Unix/UnixFileSystemInfo.cs
//
// Authors:
//   Jonathan Pryor (jonpryor@vt.edu)
//
// (C) 2004-2006 Jonathan Pryor
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.IO;
using System.Text;
using Mono.Unix;

namespace Mono.Unix {

	public abstract class UnixFileSystemInfo
	{
		private Native.Stat stat;
		private string fullPath;
		private string originalPath;
		private bool valid = false;

		internal const FileSpecialAttributes AllSpecialAttributes = 
			FileSpecialAttributes.SetUserId | FileSpecialAttributes.SetGroupId |
			FileSpecialAttributes.Sticky;
		internal const FileTypes AllFileTypes = 
			FileTypes.Directory | FileTypes.CharacterDevice | FileTypes.BlockDevice |
			FileTypes.RegularFile | FileTypes.Fifo | FileTypes.SymbolicLink | 
			FileTypes.Socket;

		protected UnixFileSystemInfo (string path)
		{
			UnixPath.CheckPath (path);
			this.originalPath = path;
			this.fullPath = UnixPath.GetFullPath (path);
			Refresh (true);
		}

		internal UnixFileSystemInfo (String path, Native.Stat stat)
		{
			this.originalPath = path;
			this.fullPath = UnixPath.GetFullPath (path);
			this.stat = stat;
			this.valid = true;
		}

		protected string FullPath {
			get {return fullPath;}
			set {
				if (fullPath != value) {
					UnixPath.CheckPath (value);
					valid = false;
					fullPath = value;
				}
			}
		}

		protected string OriginalPath {
			get {return originalPath;}
			set {originalPath = value;}
		}

		private void AssertValid ()
		{
			Refresh (false);
			if (!valid)
				throw new InvalidOperationException ("Path doesn't exist!");
		}

		public virtual string FullName {
			get {return FullPath;}
		}

		public abstract string Name {get;}

		public bool Exists {
			get {
				Refresh (true);
				return valid;
			}
		}

		public long Device {
			get {AssertValid (); return Convert.ToInt64 (stat.st_dev);}
		}

		public long Inode {
			get {AssertValid (); return Convert.ToInt64 (stat.st_ino);}
		}

		[CLSCompliant (false)]
		public Native.FilePermissions Protection {
			get {AssertValid (); return (Native.FilePermissions) stat.st_mode;}
			set {
				int r = Native.Syscall.chmod (FullPath, value);
				UnixMarshal.ThrowExceptionForLastErrorIf (r);
			}
		}

		public FileTypes FileType {
			get {
				AssertValid ();
				return (FileTypes) (stat.st_mode & Native.FilePermissions.S_IFMT);
			}
			// no set as chmod(2) won't accept changing the file type.
		}

		public FileAccessPermissions FileAccessPermissions {
			get {
				AssertValid (); 
				int perms = (int) stat.st_mode;
				return (FileAccessPermissions) (perms & (int) FileAccessPermissions.AllPermissions);
			}
			set {
				AssertValid ();
				int perms = (int) stat.st_mode;
				perms &= (int) ~FileAccessPermissions.AllPermissions;
				perms |= (int) value;
				Protection = (Native.FilePermissions) perms;
			}
		}

		public FileSpecialAttributes FileSpecialAttributes {
			get {
				AssertValid ();
				int attrs = (int) stat.st_mode;
				return (FileSpecialAttributes) (attrs & (int) AllSpecialAttributes);
			}
			set {
				AssertValid ();
				int perms = (int) stat.st_mode;
				perms &= (int) ~AllSpecialAttributes;
				perms |= (int) value;
				Protection = (Native.FilePermissions) perms;
			}
		}

		public long LinkCount {
			get {AssertValid (); return Convert.ToInt64 (stat.st_nlink);}
		}

		public UnixUserInfo OwnerUser {
			get {AssertValid (); return new UnixUserInfo (stat.st_uid);}
		}

		public long OwnerUserId {
			get {AssertValid (); return stat.st_uid;}
		}

		public UnixGroupInfo OwnerGroup {
			get {AssertValid (); return new UnixGroupInfo (stat.st_gid);}
		}

		public long OwnerGroupId {
			get {AssertValid (); return stat.st_gid;}
		}

		public long DeviceType {
			get {AssertValid (); return Convert.ToInt64 (stat.st_rdev);}
		}

		public long Length {
			get {AssertValid (); return (long) stat.st_size;}
		}

		public long BlockSize {
			get {AssertValid (); return (long) stat.st_blksize;}
		}

		public long BlocksAllocated {
			get {AssertValid (); return (long) stat.st_blocks;}
		}

		public DateTime LastAccessTime {
			get {AssertValid (); return Native.NativeConvert.ToDateTime (stat.st_atime, stat.st_atime_nsec);}
		}

		public DateTime LastAccessTimeUtc {
			get {return LastAccessTime.ToUniversalTime ();}
		}

		public DateTime LastWriteTime {
			get {AssertValid (); return Native.NativeConvert.ToDateTime (stat.st_mtime, stat.st_mtime_nsec);}
		}

		public DateTime LastWriteTimeUtc {
			get {return LastWriteTime.ToUniversalTime ();}
		}

		public DateTime LastStatusChangeTime {
			get {AssertValid (); return Native.NativeConvert.ToDateTime (stat.st_ctime, stat.st_ctime_nsec);}
		}

		public DateTime LastStatusChangeTimeUtc {
			get {return LastStatusChangeTime.ToUniversalTime ();}
		}

		public bool IsDirectory {
			get {AssertValid (); return IsFileType (stat.st_mode, Native.FilePermissions.S_IFDIR);}
		}

		public bool IsCharacterDevice {
			get {AssertValid (); return IsFileType (stat.st_mode, Native.FilePermissions.S_IFCHR);}
		}

		public bool IsBlockDevice {
			get {AssertValid (); return IsFileType (stat.st_mode, Native.FilePermissions.S_IFBLK);}
		}

		public bool IsRegularFile {
			get {AssertValid (); return IsFileType (stat.st_mode, Native.FilePermissions.S_IFREG);}
		}

		public bool IsFifo {
			get {AssertValid (); return IsFileType (stat.st_mode, Native.FilePermissions.S_IFIFO);}
		}

		public bool IsSymbolicLink {
			get {AssertValid (); return IsFileType (stat.st_mode, Native.FilePermissions.S_IFLNK);}
		}

		public bool IsSocket {
			get {AssertValid (); return IsFileType (stat.st_mode, Native.FilePermissions.S_IFSOCK);}
		}

		public bool IsSetUser {
			get {AssertValid (); return IsSet (stat.st_mode, Native.FilePermissions.S_ISUID);}
		}

		public bool IsSetGroup {
			get {AssertValid (); return IsSet (stat.st_mode, Native.FilePermissions.S_ISGID);}
		}

		public bool IsSticky {
			get {AssertValid (); return IsSet (stat.st_mode, Native.FilePermissions.S_ISVTX);}
		}

		internal static bool IsFileType (Native.FilePermissions mode, Native.FilePermissions type)
		{
			return (mode & Native.FilePermissions.S_IFMT) == type;
		}

		internal static bool IsSet (Native.FilePermissions mode, Native.FilePermissions type)
		{
			return (mode & type) == type;
		}

		[CLSCompliant (false)]
		public bool CanAccess (Native.AccessModes mode)
		{
			int r = Native.Syscall.access (FullPath, mode);
			return r == 0;
		}

		public UnixFileSystemInfo CreateLink (string path)
		{
			int r = Native.Syscall.link (FullName, path);
			UnixMarshal.ThrowExceptionForLastErrorIf (r);
			return GetFileSystemEntry (path);
		}

		public UnixSymbolicLinkInfo CreateSymbolicLink (string path)
		{
			int r = Native.Syscall.symlink (FullName, path);
			UnixMarshal.ThrowExceptionForLastErrorIf (r);
			return new UnixSymbolicLinkInfo (path);
		}

		public abstract void Delete ();

		[CLSCompliant (false)]
		public long GetConfigurationValue (Native.PathconfName name)
		{
			long r = Native.Syscall.pathconf (FullPath, name);
			if (r == -1 && Native.Stdlib.GetLastError() != (Native.Errno) 0)
				UnixMarshal.ThrowExceptionForLastError ();
			return r;
		}

		public void Refresh ()
		{
			Refresh (true);
		}

		internal void Refresh (bool force)
		{
			if (valid && !force)
				return;
			valid = GetFileStatus (FullPath, out this.stat);
		}

		protected virtual bool GetFileStatus (string path, out Native.Stat stat)
		{
			return Native.Syscall.stat (path, out stat) == 0;
		}

		public void SetLength (long length)
		{
			int r;
			do {
				r = Native.Syscall.truncate (FullPath, length);
			}	while (UnixMarshal.ShouldRetrySyscall (r));
			UnixMarshal.ThrowExceptionForLastErrorIf (r);
		}

		public virtual void SetOwner (long owner, long group)
		{
			uint _owner = Convert.ToUInt32 (owner);
			uint _group = Convert.ToUInt32 (group);
			int r = Native.Syscall.chown (FullPath, _owner, _group);
			UnixMarshal.ThrowExceptionForLastErrorIf (r);
		}

		public void SetOwner (string owner)
		{
			Native.Passwd pw = Native.Syscall.getpwnam (owner);
			if (pw == null)
				throw new ArgumentException (Locale.GetText ("invalid username"), "owner");
			uint uid = pw.pw_uid;
			uint gid = pw.pw_gid;
			SetOwner ((long) uid, (long) gid);
		}

		public void SetOwner (string owner, string group)
		{
			long uid = -1;
			if (owner != null)
				uid = new UnixUserInfo (owner).UserId;
			long gid = -1;
			if (group != null)
				gid = new UnixGroupInfo (group).GroupId;

			SetOwner (uid, gid);
		}

		public void SetOwner (UnixUserInfo owner)
		{
			long uid, gid;
			uid = gid = -1;
			if (owner != null) {
				uid = owner.UserId;
				gid = owner.GroupId;
			}
			SetOwner (uid, gid);
		}

		public void SetOwner (UnixUserInfo owner, UnixGroupInfo group)
		{
			long uid, gid;
			uid = gid = -1;
			if (owner != null)
				uid = owner.UserId;
			if (group != null)
				gid = owner.GroupId;
			SetOwner (uid, gid);
		}

		public override string ToString ()
		{
			return FullPath;
		}

		public Native.Stat ToStat ()
		{
			AssertValid ();
			return stat;
		}

		public static UnixFileSystemInfo GetFileSystemEntry (string path)
		{
			UnixFileSystemInfo info;
			if (TryGetFileSystemEntry (path, out info))
				return info;

			UnixMarshal.ThrowExceptionForLastError ();

			// Throw DirectoryNotFoundException because lstat(2) probably failed
			// because of ENOTDIR (e.g. "/path/to/file/wtf"), so
			// DirectoryNotFoundException is what would have been thrown anyway.
			throw new DirectoryNotFoundException ("UnixMarshal.ThrowExceptionForLastError didn't throw?!");
		}

		public static bool TryGetFileSystemEntry (string path, out UnixFileSystemInfo entry)
		{
			Native.Stat stat;
			int r = Native.Syscall.lstat (path, out stat);
			if (r == -1) {
				if (Native.Stdlib.GetLastError() == Native.Errno.ENOENT) {
					entry = new UnixFileInfo (path);
					return true;
				}
				entry = null;
				return false;
			}

			if (IsFileType (stat.st_mode, Native.FilePermissions.S_IFDIR))
				entry = new UnixDirectoryInfo (path, stat);
			else if (IsFileType (stat.st_mode, Native.FilePermissions.S_IFLNK))
				entry = new UnixSymbolicLinkInfo (path, stat);
			else
				entry = new UnixFileInfo (path, stat);

			return true;
		}
	}
}

// vim: noexpandtab