//
// Copyright (c) 2008-2011, Kenneth Bell
//
// 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.
//

namespace DiscUtils.Ntfs
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    internal class File
    {
        protected INtfsContext _context;

        private MasterFileTable _mft;
        private List<FileRecord> _records;
        private ObjectCache<string, Index> _indexCache;
        private List<NtfsAttribute> _attributes;

        private bool _dirty;

        public File(INtfsContext context, FileRecord baseRecord)
        {
            _context = context;
            _mft = _context.Mft;
            _records = new List<FileRecord>();
            _records.Add(baseRecord);
            _indexCache = new ObjectCache<string, Index>();
            _attributes = new List<NtfsAttribute>();

            LoadAttributes();
        }

        public uint IndexInMft
        {
            get { return _records[0].MasterFileTableIndex; }
        }

        public uint MaxMftRecordSize
        {
            get { return _records[0].AllocatedSize; }
        }

        public FileRecordReference MftReference
        {
            get { return _records[0].Reference; }
        }

        public List<string> Names
        {
            get
            {
                List<string> result = new List<string>();

                if (IndexInMft == MasterFileTable.RootDirIndex)
                {
                    result.Add(string.Empty);
                }
                else
                {
                    foreach (StructuredNtfsAttribute<FileNameRecord> attr in GetAttributes(AttributeType.FileName))
                    {
                        string name = attr.Content.FileName;

                        Directory parentDir = _context.GetDirectoryByRef(attr.Content.ParentDirectory);
                        if (parentDir != null)
                        {
                            foreach (string dirName in parentDir.Names)
                            {
                                result.Add(Utilities.CombinePaths(dirName, name));
                            }
                        }
                    }
                }

                return result;
            }
        }

        public bool HasWin32OrDosName
        {
            get
            {
                foreach (StructuredNtfsAttribute<FileNameRecord> attr in GetAttributes(AttributeType.FileName))
                {
                    FileNameRecord fnr = attr.Content;
                    if (fnr.FileNameNamespace != FileNameNamespace.Posix)
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        public bool MftRecordIsDirty
        {
            get
            {
                return _dirty;
            }
        }

        public ushort HardLinkCount
        {
            get { return _records[0].HardLinkCount; }
            set { _records[0].HardLinkCount = value; }
        }

        public IEnumerable<NtfsStream> AllStreams
        {
            get
            {
                foreach (var attr in _attributes)
                {
                    yield return new NtfsStream(this, attr);
                }
            }
        }

        public DirectoryEntry DirectoryEntry
        {
            get
            {
                if (_context.GetDirectoryByRef == null)
                {
                    return null;
                }

                NtfsStream stream = GetStream(AttributeType.FileName, null);
                if (stream == null)
                {
                    return null;
                }

                FileNameRecord record = stream.GetContent<FileNameRecord>();

                // Root dir is stored without root directory flag set in FileNameRecord, simulate it.
                if (_records[0].MasterFileTableIndex == MasterFileTable.RootDirIndex)
                {
                    record.Flags |= FileAttributeFlags.Directory;
                }

                return new DirectoryEntry(_context.GetDirectoryByRef(record.ParentDirectory), MftReference, record);
            }
        }

        public string BestName
        {
            get
            {
                NtfsAttribute[] attrs = GetAttributes(AttributeType.FileName);

                string bestName = null;

                if (attrs != null && attrs.Length != 0)
                {
                    bestName = attrs[0].ToString();

                    for (int i = 1; i < attrs.Length; ++i)
                    {
                        string name = attrs[i].ToString();

                        if (Utilities.Is8Dot3(bestName))
                        {
                            bestName = name;
                        }
                    }
                }

                return bestName;
            }
        }

        public bool IsDirectory
        {
            get { return (_records[0].Flags & FileRecordFlags.IsDirectory) != 0; }
        }

        public StandardInformation StandardInformation
        {
            get { return GetStream(AttributeType.StandardInformation, null).GetContent<StandardInformation>(); }
        }

        internal INtfsContext Context
        {
            get
            {
                return _context;
            }
        }

        /// <summary>
        /// Gets an enumeration of all the attributes.
        /// </summary>
        internal IEnumerable<NtfsAttribute> AllAttributes
        {
            get { return _attributes; }
        }

        public static File CreateNew(INtfsContext context, FileAttributeFlags dirFlags)
        {
            return CreateNew(context, FileRecordFlags.None, dirFlags);
        }

        public static File CreateNew(INtfsContext context, FileRecordFlags flags, FileAttributeFlags dirFlags)
        {
            File newFile = context.AllocateFile(flags);

            FileAttributeFlags fileFlags =
                FileAttributeFlags.Archive
                | FileRecord.ConvertFlags(flags)
                | (dirFlags & FileAttributeFlags.Compressed);

            AttributeFlags dataAttrFlags = AttributeFlags.None;
            if ((dirFlags & FileAttributeFlags.Compressed) != 0)
            {
                dataAttrFlags |= AttributeFlags.Compressed;
            }

            StandardInformation.InitializeNewFile(newFile, fileFlags);

            if (context.ObjectIds != null)
            {
                Guid newId = CreateNewGuid(context);
                NtfsStream stream = newFile.CreateStream(AttributeType.ObjectId, null);
                ObjectId objId = new ObjectId();
                objId.Id = newId;
                stream.SetContent(objId);
                context.ObjectIds.Add(newId, newFile.MftReference, newId, Guid.Empty, Guid.Empty);
            }

            newFile.CreateAttribute(AttributeType.Data, dataAttrFlags);

            newFile.UpdateRecordInMft();

            return newFile;
        }

        public int MftRecordFreeSpace(AttributeType attrType, string attrName)
        {
            foreach (var record in _records)
            {
                if (record.GetAttribute(attrType, attrName) != null)
                {
                    return _mft.RecordSize - record.Size;
                }
            }

            throw new IOException("Attempt to determine free space for non-existent attribute");
        }

        public void Modified()
        {
            DateTime now = DateTime.UtcNow;

            NtfsStream siStream = GetStream(AttributeType.StandardInformation, null);
            StandardInformation si = siStream.GetContent<StandardInformation>();
            si.LastAccessTime = now;
            si.ModificationTime = now;
            siStream.SetContent(si);

            MarkMftRecordDirty();
        }

        public void Accessed()
        {
            DateTime now = DateTime.UtcNow;

            NtfsStream siStream = GetStream(AttributeType.StandardInformation, null);
            StandardInformation si = siStream.GetContent<StandardInformation>();
            si.LastAccessTime = now;
            siStream.SetContent(si);

            MarkMftRecordDirty();
        }

        public void MarkMftRecordDirty()
        {
            _dirty = true;
        }

        public void UpdateRecordInMft()
        {
            if (_dirty)
            {
                if (NtfsTransaction.Current != null)
                {
                    NtfsStream stream = GetStream(AttributeType.StandardInformation, null);
                    StandardInformation si = stream.GetContent<StandardInformation>();
                    si.MftChangedTime = NtfsTransaction.Current.Timestamp;
                    stream.SetContent(si);
                }

                bool fixesApplied = true;
                while (fixesApplied)
                {
                    fixesApplied = false;

                    for (int i = 0; i < _records.Count; ++i)
                    {
                        var record = _records[i];

                        bool fixedAttribute = true;
                        while (record.Size > _mft.RecordSize && fixedAttribute)
                        {
                            fixedAttribute = false;

                            if (!fixedAttribute && !record.IsMftRecord)
                            {
                                foreach (var attr in record.Attributes)
                                {
                                    if (!attr.IsNonResident && !_context.AttributeDefinitions.MustBeResident(attr.AttributeType))
                                    {
                                        MakeAttributeNonResident(new AttributeReference(record.Reference, attr.AttributeId), (int)attr.DataLength);
                                        fixedAttribute = true;
                                        break;
                                    }
                                }
                            }

                            if (!fixedAttribute)
                            {
                                foreach (var attr in record.Attributes)
                                {
                                    if (attr.AttributeType == AttributeType.IndexRoot
                                        && ShrinkIndexRoot(attr.Name))
                                    {
                                        fixedAttribute = true;
                                        break;
                                    }
                                }
                            }

                            if (!fixedAttribute)
                            {
                                if (record.Attributes.Count == 1)
                                {
                                    fixedAttribute = SplitAttribute(record);
                                }
                                else
                                {
                                    if (_records.Count == 1)
                                    {
                                        CreateAttributeList();
                                    }

                                    fixedAttribute = ExpelAttribute(record);
                                }
                            }

                            fixesApplied |= fixedAttribute;
                        }
                    }
                }

                _dirty = false;
                foreach (var record in _records)
                {
                    _mft.WriteRecord(record);
                }
            }
        }

        public Index CreateIndex(string name, AttributeType attrType, AttributeCollationRule collRule)
        {
            Index.Create(attrType, collRule, this, name);
            return GetIndex(name);
        }

        public Index GetIndex(string name)
        {
            Index idx = _indexCache[name];

            if (idx == null)
            {
                idx = new Index(this, name, _context.BiosParameterBlock, _context.UpperCase);
                _indexCache[name] = idx;
            }

            return idx;
        }

        public void Delete()
        {
            if (_records[0].HardLinkCount != 0)
            {
                throw new InvalidOperationException("Attempt to delete in-use file: " + ToString());
            }

            _context.ForgetFile(this);

            NtfsStream objIdStream = GetStream(AttributeType.ObjectId, null);
            if (objIdStream != null)
            {
                ObjectId objId = objIdStream.GetContent<ObjectId>();
                Context.ObjectIds.Remove(objId.Id);
            }

            // Truncate attributes, allowing for truncation silently removing the AttributeList attribute
            // in some cases (large file with all attributes first extent in the first MFT record).  This
            // releases all allocated clusters in most cases.
            List<NtfsAttribute> truncateAttrs = new List<NtfsAttribute>(_attributes.Count);
            foreach (var attr in _attributes)
            {
                if (attr.Type != AttributeType.AttributeList)
                {
                    truncateAttrs.Add(attr);
                }
            }

            foreach (NtfsAttribute attr in truncateAttrs)
            {
                attr.GetDataBuffer().SetCapacity(0);
            }

            // If the attribute list record remains, free any possible clusters it owns.  We've now freed
            // all clusters.
            NtfsAttribute attrList = GetAttribute(AttributeType.AttributeList, null);
            if (attrList != null)
            {
                attrList.GetDataBuffer().SetCapacity(0);
            }

            // Now go through the MFT records, freeing them up
            foreach (var mftRecord in _records)
            {
                _context.Mft.RemoveRecord(mftRecord.Reference);
            }

            _attributes.Clear();
            _records.Clear();
        }

        public bool StreamExists(AttributeType attrType, string name)
        {
            return GetStream(attrType, name) != null;
        }

        public NtfsStream GetStream(AttributeType attrType, string name)
        {
            foreach (NtfsStream stream in GetStreams(attrType, name))
            {
                return stream;
            }

            return null;
        }

        public IEnumerable<NtfsStream> GetStreams(AttributeType attrType, string name)
        {
            foreach (var attr in _attributes)
            {
                if (attr.Type == attrType && attr.Name == name)
                {
                    yield return new NtfsStream(this, attr);
                }
            }
        }

        public NtfsStream CreateStream(AttributeType attrType, string name)
        {
            return new NtfsStream(this, CreateAttribute(attrType, name, AttributeFlags.None));
        }

        public NtfsStream CreateStream(AttributeType attrType, string name, long firstCluster, ulong numClusters, uint bytesPerCluster)
        {
            return new NtfsStream(this, CreateAttribute(attrType, name, AttributeFlags.None, firstCluster, numClusters, bytesPerCluster));
        }

        public SparseStream OpenStream(AttributeType attrType, string name, FileAccess access)
        {
            NtfsAttribute attr = GetAttribute(attrType, name);
            if (attr != null)
            {
                return new FileStream(this, attr, access);
            }

            return null;
        }

        public void RemoveStream(NtfsStream stream)
        {
            RemoveAttribute(stream.Attribute);
        }

        public FileNameRecord GetFileNameRecord(string name, bool freshened)
        {
            NtfsAttribute[] attrs = GetAttributes(AttributeType.FileName);
            StructuredNtfsAttribute<FileNameRecord> attr = null;
            if (String.IsNullOrEmpty(name))
            {
                if (attrs.Length != 0)
                {
                    attr = (StructuredNtfsAttribute<FileNameRecord>)attrs[0];
                }
            }
            else
            {
                foreach (StructuredNtfsAttribute<FileNameRecord> a in attrs)
                {
                    if (_context.UpperCase.Compare(a.Content.FileName, name) == 0)
                    {
                        attr = a;
                    }
                }

                if (attr == null)
                {
                    throw new FileNotFoundException("File name not found on file", name);
                }
            }

            FileNameRecord fnr = attr == null ? new FileNameRecord() : new FileNameRecord(attr.Content);

            if (freshened)
            {
                FreshenFileName(fnr, false);
            }

            return fnr;
        }

        public virtual void Dump(TextWriter writer, string indent)
        {
            writer.WriteLine(indent + "FILE (" + ToString() + ")");
            writer.WriteLine(indent + "  File Number: " + _records[0].MasterFileTableIndex);

            _records[0].Dump(writer, indent + "  ");

            foreach (AttributeRecord attrRec in _records[0].Attributes)
            {
                NtfsAttribute.FromRecord(this, MftReference, attrRec).Dump(writer, indent + "  ");
            }
        }

        public override string ToString()
        {
            string bestName = BestName;
            if (bestName == null)
            {
                return "?????";
            }
            else
            {
                return bestName;
            }
        }

        internal void RemoveAttributeExtents(NtfsAttribute attr)
        {
            attr.GetDataBuffer().SetCapacity(0);

            foreach (var extentRef in attr.Extents.Keys)
            {
                RemoveAttributeExtent(extentRef);
            }
        }

        internal void RemoveAttributeExtent(AttributeReference extentRef)
        {
            FileRecord fileRec = GetFileRecord(extentRef.File);
            if (fileRec != null)
            {
                fileRec.RemoveAttribute(extentRef.AttributeId);

                // Remove empty non-primary MFT records
                if (fileRec.Attributes.Count == 0 && fileRec.BaseFile.Value != 0)
                {
                    RemoveFileRecord(extentRef.File);
                }
            }
        }

        /// <summary>
        /// Gets an attribute by reference.
        /// </summary>
        /// <param name="attrRef">Reference to the attribute.</param>
        /// <returns>The attribute.</returns>
        internal NtfsAttribute GetAttribute(AttributeReference attrRef)
        {
            foreach (var attr in _attributes)
            {
                if (attr.Reference.Equals(attrRef))
                {
                    return attr;
                }
            }

            return null;
        }

        /// <summary>
        ///  Gets the first (if more than one) instance of a named attribute.
        /// </summary>
        /// <param name="type">The attribute type.</param>
        /// <param name="name">The attribute's name.</param>
        /// <returns>The attribute of <c>null</c>.</returns>
        internal NtfsAttribute GetAttribute(AttributeType type, string name)
        {
            foreach (var attr in _attributes)
            {
                if (attr.PrimaryRecord.AttributeType == type && attr.Name == name)
                {
                    return attr;
                }
            }

            return null;
        }

        /// <summary>
        ///  Gets all instances of an unnamed attribute.
        /// </summary>
        /// <param name="type">The attribute type.</param>
        /// <returns>The attributes.</returns>
        internal NtfsAttribute[] GetAttributes(AttributeType type)
        {
            List<NtfsAttribute> matches = new List<NtfsAttribute>();

            foreach (var attr in _attributes)
            {
                if (attr.PrimaryRecord.AttributeType == type && string.IsNullOrEmpty(attr.Name))
                {
                    matches.Add(attr);
                }
            }

            return matches.ToArray();
        }

        internal void MakeAttributeNonResident(AttributeReference attrRef, int maxData)
        {
            NtfsAttribute attr = GetAttribute(attrRef);
            if (attr.IsNonResident)
            {
                throw new InvalidOperationException("Attribute is already non-resident");
            }

            ushort id = _records[0].CreateNonResidentAttribute(attr.Type, attr.Name, attr.Flags);
            AttributeRecord newAttrRecord = _records[0].GetAttribute(id);

            IBuffer attrBuffer = attr.GetDataBuffer();
            byte[] tempData = Utilities.ReadFully(attrBuffer, 0, (int)Math.Min(maxData, attrBuffer.Capacity));

            RemoveAttributeExtents(attr);
            attr.SetExtent(_records[0].Reference, newAttrRecord);

            attr.GetDataBuffer().Write(0, tempData, 0, tempData.Length);

            UpdateAttributeList();
        }

        internal void FreshenFileName(FileNameRecord fileName, bool updateMftRecord)
        {
            //
            // Freshen the record from the definitive info in the other attributes
            //
            StandardInformation si = StandardInformation;
            NtfsAttribute anonDataAttr = GetAttribute(AttributeType.Data, null);

            fileName.CreationTime = si.CreationTime;
            fileName.ModificationTime = si.ModificationTime;
            fileName.MftChangedTime = si.MftChangedTime;
            fileName.LastAccessTime = si.LastAccessTime;
            fileName.Flags = si.FileAttributes;

            if (_dirty && NtfsTransaction.Current != null)
            {
                fileName.MftChangedTime = NtfsTransaction.Current.Timestamp;
            }

            // Directories don't have directory flag set in StandardInformation, so set from MFT record
            if ((_records[0].Flags & FileRecordFlags.IsDirectory) != 0)
            {
                fileName.Flags |= FileAttributeFlags.Directory;
            }

            if (anonDataAttr != null)
            {
                fileName.RealSize = (ulong)anonDataAttr.PrimaryRecord.DataLength;
                fileName.AllocatedSize = (ulong)anonDataAttr.PrimaryRecord.AllocatedLength;
            }

            if (updateMftRecord)
            {
                foreach (NtfsStream stream in GetStreams(AttributeType.FileName, null))
                {
                    FileNameRecord fnr = stream.GetContent<FileNameRecord>();
                    if (fnr.Equals(fileName))
                    {
                        fnr = new FileNameRecord(fileName);
                        fnr.Flags &= ~FileAttributeFlags.ReparsePoint;
                        stream.SetContent(fnr);
                    }
                }
            }
        }

        internal long GetAttributeOffset(AttributeReference attrRef)
        {
            long recordOffset = _mft.GetRecordOffset(attrRef.File);

            FileRecord frs = GetFileRecord(attrRef.File);
            return recordOffset + frs.GetAttributeOffset(attrRef.AttributeId);
        }

        private static Guid CreateNewGuid(INtfsContext context)
        {
            Random rng = context.Options.RandomNumberGenerator;
            if (rng != null)
            {
                byte[] buffer = new byte[16];
                rng.NextBytes(buffer);
                return new Guid(buffer);
            }
            else
            {
                return Guid.NewGuid();
            }
        }

        private void LoadAttributes()
        {
            Dictionary<long, FileRecord> extraFileRecords = new Dictionary<long, FileRecord>();

            AttributeRecord attrListRec = _records[0].GetAttribute(AttributeType.AttributeList);
            if (attrListRec != null)
            {
                NtfsAttribute lastAttr = null;

                StructuredNtfsAttribute<AttributeList> attrListAttr = (StructuredNtfsAttribute<AttributeList>)NtfsAttribute.FromRecord(this, MftReference, attrListRec);
                var attrList = attrListAttr.Content;
                _attributes.Add(attrListAttr);

                foreach (var record in attrList)
                {
                    FileRecord attrFileRecord = _records[0];
                    if (record.BaseFileReference.MftIndex != _records[0].MasterFileTableIndex)
                    {
                        if (!extraFileRecords.TryGetValue(record.BaseFileReference.MftIndex, out attrFileRecord))
                        {
                            attrFileRecord = _context.Mft.GetRecord(record.BaseFileReference);
                            if (attrFileRecord != null)
                            {
                                extraFileRecords[attrFileRecord.MasterFileTableIndex] = attrFileRecord;
                            }
                        }
                    }

                    if (attrFileRecord != null)
                    {
                        AttributeRecord attrRec = attrFileRecord.GetAttribute(record.AttributeId);

                        if (attrRec != null)
                        {
                            if (record.StartVcn == 0)
                            {
                                lastAttr = NtfsAttribute.FromRecord(this, record.BaseFileReference, attrRec);
                                _attributes.Add(lastAttr);
                            }
                            else
                            {
                                lastAttr.AddExtent(record.BaseFileReference, attrRec);
                            }
                        }
                    }
                }

                foreach (var extraFileRecord in extraFileRecords)
                {
                    _records.Add(extraFileRecord.Value);
                }
            }
            else
            {
                foreach (var record in _records[0].Attributes)
                {
                    _attributes.Add(NtfsAttribute.FromRecord(this, MftReference, record));
                }
            }
        }

        private bool SplitAttribute(FileRecord record)
        {
            if (record.Attributes.Count != 1)
            {
                throw new InvalidOperationException("Attempting to split attribute in MFT record containing multiple attributes");
            }

            return SplitAttribute(record, (NonResidentAttributeRecord)record.FirstAttribute, false);
        }
        
        private bool SplitAttribute(FileRecord record, NonResidentAttributeRecord targetAttr, bool atStart)
        {
            if (targetAttr.DataRuns.Count <= 1)
            {
                return false;
            }

            int splitIndex = 1;
            if (!atStart)
            {
                List<DataRun> runs = targetAttr.DataRuns;

                splitIndex = runs.Count - 1;
                int saved = runs[splitIndex].Size;
                while (splitIndex > 1 && record.Size - saved > record.AllocatedSize)
                {
                    --splitIndex;
                    saved += runs[splitIndex].Size;
                }
            }

            AttributeRecord newAttr = targetAttr.Split(splitIndex);

            // Find a home for the new attribute record
            FileRecord newAttrHome = null;
            foreach (var targetRecord in _records)
            {
                if (!targetRecord.IsMftRecord && _mft.RecordSize - targetRecord.Size >= newAttr.Size)
                {
                    targetRecord.AddAttribute(newAttr);
                    newAttrHome = targetRecord;
                }
            }

            if (newAttrHome == null)
            {
                newAttrHome = _mft.AllocateRecord(_records[0].Flags & (~FileRecordFlags.InUse), record.IsMftRecord);
                newAttrHome.BaseFile = record.BaseFile.IsNull ? record.Reference : record.BaseFile;
                _records.Add(newAttrHome);
                newAttrHome.AddAttribute(newAttr);
            }

            // Add the new attribute record as an extent on the attribute it split from
            bool added = false;
            foreach (var attr in _attributes)
            {
                foreach (var existingRecord in attr.Extents)
                {
                    if (existingRecord.Key.File == record.Reference && existingRecord.Key.AttributeId == targetAttr.AttributeId)
                    {
                        attr.AddExtent(newAttrHome.Reference, newAttr);
                        added = true;
                        break;
                    }
                }

                if (added)
                {
                    break;
                }
            }

            UpdateAttributeList();

            return true;
        }

        private bool ExpelAttribute(FileRecord record)
        {
            if (record.MasterFileTableIndex == MasterFileTable.MftIndex)
            {
                // Special case for MFT - can't fully expel attributes, instead split most of the data runs off.
                List<AttributeRecord> attrs = record.Attributes;
                for (int i = attrs.Count - 1; i >= 0; --i)
                {
                    AttributeRecord attr = attrs[i];
                    if (attr.AttributeType == AttributeType.Data)
                    {
                        if (SplitAttribute(record, (NonResidentAttributeRecord)attr, true))
                        {
                            return true;
                        }
                    }
                }
            }
            else
            {
                List<AttributeRecord> attrs = record.Attributes;
                for (int i = attrs.Count - 1; i >= 0; --i)
                {
                    AttributeRecord attr = attrs[i];
                    if (attr.AttributeType > AttributeType.AttributeList)
                    {
                        foreach (var targetRecord in _records)
                        {
                            if (_mft.RecordSize - targetRecord.Size >= attr.Size)
                            {
                                MoveAttribute(record, attr, targetRecord);
                                return true;
                            }
                        }

                        FileRecord newFileRecord = _mft.AllocateRecord(FileRecordFlags.None, record.IsMftRecord);
                        newFileRecord.BaseFile = record.Reference;
                        _records.Add(newFileRecord);
                        MoveAttribute(record, attr, newFileRecord);
                        return true;
                    }
                }
            }

            return false;
        }

        private void MoveAttribute(FileRecord record, AttributeRecord attrRec, FileRecord targetRecord)
        {
            AttributeReference oldRef = new AttributeReference(record.Reference, attrRec.AttributeId);

            record.RemoveAttribute(attrRec.AttributeId);
            targetRecord.AddAttribute(attrRec);

            AttributeReference newRef = new AttributeReference(targetRecord.Reference, attrRec.AttributeId);

            foreach (var attr in _attributes)
            {
                attr.ReplaceExtent(oldRef, newRef, attrRec);
            }

            UpdateAttributeList();
        }

        private void CreateAttributeList()
        {
            ushort id = _records[0].CreateAttribute(AttributeType.AttributeList, null, false, AttributeFlags.None);

            StructuredNtfsAttribute<AttributeList> newAttr = (StructuredNtfsAttribute<AttributeList>)NtfsAttribute.FromRecord(this, MftReference, _records[0].GetAttribute(id));
            _attributes.Add(newAttr);
            UpdateAttributeList();
        }

        private void UpdateAttributeList()
        {
            if (_records.Count > 1)
            {
                AttributeList attrList = new AttributeList();

                foreach (var attr in _attributes)
                {
                    if (attr.Type != AttributeType.AttributeList)
                    {
                        foreach (var extent in attr.Extents)
                        {
                            attrList.Add(AttributeListRecord.FromAttribute(extent.Value, extent.Key.File));
                        }
                    }
                }

                StructuredNtfsAttribute<AttributeList> alAttr;
                alAttr = (StructuredNtfsAttribute<AttributeList>)GetAttribute(AttributeType.AttributeList, null);
                alAttr.Content = attrList;
                alAttr.Save();
            }
        }

        /// <summary>
        /// Creates a new unnamed attribute.
        /// </summary>
        /// <param name="type">The type of the new attribute.</param>
        /// <param name="flags">The flags of the new attribute.</param>
        /// <returns>The new attribute.</returns>
        private NtfsAttribute CreateAttribute(AttributeType type, AttributeFlags flags)
        {
            return CreateAttribute(type, null, flags);
        }

        /// <summary>
        /// Creates a new attribute.
        /// </summary>
        /// <param name="type">The type of the new attribute.</param>
        /// <param name="name">The name of the new attribute.</param>
        /// <param name="flags">The flags of the new attribute.</param>
        /// <returns>The new attribute.</returns>
        private NtfsAttribute CreateAttribute(AttributeType type, string name, AttributeFlags flags)
        {
            bool indexed = _context.AttributeDefinitions.IsIndexed(type);
            ushort id = _records[0].CreateAttribute(type, name, indexed, flags);

            AttributeRecord newAttrRecord = _records[0].GetAttribute(id);
            NtfsAttribute newAttr = NtfsAttribute.FromRecord(this, MftReference, newAttrRecord);

            _attributes.Add(newAttr);
            UpdateAttributeList();

            MarkMftRecordDirty();

            return newAttr;
        }

        /// <summary>
        /// Creates a new attribute at a fixed cluster.
        /// </summary>
        /// <param name="type">The type of the new attribute.</param>
        /// <param name="name">The name of the new attribute.</param>
        /// <param name="flags">The flags of the new attribute.</param>
        /// <param name="firstCluster">The first cluster to assign to the attribute.</param>
        /// <param name="numClusters">The number of sequential clusters to assign to the attribute.</param>
        /// <param name="bytesPerCluster">The number of bytes in each cluster.</param>
        /// <returns>The new attribute.</returns>
        private NtfsAttribute CreateAttribute(AttributeType type, string name, AttributeFlags flags, long firstCluster, ulong numClusters, uint bytesPerCluster)
        {
            bool indexed = _context.AttributeDefinitions.IsIndexed(type);
            ushort id = _records[0].CreateNonResidentAttribute(type, name, flags, firstCluster, numClusters, bytesPerCluster);

            AttributeRecord newAttrRecord = _records[0].GetAttribute(id);
            NtfsAttribute newAttr = NtfsAttribute.FromRecord(this, MftReference, newAttrRecord);

            _attributes.Add(newAttr);
            UpdateAttributeList();
            MarkMftRecordDirty();
            return newAttr;
        }

        private void RemoveAttribute(NtfsAttribute attr)
        {
            if (attr != null)
            {
                if (attr.PrimaryRecord.AttributeType == AttributeType.IndexRoot)
                {
                    _indexCache.Remove(attr.PrimaryRecord.Name);
                }

                RemoveAttributeExtents(attr);

                _attributes.Remove(attr);

                UpdateAttributeList();
            }
        }

        private bool ShrinkIndexRoot(string indexName)
        {
            NtfsAttribute attr = GetAttribute(AttributeType.IndexRoot, indexName);

            // Nothing to win, can't make IndexRoot smaller than this
            // 8 = min size of entry that points to IndexAllocation...
            if (attr.Length <= IndexRoot.HeaderOffset + IndexHeader.Size + 8)
            {
                return false;
            }

            Index idx = GetIndex(indexName);
            return idx.ShrinkRoot();
        }

        private void MakeAttributeResident(AttributeReference attrRef, int maxData)
        {
            NtfsAttribute attr = GetAttribute(attrRef);
            if (!attr.IsNonResident)
            {
                throw new InvalidOperationException("Attribute is already resident");
            }

            ushort id = _records[0].CreateAttribute(attr.Type, attr.Name, _context.AttributeDefinitions.IsIndexed(attr.Type), attr.Flags);
            AttributeRecord newAttrRecord = _records[0].GetAttribute(id);

            IBuffer attrBuffer = attr.GetDataBuffer();
            byte[] tempData = Utilities.ReadFully(attrBuffer, 0, (int)Math.Min(maxData, attrBuffer.Capacity));

            RemoveAttributeExtents(attr);
            attr.SetExtent(_records[0].Reference, newAttrRecord);

            attr.GetDataBuffer().Write(0, tempData, 0, tempData.Length);

            UpdateAttributeList();
        }

        private FileRecord GetFileRecord(FileRecordReference fileReference)
        {
            foreach (var record in _records)
            {
                if (record.MasterFileTableIndex == fileReference.MftIndex)
                {
                    return record;
                }
            }

            return null;
        }

        private void RemoveFileRecord(FileRecordReference fileReference)
        {
            for (int i = 0; i < _records.Count; ++i)
            {
                if (_records[i].MasterFileTableIndex == fileReference.MftIndex)
                {
                    FileRecord record = _records[i];

                    if (record.Attributes.Count > 0)
                    {
                        throw new IOException("Attempting to remove non-empty MFT record");
                    }

                    _context.Mft.RemoveRecord(fileReference);
                    _records.Remove(record);

                    if (_records.Count == 1)
                    {
                        NtfsAttribute attrListAttr = GetAttribute(AttributeType.AttributeList, null);
                        if (attrListAttr != null)
                        {
                            RemoveAttribute(attrListAttr);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Wrapper for Resident/Non-Resident attribute streams, that remains valid
        /// despite the attribute oscillating between resident and not.
        /// </summary>
        private class FileStream : SparseStream
        {
            private File _file;
            private NtfsAttribute _attr;
            private SparseStream _wrapped;

            public FileStream(File file, NtfsAttribute attr, FileAccess access)
            {
                _file = file;
                _attr = attr;
                _wrapped = attr.Open(access);
            }

            public override IEnumerable<StreamExtent> Extents
            {
                get
                {
                    return _wrapped.Extents;
                }
            }

            public override bool CanRead
            {
                get
                {
                    return _wrapped.CanRead;
                }
            }

            public override bool CanSeek
            {
                get
                {
                    return _wrapped.CanSeek;
                }
            }

            public override bool CanWrite
            {
                get
                {
                    return _wrapped.CanWrite;
                }
            }

            public override long Length
            {
                get
                {
                    return _wrapped.Length;
                }
            }

            public override long Position
            {
                get
                {
                    return _wrapped.Position;
                }

                set
                {
                    _wrapped.Position = value;
                }
            }

            public override void Close()
            {
                base.Close();
                _wrapped.Close();
            }

            public override void Flush()
            {
                _wrapped.Flush();
            }

            public override int Read(byte[] buffer, int offset, int count)
            {
                return _wrapped.Read(buffer, offset, count);
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                return _wrapped.Seek(offset, origin);
            }

            public override void SetLength(long value)
            {
                ChangeAttributeResidencyByLength(value);
                _wrapped.SetLength(value);
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                if (_wrapped.Position + count > Length)
                {
                    ChangeAttributeResidencyByLength(_wrapped.Position + count);
                }

                _wrapped.Write(buffer, offset, count);
            }

            public override void Clear(int count)
            {
                if (_wrapped.Position + count > Length)
                {
                    ChangeAttributeResidencyByLength(_wrapped.Position + count);
                }

                _wrapped.Clear(count);
            }

            public override string ToString()
            {
                return _file.ToString() + ".attr[" + _attr.Id + "]";
            }

            /// <summary>
            /// Change attribute residency if it gets too big (or small).
            /// </summary>
            /// <param name="value">The new (anticipated) length of the stream.</param>
            /// <remarks>Has hysteresis - the decision is based on the input and the current
            /// state, not the current state alone.</remarks>
            private void ChangeAttributeResidencyByLength(long value)
            {
                // This is a bit of a hack - but it's really important the bitmap file remains non-resident
                if (_file._records[0].MasterFileTableIndex == MasterFileTable.BitmapIndex)
                {
                    return;
                }

                if (!_attr.IsNonResident && value >= _file.MaxMftRecordSize)
                {
                    _file.MakeAttributeNonResident(_attr.Reference, (int)Math.Min(value, _wrapped.Length));
                }
                else if (_attr.IsNonResident && value <= _file.MaxMftRecordSize / 4)
                {
                    // Use of 1/4 of record size here is just a heuristic - the important thing is not to end up with
                    // zero-length non-resident attributes
                    _file.MakeAttributeResident(_attr.Reference, (int)value);
                }
            }
        }
    }
}
