//
// 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.Globalization;
    using System.IO;
    using System.Security.AccessControl;

    internal sealed class SecurityDescriptors : IDiagnosticTraceable
    {
        // File consists of pairs of duplicate blocks (one after the other), providing
        // redundancy.  When a pair is full, the next pair is used.
        private const int BlockSize = 0x40000;

        private File _file;
        private IndexView<HashIndexKey, HashIndexData> _hashIndex;
        private IndexView<IdIndexKey, IdIndexData> _idIndex;
        private uint _nextId;
        private long _nextSpace;

        public SecurityDescriptors(File file)
        {
            _file = file;
            _hashIndex = new IndexView<HashIndexKey, HashIndexData>(file.GetIndex("$SDH"));
            _idIndex = new IndexView<IdIndexKey, IdIndexData>(file.GetIndex("$SII"));

            foreach (var entry in _idIndex.Entries)
            {
                if (entry.Key.Id > _nextId)
                {
                    _nextId = entry.Key.Id;
                }

                long end = entry.Value.SdsOffset + entry.Value.SdsLength;
                if (end > _nextSpace)
                {
                    _nextSpace = end;
                }
            }

            if (_nextId == 0)
            {
                _nextId = 256;
            }
            else
            {
                _nextId++;
            }

            _nextSpace = Utilities.RoundUp(_nextSpace, 16);
        }

        public static SecurityDescriptors Initialize(File file)
        {
            file.CreateIndex("$SDH", (AttributeType)0, AttributeCollationRule.SecurityHash);
            file.CreateIndex("$SII", (AttributeType)0, AttributeCollationRule.UnsignedLong);
            file.CreateStream(AttributeType.Data, "$SDS");

            return new SecurityDescriptors(file);
        }

        public RawSecurityDescriptor GetDescriptorById(uint id)
        {
            IdIndexData data;
            if (_idIndex.TryGetValue(new IdIndexKey(id), out data))
            {
                return ReadDescriptor(data).Descriptor;
            }

            return null;
        }

        public uint AddDescriptor(RawSecurityDescriptor newDescriptor)
        {
            // Search to see if this is a known descriptor
            SecurityDescriptor newDescObj = new SecurityDescriptor(newDescriptor);
            uint newHash = newDescObj.CalcHash();
            byte[] newByteForm = new byte[newDescObj.Size];
            newDescObj.WriteTo(newByteForm, 0);

            foreach (var entry in _hashIndex.FindAll(new HashFinder(newHash)))
            {
                SecurityDescriptor stored = ReadDescriptor(entry.Value);

                byte[] storedByteForm = new byte[stored.Size];
                stored.WriteTo(storedByteForm, 0);

                if (Utilities.AreEqual(newByteForm, storedByteForm))
                {
                    return entry.Value.Id;
                }
            }

            long offset = _nextSpace;

            // Write the new descriptor to the end of the existing descriptors
            SecurityDescriptorRecord record = new SecurityDescriptorRecord();
            record.SecurityDescriptor = newByteForm;
            record.Hash = newHash;
            record.Id = _nextId;

            // If we'd overflow into our duplicate block, skip over it to the
            // start of the next block
            if (((offset + record.Size) / BlockSize) % 2 == 1)
            {
                _nextSpace = Utilities.RoundUp(offset, BlockSize * 2);
                offset = _nextSpace;
            }

            record.OffsetInFile = offset;

            byte[] buffer = new byte[record.Size];
            record.WriteTo(buffer, 0);

            using (Stream s = _file.OpenStream(AttributeType.Data, "$SDS", FileAccess.ReadWrite))
            {
                s.Position = _nextSpace;
                s.Write(buffer, 0, buffer.Length);
                s.Position = BlockSize + _nextSpace;
                s.Write(buffer, 0, buffer.Length);
            }

            // Make the next descriptor land at the end of this one
            _nextSpace = Utilities.RoundUp(_nextSpace + buffer.Length, 16);
            _nextId++;

            // Update the indexes
            HashIndexData hashIndexData = new HashIndexData();
            hashIndexData.Hash = record.Hash;
            hashIndexData.Id = record.Id;
            hashIndexData.SdsOffset = record.OffsetInFile;
            hashIndexData.SdsLength = (int)record.EntrySize;

            HashIndexKey hashIndexKey = new HashIndexKey();
            hashIndexKey.Hash = record.Hash;
            hashIndexKey.Id = record.Id;

            _hashIndex[hashIndexKey] = hashIndexData;

            IdIndexData idIndexData = new IdIndexData();
            idIndexData.Hash = record.Hash;
            idIndexData.Id = record.Id;
            idIndexData.SdsOffset = record.OffsetInFile;
            idIndexData.SdsLength = (int)record.EntrySize;

            IdIndexKey idIndexKey = new IdIndexKey();
            idIndexKey.Id = record.Id;

            _idIndex[idIndexKey] = idIndexData;

            _file.UpdateRecordInMft();

            return record.Id;
        }

        public void Dump(TextWriter writer, string indent)
        {
            writer.WriteLine(indent + "SECURITY DESCRIPTORS");

            using (Stream s = _file.OpenStream(AttributeType.Data, "$SDS", FileAccess.Read))
            {
                byte[] buffer = Utilities.ReadFully(s, (int)s.Length);

                foreach (var entry in _idIndex.Entries)
                {
                    int pos = (int)entry.Value.SdsOffset;

                    SecurityDescriptorRecord rec = new SecurityDescriptorRecord();
                    if (!rec.Read(buffer, pos))
                    {
                        break;
                    }

                    string secDescStr = "--unknown--";
                    if (rec.SecurityDescriptor[0] != 0)
                    {
                        RawSecurityDescriptor sd = new RawSecurityDescriptor(rec.SecurityDescriptor, 0);
                        secDescStr = sd.GetSddlForm(AccessControlSections.All);
                    }

                    writer.WriteLine(indent + "  SECURITY DESCRIPTOR RECORD");
                    writer.WriteLine(indent + "           Hash: " + rec.Hash);
                    writer.WriteLine(indent + "             Id: " + rec.Id);
                    writer.WriteLine(indent + "    File Offset: " + rec.OffsetInFile);
                    writer.WriteLine(indent + "           Size: " + rec.EntrySize);
                    writer.WriteLine(indent + "          Value: " + secDescStr);
                }
            }
        }

        private SecurityDescriptor ReadDescriptor(IndexData data)
        {
            using (Stream s = _file.OpenStream(AttributeType.Data, "$SDS", FileAccess.Read))
            {
                s.Position = data.SdsOffset;
                byte[] buffer = Utilities.ReadFully(s, data.SdsLength);

                SecurityDescriptorRecord record = new SecurityDescriptorRecord();
                record.Read(buffer, 0);

                return new SecurityDescriptor(new RawSecurityDescriptor(record.SecurityDescriptor, 0));
            }
        }

        internal abstract class IndexData
        {
            public uint Hash;
            public uint Id;
            public long SdsOffset;
            public int SdsLength;

            public override string ToString()
            {
                return string.Format(CultureInfo.InvariantCulture, "[Data-Hash:{0},Id:{1},SdsOffset:{2},SdsLength:{3}]", Hash, Id, SdsOffset, SdsLength);
            }
        }

        internal sealed class HashIndexKey : IByteArraySerializable
        {
            public uint Hash;
            public uint Id;

            public int Size
            {
                get { return 8; }
            }

            public int ReadFrom(byte[] buffer, int offset)
            {
                Hash = Utilities.ToUInt32LittleEndian(buffer, offset + 0);
                Id = Utilities.ToUInt32LittleEndian(buffer, offset + 4);
                return 8;
            }

            public void WriteTo(byte[] buffer, int offset)
            {
                Utilities.WriteBytesLittleEndian(Hash, buffer, offset + 0);
                Utilities.WriteBytesLittleEndian(Id, buffer, offset + 4);
            }

            public override string ToString()
            {
                return string.Format(CultureInfo.InvariantCulture, "[Key-Hash:{0},Id:{1}]", Hash, Id);
            }
        }

        internal sealed class HashIndexData : IndexData, IByteArraySerializable
        {
            public int Size
            {
                get { return 0x14; }
            }

            public int ReadFrom(byte[] buffer, int offset)
            {
                Hash = Utilities.ToUInt32LittleEndian(buffer, offset + 0x00);
                Id = Utilities.ToUInt32LittleEndian(buffer, offset + 0x04);
                SdsOffset = Utilities.ToInt64LittleEndian(buffer, offset + 0x08);
                SdsLength = Utilities.ToInt32LittleEndian(buffer, offset + 0x10);
                return 0x14;
            }

            public void WriteTo(byte[] buffer, int offset)
            {
                Utilities.WriteBytesLittleEndian(Hash, buffer, offset + 0x00);
                Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0x04);
                Utilities.WriteBytesLittleEndian(SdsOffset, buffer, offset + 0x08);
                Utilities.WriteBytesLittleEndian(SdsLength, buffer, offset + 0x10);
                ////Array.Copy(new byte[] { (byte)'I', 0, (byte)'I', 0 }, 0, buffer, offset + 0x14, 4);
            }
        }

        internal sealed class IdIndexKey : IByteArraySerializable
        {
            public uint Id;

            public IdIndexKey()
            {
            }

            public IdIndexKey(uint id)
            {
                Id = id;
            }

            public int Size
            {
                get { return 4; }
            }

            public int ReadFrom(byte[] buffer, int offset)
            {
                Id = Utilities.ToUInt32LittleEndian(buffer, offset + 0);
                return 4;
            }

            public void WriteTo(byte[] buffer, int offset)
            {
                Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0);
            }

            public override string ToString()
            {
                return string.Format(CultureInfo.InvariantCulture, "[Key-Id:{0}]", Id);
            }
        }

        internal sealed class IdIndexData : IndexData, IByteArraySerializable
        {
            public int Size
            {
                get { return 0x14; }
            }

            public int ReadFrom(byte[] buffer, int offset)
            {
                Hash = Utilities.ToUInt32LittleEndian(buffer, offset + 0x00);
                Id = Utilities.ToUInt32LittleEndian(buffer, offset + 0x04);
                SdsOffset = Utilities.ToInt64LittleEndian(buffer, offset + 0x08);
                SdsLength = Utilities.ToInt32LittleEndian(buffer, offset + 0x10);
                return 0x14;
            }

            public void WriteTo(byte[] buffer, int offset)
            {
                Utilities.WriteBytesLittleEndian(Hash, buffer, offset + 0x00);
                Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0x04);
                Utilities.WriteBytesLittleEndian(SdsOffset, buffer, offset + 0x08);
                Utilities.WriteBytesLittleEndian(SdsLength, buffer, offset + 0x10);
            }
        }

        private class HashFinder : IComparable<HashIndexKey>
        {
            private uint _toMatch;

            public HashFinder(uint toMatch)
            {
                _toMatch = toMatch;
            }

            public int CompareTo(uint otherHash)
            {
                if (_toMatch < otherHash)
                {
                    return -1;
                }
                else if (_toMatch > otherHash)
                {
                    return 1;
                }

                return 0;
            }

            public int CompareTo(HashIndexKey other)
            {
                return CompareTo(other.Hash);
            }
        }
    }
}
