//
// 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 NonResidentAttributeBuffer : NonResidentDataBuffer
    {
        private File _file;
        private NtfsAttribute _attribute;

        public NonResidentAttributeBuffer(File file, NtfsAttribute attribute)
            : base(file.Context, CookRuns(attribute), file.IndexInMft == MasterFileTable.MftIndex)
        {
            _file = file;
            _attribute = attribute;

            switch (attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse))
            {
                case AttributeFlags.Sparse:
                    _activeStream = new SparseClusterStream(_attribute, _rawStream);
                    break;

                case AttributeFlags.Compressed:
                    _activeStream = new CompressedClusterStream(_context, _attribute, _rawStream);
                    break;

                case AttributeFlags.None:
                    _activeStream = _rawStream;
                    break;

                default:
                    throw new NotImplementedException("Unhandled attribute type '" + attribute.Flags + "'");
            }
        }

        public override bool CanWrite
        {
            get { return _context.RawStream.CanWrite && _file != null; }
        }

        public override long Capacity
        {
            get { return PrimaryAttributeRecord.DataLength; }
        }

        private NonResidentAttributeRecord PrimaryAttributeRecord
        {
            get { return _attribute.PrimaryRecord as NonResidentAttributeRecord; }
        }

        public void AlignVirtualClusterCount()
        {
            _file.MarkMftRecordDirty();
            _activeStream.ExpandToClusters(Utilities.Ceil(_attribute.Length, _bytesPerCluster), (NonResidentAttributeRecord)_attribute.LastExtent, false);
        }

        public override void SetCapacity(long value)
        {
            if (!CanWrite)
            {
                throw new IOException("Attempt to change length of file not opened for write");
            }

            if (value == Capacity)
            {
                return;
            }

            _file.MarkMftRecordDirty();

            long newClusterCount = Utilities.Ceil(value, _bytesPerCluster);

            if (value < Capacity)
            {
                Truncate(value);
            }
            else
            {
                _activeStream.ExpandToClusters(newClusterCount, (NonResidentAttributeRecord)_attribute.LastExtent, true);

                PrimaryAttributeRecord.AllocatedLength = _cookedRuns.NextVirtualCluster * _bytesPerCluster;
            }

            PrimaryAttributeRecord.DataLength = value;

            if (PrimaryAttributeRecord.InitializedDataLength > value)
            {
                PrimaryAttributeRecord.InitializedDataLength = value;
            }

            _cookedRuns.CollapseRuns();
        }

        public override void Write(long pos, byte[] buffer, int offset, int count)
        {
            if (!CanWrite)
            {
                throw new IOException("Attempt to write to file not opened for write");
            }

            if (count == 0)
            {
                return;
            }

            if (pos + count > Capacity)
            {
                SetCapacity(pos + count);
            }

            // Write zeros from end of current initialized data to the start of the new write
            if (pos > PrimaryAttributeRecord.InitializedDataLength)
            {
                InitializeData(pos);
            }

            int allocatedClusters = 0;

            long focusPos = pos;
            while (focusPos < pos + count)
            {
                long vcn = focusPos / _bytesPerCluster;
                long remaining = (pos + count) - focusPos;
                long clusterOffset = focusPos - (vcn * _bytesPerCluster);

                if (vcn * _bytesPerCluster != focusPos || remaining < _bytesPerCluster)
                {
                    // Unaligned or short write
                    int toWrite = (int)Math.Min(remaining, _bytesPerCluster - clusterOffset);

                    _activeStream.ReadClusters(vcn, 1, _ioBuffer, 0);
                    Array.Copy(buffer, offset + (focusPos - pos), _ioBuffer, clusterOffset, toWrite);
                    allocatedClusters += _activeStream.WriteClusters(vcn, 1, _ioBuffer, 0);

                    focusPos += toWrite;
                }
                else
                {
                    // Aligned, full cluster writes...
                    int fullClusters = (int)(remaining / _bytesPerCluster);
                    allocatedClusters += _activeStream.WriteClusters(vcn, fullClusters, buffer, (int)(offset + (focusPos - pos)));

                    focusPos += fullClusters * _bytesPerCluster;
                }
            }

            if (pos + count > PrimaryAttributeRecord.InitializedDataLength)
            {
                _file.MarkMftRecordDirty();

                PrimaryAttributeRecord.InitializedDataLength = pos + count;
            }

            if (pos + count > PrimaryAttributeRecord.DataLength)
            {
                _file.MarkMftRecordDirty();

                PrimaryAttributeRecord.DataLength = pos + count;
            }

            if ((_attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0)
            {
                PrimaryAttributeRecord.CompressedDataSize += allocatedClusters * _bytesPerCluster;
            }

            _cookedRuns.CollapseRuns();
        }

        public override void Clear(long pos, int count)
        {
            if (!CanWrite)
            {
                throw new IOException("Attempt to erase bytes from file not opened for write");
            }

            if (count == 0)
            {
                return;
            }

            if (pos + count > Capacity)
            {
                SetCapacity(pos + count);
            }

            _file.MarkMftRecordDirty();

            // Write zeros from end of current initialized data to the start of the new write
            if (pos > PrimaryAttributeRecord.InitializedDataLength)
            {
                InitializeData(pos);
            }

            int releasedClusters = 0;

            long focusPos = pos;
            while (focusPos < pos + count)
            {
                long vcn = focusPos / _bytesPerCluster;
                long remaining = (pos + count) - focusPos;
                long clusterOffset = focusPos - (vcn * _bytesPerCluster);

                if (vcn * _bytesPerCluster != focusPos || remaining < _bytesPerCluster)
                {
                    // Unaligned or short write
                    int toClear = (int)Math.Min(remaining, _bytesPerCluster - clusterOffset);

                    if (_activeStream.IsClusterStored(vcn))
                    {
                        _activeStream.ReadClusters(vcn, 1, _ioBuffer, 0);
                        Array.Clear(_ioBuffer, (int)clusterOffset, toClear);
                        releasedClusters -= _activeStream.WriteClusters(vcn, 1, _ioBuffer, 0);
                    }

                    focusPos += toClear;
                }
                else
                {
                    // Aligned, full cluster clears...
                    int fullClusters = (int)(remaining / _bytesPerCluster);
                    releasedClusters += _activeStream.ClearClusters(vcn, fullClusters);

                    focusPos += fullClusters * _bytesPerCluster;
                }
            }

            if (pos + count > PrimaryAttributeRecord.InitializedDataLength)
            {
                PrimaryAttributeRecord.InitializedDataLength = pos + count;
            }

            if (pos + count > PrimaryAttributeRecord.DataLength)
            {
                PrimaryAttributeRecord.DataLength = pos + count;
            }

            if ((_attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0)
            {
                PrimaryAttributeRecord.CompressedDataSize -= releasedClusters * _bytesPerCluster;
            }

            _cookedRuns.CollapseRuns();
        }

        private static CookedDataRuns CookRuns(NtfsAttribute attribute)
        {
            CookedDataRuns result = new CookedDataRuns();

            foreach (NonResidentAttributeRecord record in attribute.Records)
            {
                if (record.StartVcn != result.NextVirtualCluster)
                {
                    throw new IOException("Invalid NTFS attribute - non-contiguous data runs");
                }

                result.Append(record.DataRuns, record);
            }

            return result;
        }

        private void InitializeData(long pos)
        {
            long initDataLen = PrimaryAttributeRecord.InitializedDataLength;
            _file.MarkMftRecordDirty();

            int clustersAllocated = 0;

            while (initDataLen < pos)
            {
                long vcn = initDataLen / _bytesPerCluster;
                if (initDataLen % _bytesPerCluster != 0 || pos - initDataLen < _bytesPerCluster)
                {
                    int clusterOffset = (int)(initDataLen - (vcn * _bytesPerCluster));
                    int toClear = (int)Math.Min(_bytesPerCluster - clusterOffset, pos - initDataLen);

                    if (_activeStream.IsClusterStored(vcn))
                    {
                        _activeStream.ReadClusters(vcn, 1, _ioBuffer, 0);
                        Array.Clear(_ioBuffer, clusterOffset, toClear);
                        clustersAllocated += _activeStream.WriteClusters(vcn, 1, _ioBuffer, 0);
                    }

                    initDataLen += toClear;
                }
                else
                {
                    int numClusters = (int)((pos / _bytesPerCluster) - vcn);
                    clustersAllocated -= _activeStream.ClearClusters(vcn, numClusters);

                    initDataLen += numClusters * _bytesPerCluster;
                }
            }

            PrimaryAttributeRecord.InitializedDataLength = pos;

            if ((_attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0)
            {
                PrimaryAttributeRecord.CompressedDataSize += clustersAllocated * _bytesPerCluster;
            }
        }

        private void Truncate(long value)
        {
            long endVcn = Utilities.Ceil(value, _bytesPerCluster);

            // Release the clusters
            _activeStream.TruncateToClusters(endVcn);

            // First, remove any extents that are now redundant.
            Dictionary<AttributeReference, AttributeRecord> extentCache = new Dictionary<AttributeReference, AttributeRecord>(_attribute.Extents);
            foreach (var extent in extentCache)
            {
                if (extent.Value.StartVcn >= endVcn)
                {
                    NonResidentAttributeRecord record = (NonResidentAttributeRecord)extent.Value;
                    _file.RemoveAttributeExtent(extent.Key);
                    _attribute.RemoveExtentCacheSafe(extent.Key);
                }
            }

            PrimaryAttributeRecord.LastVcn = Math.Max(0, endVcn - 1);
            PrimaryAttributeRecord.AllocatedLength = endVcn * _bytesPerCluster;
            PrimaryAttributeRecord.DataLength = value;
            PrimaryAttributeRecord.InitializedDataLength = Math.Min(PrimaryAttributeRecord.InitializedDataLength, value);

            _file.MarkMftRecordDirty();
        }
    }
}
