//
// 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.Globalization;
    using System.IO;
    using System.Text;
    using DirectoryIndexEntry = System.Collections.Generic.KeyValuePair<DiscUtils.Ntfs.FileNameRecord, DiscUtils.Ntfs.FileRecordReference>;

    internal class Directory : File
    {
        private IndexView<FileNameRecord, FileRecordReference> _index;

        public Directory(INtfsContext context, FileRecord baseRecord)
            : base(context, baseRecord)
        {
        }

        public bool IsEmpty
        {
            get { return Index.Count == 0; }
        }

        private IndexView<FileNameRecord, FileRecordReference> Index
        {
            get
            {
                if (_index == null && StreamExists(AttributeType.IndexRoot, "$I30"))
                {
                    _index = new IndexView<FileNameRecord, FileRecordReference>(GetIndex("$I30"));
                }

                return _index;
            }
        }

        public IEnumerable<DirectoryEntry> GetAllEntries(bool filter)
        {
            IEnumerable<DirectoryIndexEntry> entries = filter ? FilterEntries(Index.Entries) : Index.Entries;

            foreach (var entry in entries)
            {
                yield return new DirectoryEntry(this, entry.Value, entry.Key);
            }
        }

        public void UpdateEntry(DirectoryEntry entry)
        {
            Index[entry.Details] = entry.Reference;
            UpdateRecordInMft();
        }

        public override void Dump(TextWriter writer, string indent)
        {
            writer.WriteLine(indent + "DIRECTORY (" + base.ToString() + ")");
            writer.WriteLine(indent + "  File Number: " + IndexInMft);

            if (Index != null)
            {
                foreach (var entry in Index.Entries)
                {
                    writer.WriteLine(indent + "  DIRECTORY ENTRY (" + entry.Key.FileName + ")");
                    writer.WriteLine(indent + "    MFT Ref: " + entry.Value);
                    entry.Key.Dump(writer, indent + "    ");
                }
            }
        }

        public override string ToString()
        {
            return base.ToString() + @"\";
        }

        internal static new Directory CreateNew(INtfsContext context, FileAttributeFlags parentDirFlags)
        {
            Directory dir = (Directory)context.AllocateFile(FileRecordFlags.IsDirectory);

            StandardInformation.InitializeNewFile(
                dir,
                FileAttributeFlags.Archive | (parentDirFlags & FileAttributeFlags.Compressed));

            // Create the index root attribute by instantiating a new index
            dir.CreateIndex("$I30", AttributeType.FileName, AttributeCollationRule.Filename);

            dir.UpdateRecordInMft();

            return dir;
        }

        internal DirectoryEntry GetEntryByName(string name)
        {
            string searchName = name;

            int streamSepPos = name.IndexOf(':');
            if (streamSepPos >= 0)
            {
                searchName = name.Substring(0, streamSepPos);
            }

            DirectoryIndexEntry entry = Index.FindFirst(new FileNameQuery(searchName, _context.UpperCase));
            if (entry.Key != null)
            {
                return new DirectoryEntry(this, entry.Value, entry.Key);
            }
            else
            {
                return null;
            }
        }

        internal DirectoryEntry AddEntry(File file, string name, FileNameNamespace nameNamespace)
        {
            if (name.Length > 255)
            {
                throw new IOException("Invalid file name, more than 255 characters: " + name);
            }
            else if (name.IndexOfAny(new char[] { '\0', '/' }) != -1)
            {
                throw new IOException(@"Invalid file name, contains '\0' or '/': " + name);
            }

            FileNameRecord newNameRecord = file.GetFileNameRecord(null, true);
            newNameRecord.FileNameNamespace = nameNamespace;
            newNameRecord.FileName = name;
            newNameRecord.ParentDirectory = MftReference;

            NtfsStream nameStream = file.CreateStream(AttributeType.FileName, null);
            nameStream.SetContent(newNameRecord);

            file.HardLinkCount++;
            file.UpdateRecordInMft();

            Index[newNameRecord] = file.MftReference;

            Modified();
            UpdateRecordInMft();

            return new DirectoryEntry(this, file.MftReference, newNameRecord);
        }

        internal void RemoveEntry(DirectoryEntry dirEntry)
        {
            File file = _context.GetFileByRef(dirEntry.Reference);

            FileNameRecord nameRecord = dirEntry.Details;

            Index.Remove(dirEntry.Details);

            foreach (NtfsStream stream in file.GetStreams(AttributeType.FileName, null))
            {
                FileNameRecord streamName = stream.GetContent<FileNameRecord>();
                if (nameRecord.Equals(streamName))
                {
                    file.RemoveStream(stream);
                    break;
                }
            }

            file.HardLinkCount--;
            file.UpdateRecordInMft();

            Modified();
            UpdateRecordInMft();
        }

        internal string CreateShortName(string name)
        {
            string baseName = string.Empty;
            string ext = string.Empty;

            int lastPeriod = name.LastIndexOf('.');

            int i = 0;
            while (baseName.Length < 6 && i < name.Length && i != lastPeriod)
            {
                char upperChar = Char.ToUpperInvariant(name[i]);
                if (Utilities.Is8Dot3Char(upperChar))
                {
                    baseName += upperChar;
                }

                ++i;
            }

            if (lastPeriod >= 0)
            {
                i = lastPeriod + 1;
                while (ext.Length < 3 && i < name.Length)
                {
                    char upperChar = Char.ToUpperInvariant(name[i]);
                    if (Utilities.Is8Dot3Char(upperChar))
                    {
                        ext += upperChar;
                    }

                    ++i;
                }
            }

            i = 1;
            string candidate;
            do
            {
                string suffix = string.Format(CultureInfo.InvariantCulture, "~{0}", i);
                candidate = baseName.Substring(0, Math.Min(8 - suffix.Length, baseName.Length)) + suffix + (ext.Length > 0 ? "." + ext : string.Empty);
                i++;
            }
            while (GetEntryByName(candidate) != null);

            return candidate;
        }

        private List<DirectoryIndexEntry> FilterEntries(IEnumerable<DirectoryIndexEntry> entriesIter)
        {
            List<DirectoryIndexEntry> entries = new List<DirectoryIndexEntry>(entriesIter);

            // Weed out short-name entries for files and any hidden / system / metadata files.
            int i = 0;
            while (i < entries.Count)
            {
                DirectoryIndexEntry entry = entries[i];

                if (((entry.Key.Flags & FileAttributeFlags.Hidden) != 0) && _context.Options.HideHiddenFiles)
                {
                    entries.RemoveAt(i);
                }
                else if (((entry.Key.Flags & FileAttributeFlags.System) != 0) && _context.Options.HideSystemFiles)
                {
                    entries.RemoveAt(i);
                }
                else if (entry.Value.MftIndex < 24 && _context.Options.HideMetafiles)
                {
                    entries.RemoveAt(i);
                }
                else if (entry.Key.FileNameNamespace == FileNameNamespace.Dos && _context.Options.HideDosFileNames)
                {
                    entries.RemoveAt(i);
                }
                else
                {
                    ++i;
                }
            }

            return entries;
        }

        private sealed class FileNameQuery : IComparable<byte[]>
        {
            private byte[] _query;
            private UpperCase _upperCase;

            public FileNameQuery(string query, UpperCase upperCase)
            {
                _query = Encoding.Unicode.GetBytes(query);
                _upperCase = upperCase;
            }

            public int CompareTo(byte[] buffer)
            {
                // Note: this is internal knowledge of FileNameRecord structure - but for performance
                // reasons, we don't want to decode the entire structure.  In fact can avoid the string
                // conversion as well.
                byte fnLen = buffer[0x40];
                return _upperCase.Compare(_query, 0, _query.Length, buffer, 0x42, fnLen * 2);
            }
        }
    }
}
