//
// Copyright 2020 Electronic Arts Inc.
//
// The Command & Conquer Map Editor and corresponding source code is free 
// software: you can redistribute it and/or modify it under the terms of 
// the GNU General Public License as published by the Free Software Foundation, 
// either version 3 of the License, or (at your option) any later version.

// The Command & Conquer Map Editor and corresponding source code is distributed 
// in the hope that it will be useful, but with permitted additional restrictions 
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT 
// distributed with this program. You should have received a copy of the 
// GNU General Public License along with permitted additional restrictions 
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace MobiusEditor.Utility
{
    public class MegafileBuilder : IDisposable
    {
        #region IDisposable Support
        private bool disposedValue = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    Out.Dispose();
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }
        #endregion

        private const float Version = 0.99f;

        public string RootPath { get; private set; }

        private Stream Out { get; set; }

        private List<(string, object)> Files = new List<(string, object)>();

        public MegafileBuilder(string rootPath, string outFile)
        {
            RootPath = rootPath.ToUpper();
            Out = new FileStream(outFile, FileMode.Create);
        }

        public void AddFile(string path)
        {
            if (File.Exists(path))
            {
                Files.Add((Path.GetFileName(path), path));
            }
        }

        public void AddFile(string path, Stream stream)
        {
            Files.Add((Path.GetFileName(path), stream));
        }

        public void AddDirectory(string path)
        {
            AddDirectory(path, "*.*");
        }

        public void AddDirectory(string path, string searchPattern)
        {
            var uriPath = new Uri(path);
            foreach (var file in Directory.GetFiles(path, searchPattern, SearchOption.AllDirectories))
            {
                var relativePath = Uri.UnescapeDataString(uriPath.MakeRelativeUri(new Uri(file)).ToString()).Replace('/', Path.DirectorySeparatorChar);
                Files.Add((relativePath, file));
            }
        }

        public void Write()
        {
            var headerSize = sizeof(uint) * 6U;
            headerSize += SubFileData.Size * (uint)Files.Count;

            var strings = new List<string>();
            Func<string, ushort> stringIndex = (string value) =>
            {
                var index = strings.IndexOf(value);
                if (index < 0)
                {
                    index = strings.Count;
                    if (index > ushort.MaxValue)
                    {
                        throw new IndexOutOfRangeException();
                    }
                    strings.Add(value);
                }
                return (ushort)index;
            };

            var files = new List<(ushort index, uint crc, Stream stream, bool dispose)>();
            foreach (var (filename, source) in Files)
            {
                var name = Encoding.ASCII.GetBytes(filename);
                var crc = CRC.Calculate(name);

                if (source is string)
                {
                    var file = source as string;
                    if (File.Exists(file))
                    {
                        files.Add((stringIndex(Path.Combine(RootPath, filename).ToUpper()), crc, new FileStream(file, FileMode.Open, FileAccess.Read), true));
                    }
                }
                else if (source is Stream)
                {
                    files.Add((stringIndex(Path.Combine(RootPath, filename).ToUpper()), crc, source as Stream, false));
                }
            }
            files = files.OrderBy(x => x.crc).ToList();

            var stringsSize = sizeof(ushort) * (uint)strings.Count;
            stringsSize += (uint)strings.Sum(x => x.Length);
            headerSize += stringsSize;

            var subfileImageOffset = headerSize;
            using (var writer = new BinaryWriter(Out))
            {
                writer.Write(0xFFFFFFFF);
                writer.Write(Version);
                writer.Write(headerSize);

                writer.Write((uint)Files.Count);
                writer.Write((uint)strings.Count);
                writer.Write(stringsSize);

                foreach (var item in strings)
                {
                    writer.Write((ushort)item.Length);
                    writer.Write(item.ToCharArray());
                }

                using (var fileStream = new MemoryStream())
                {
                    for (var i = 0; i < files.Count; ++i)
                    {
                        var (index, crc, stream, dispose) = files[i];

                        var fileSize = (uint)(stream.Length - stream.Position);
                        var fileBytes = new byte[fileSize];
                        stream.Read(fileBytes, 0, fileBytes.Length);
                        fileStream.Write(fileBytes, 0, fileBytes.Length);

                        if (dispose)
                        {
                            stream.Dispose();
                        }

                        SubFileData data = new SubFileData
                        {
                            Flags = 0,
                            CRCValue = crc,
                            SubfileIndex = i,
                            SubfileSize = fileSize,
                            SubfileImageDataOffset = subfileImageOffset,
                            SubfileNameIndex = index
                        };

                        var ptr = Marshal.AllocHGlobal((int)SubFileData.Size);
                        Marshal.StructureToPtr(data, ptr, false);
                        var bytes = new byte[SubFileData.Size];
                        Marshal.Copy(ptr, bytes, 0, bytes.Length);
                        Marshal.FreeHGlobal(ptr);

                        writer.Write(bytes);

                        subfileImageOffset += data.SubfileSize;
                    }

                    fileStream.Seek(0, SeekOrigin.Begin);
                    fileStream.CopyTo(writer.BaseStream);
                }
            }
        }
    }
}