#region <<< Using Directives >>>
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;

using Volpe.Cafe.Resources;

using TechKey = Volpe.Cafe.TechKeys.TechKey128;
#endregion

namespace Volpe.Cafe.VehicleSimulation
{
    /// <summary>
    /// Provides a database of vehicle simulation results for all simulated technology combinations and classes.
    /// </summary>
    [Serializable]
    public static class SimulationDatabase
    {
        #region <<< Variables >>>

        private const int BaseFileCount = 3;
        private const int ExtendedFileCount = 8;

        private static readonly int[] ExtendedDataIndexes = new int[] { 3, 4, 5, 6, 7 };

        // init capacity with prime >= current count of expected records for each file
        private static DataFileInfo[] DataFiles = new DataFileInfo[] {
            new DataFileInfo("FE1_Adjustments.csv", 150001, true),
            new DataFileInfo("FE2_Adjustments.csv",    727, true),
            new DataFileInfo("Battery_Costs.csv",   150001, true),
            new DataFileInfo("BatteryEnergy.csv",   150001, false),
            new DataFileInfo("Motor1PeakPwr.csv",   150001, false),
            new DataFileInfo("Motor1ContPwr.csv",   150001, false),
            new DataFileInfo("Motor2PeakPwr.csv",   150001, false),
            new DataFileInfo("Motor2ContPwr.csv",   150001, false),
        };

        private static List<string>[] FileLines = new List<string>[DataFiles.Length];

        // init capacity with prime >= 2x the current count of expected records in the dictionary
        // this will allow for the load factor to be <=0.5, which should improve key distribution
        private static Dictionary<TechKey, ISimulationResult> DB = new Dictionary<TechKey, ISimulationResult>(300007);

        // object for synchronizing multi-threaded DB initialization requests
        private static readonly object InitSync = new object();

        #endregion

        #region <<< Properties >>>

        /// <summary>
        /// Gets whether the vehicle simulation database files have been successfully pre-cached.
        /// </summary>
        public static bool IsPrecached { get; private set; } = false;

        /// <summary>
        /// Gets whether the vehicle simulation database has been successfully parsed and initialized.
        /// </summary>
        public static bool IsInitialized { get; private set; } = false;

        /// <summary>
        /// Gets whether the vehicle simulation database includes results for extended simulation data.
        /// </summary>
        public static bool HasExtendedData { get; private set; } = false;

        #endregion

        #region <<< Methods >>>

        #region < Simulation DB initialization methods >

        /// <summary>
        /// Parses and initializes the vehicle simulation database using the specified mock data file.
        /// </summary>
        /// <param name="filePath">The path to the mock data file to load.</param>
        /// <returns>
        /// <c>true</c>, if the vehicle simulation database has been successfully initialized; <c>false</c>, otherwise.
        /// </returns>
        /// <exception cref="ArgumentNullException">Mock data file is null.</exception>
        /// <exception cref="FileNotFoundException">Mock data file could not be found.</exception>
        public static bool InitializeWithMockData(string filePath)
        {
            if (filePath == null) {
                throw new ArgumentNullException(nameof(filePath), "Mock data file may not be null.");
            }
            if (!File.Exists(filePath)) {
                throw new FileNotFoundException($"The specified mock data file was not found.", filePath);
            }

            lock (InitSync) {
                try {
                    IsPrecached = UnzipSimulationData(true, mockDataFile: filePath);
                    if (IsPrecached) {
                        IsInitialized = ParseSimulationData(true);
                        Array.Clear(FileLines, 0, FileLines.Length);
                        ForceGC();
                    }
                }
                catch {
                    IsPrecached = false;
                }

                if (!IsPrecached || !IsInitialized) {
                    IsPrecached = false;
                    IsInitialized = false;
                    HasExtendedData = false;
                }
            }

            return IsInitialized;
        }

        /// <summary>
        /// Extracts and pre-caches the vehicle simulation database into memory.
        /// </summary>
        /// <param name="silent">
        ///     Specifies whether vehicle simulation database processing errors should be displayed in the console
        ///     (silent=<c>false</c>) or suppressed (silent=<c>true</c>).
        /// </param>
        /// <returns>
        /// <c>true</c>, if the vehicle simulation database has been successfully pre-cached; <c>false</c>, otherwise.
        /// </returns>
        /// <remarks>
        /// If the vehicle simulation database has already been pre-cached or initialized, this function call has no
        /// effect.
        /// </remarks>
        public static bool Precache(bool silent = false)
        {
            lock (InitSync) {
                if (!IsPrecached) {
                    IsPrecached = UnzipSimulationData(silent);
                    ForceGC();
                }
            }
            return IsPrecached;
        }

        /// <summary>
        /// Parses and initializes the vehicle simulation database.
        /// </summary>
        /// <param name="silent">
        ///     Specifies whether vehicle simulation database processing errors should be displayed in the console
        ///     (silent=<c>false</c>) or suppressed (silent=<c>true</c>).
        /// </param>
        /// <returns>
        /// <c>true</c>, if the vehicle simulation database has been successfully initialized; <c>false</c>, otherwise.
        /// </returns>
        /// <remarks>
        /// If the vehicle simulation database has already been initialized, this function call has no effect. If the
        /// database is in the process of being initialized (via a previous call to <see cref="Initialize(bool)"/> or
        /// <see cref="InitializeAsync(bool)"/>), this function call will block until the pending operation completes.
        /// <br/><br/>
        /// If the vehicle simulation database has not been previously extracted and pre-cached into memory, it will be
        /// done so prior to initialization.
        /// </remarks>
        public static bool Initialize(bool silent = false)
        {
            lock (InitSync) {
                if (!IsInitialized) {
                    if (Precache()) {
                        IsInitialized = ParseSimulationData(silent);
                        FileLines = null;
                        ForceGC();
                    }
                }
            }
            return IsInitialized;
        }

        /// <summary>
        /// Parses and initializes the vehicle simulation database asynchronously.
        /// </summary>
        /// <param name="silent">
        ///     Specifies whether vehicle simulation database processing errors should be displayed in the console
        ///     (silent=<c>false</c>) or suppressed (silent=<c>true</c>).
        /// </param>
        /// <returns>
        /// A task that represents the asynchronous initialization operation. The value of the <c>TResult</c> parameter
        /// specifies: <c>true</c>, if the vehicle simulation database has been successfully initialized; or
        /// <c>false</c>, if initialization failed.
        /// </returns>
        /// <remarks>
        /// If the vehicle simulation database has already been initialized, this function call has no effect. If the
        /// database is in the process of being initialized (via a previous call to <see cref="Initialize(bool)"/> or
        /// <see cref="InitializeAsync(bool)"/>), the underlying task of this function call will block until the
        /// pending operation completes.
        /// <br/><br/>
        /// If the vehicle simulation database has not been previously extracted and pre-cached into memory, it will be
        /// done so prior to initialization.
        /// </remarks>
        public static async Task<bool> InitializeAsync(bool silent = false)
        {
            bool initialized = await Task.Run(() => Initialize(silent));
            return initialized;
        }

        /// <summary>
        /// Extracts the vehicle simulation database to the specified directory.
        /// </summary>
        /// <param name="path">The path where to extract the vehicle simulation database.</param>
        /// <returns>
        /// <c>true</c>, if the vehicle simulation database has been successfully extracted; <c>false</c>, otherwise.
        /// </returns>
        public static bool ExtractSimulationData(string path)
        {
            return UnzipSimulationData(false, path);
        }

        private static bool UnzipSimulationData(bool silent, string outPath = null, string mockDataFile = null)
        {
            bool isValid = true;
            bool[] filesFound;

            using (Stream zipStream = (mockDataFile == null)
                                    ? AppResources.GetResourceStream(EmbeddedResources.SimulationDatabase)
                                    : File.OpenRead(mockDataFile))
            using (ZipArchive zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read)) {
                HasExtendedData = zipArchive.Entries.Count == ExtendedFileCount;
                filesFound = HasExtendedData ? new bool[ExtendedFileCount] : new bool[BaseFileCount];

                foreach (ZipArchiveEntry zipEntry in zipArchive.Entries) {
                    string fileName = zipEntry.Name;
                    int fileIndex = Array.FindIndex(DataFiles,
                        item => item.FileName.Equals(fileName, StringComparison.InvariantCultureIgnoreCase));

                    if (!HasExtendedData && ExtendedDataIndexes.Contains(fileIndex)) {
                        LogHelper($"Incomplete extended database; extended file entry found: '{fileName}'.", silent);
                        isValid = false;
                        continue;
                    }

                    if (fileIndex == -1) {
                        LogHelper($"Invalid file entry found: '{fileName}'.", silent);
                        isValid = false;
                        continue;
                    }
                    else if (filesFound[fileIndex]) {
                        LogHelper($"Duplicate file entry found: '{fileName}'.", silent);
                        isValid = false;
                        continue;
                    }

                    filesFound[fileIndex] = true;
                    UnzipFileEntry(zipEntry, fileIndex, outPath);
                }
            }

            // ensure all required files were found in zip file
            for (int i = 0; i < filesFound.Length; i++) {
                if (!filesFound[i]) {
                    LogHelper($"Input file not found: '{DataFiles[i].FileName}'.", silent);
                    isValid = false;
                }
            }

            return isValid;
        }

        private static void UnzipFileEntry(ZipArchiveEntry zipEntry, int fileIndex, string outPath)
        {
            using (var reader = new StreamReader(zipEntry.Open())) {
                if (outPath == null) {
                    UnzipToMemory(reader);
                }
                else {
                    UnzipToFile(reader);
                }
            }

            // local helper functions
            void UnzipToMemory(StreamReader sr)
            {
                var fileLines = new List<string>(DataFiles[fileIndex].LineCount);
                FileLines[fileIndex] = fileLines;

                string line;
                while ((line = sr.ReadLine()) != null) {
                    fileLines.Add(line);
                }
            }

            void UnzipToFile(StreamReader sr)
            {
                using (StreamWriter sw = new StreamWriter($"{outPath}\\{DataFiles[fileIndex].FileName}")) {
                    string line;
                    while ((line = sr.ReadLine()) != null) {
                        sw.WriteLine(line);
                    }
                }
            }
        }

        private static bool ParseSimulationData(bool silent)
        {
            int count = HasExtendedData ? ExtendedFileCount : BaseFileCount;
            bool isValid = true;
            DBParser parser = new DBParser();
            DB.Clear();

            Parallel.For(0, count, i => {
                if (parser.Parse(DataFiles[i].FileName, FileLines[i], out var dict, silent)) {
                    lock (DB) {
                        MergeData(dict, i);
                    }
                }
                else {
                    isValid = false;
                }
            });

            return isValid;

            // local helper function
            void MergeData(Dictionary<TechKey, double[]> dict, int index)
            {
                // iterate the new dict for all keys
                // then add/update the DB with simulation results
                foreach (TechKey key in dict.Keys) {
                    if (DB.TryGetValue(key, out var value)) {
                        ((SimulationResult)value)[index] = dict[key];
                    }
                    else {
                        var newValue = new SimulationResult(key);
                        newValue[index] = dict[key];
                        DB.Add(key, newValue);
                    }
                }
            }
        }

        #endregion

        #region < Simulation DB querying methods >

        /// <summary>
        /// Determines whether the simulation database contains vehicle simulation results for a technology combination
        /// defined by the specified key.
        /// </summary>
        /// <param name="key">The technology key for which to check vehicle simulation results.</param>
        /// <returns>
        /// <c>true</c>, if the simulation database contains vehicle simulation results for the specified key;
        /// <c>false</c>, otherwise.
        /// </returns>
        public static bool HasValue(TechKey key)
        {
            if (!IsInitialized) {
                ThrowHelper();
            }
            return DB.ContainsKey(key);
        }

        /// <summary>
        /// Obtains vehicle simulation results for a technology combination defined by the specified key.
        /// </summary>
        /// <param name="key">The technology key for which to obtain vehicle simulation results.</param>
        /// <param name="value">
        ///     Vehicle simulation results for a technology combination defined by the specified key.
        /// </param>
        /// <returns>
        /// <c>true</c>, if the simulation database contains vehicle simulation results for the specified key;
        /// <c>false</c>, otherwise.
        /// </returns>
        public static bool TryGetValue(TechKey key, out ISimulationResult value)
        {
            if (!IsInitialized) {
                ThrowHelper();
            }
            return DB.TryGetValue(key, out value);
        }

        /// <summary>
        /// Returns vehicle simulation results for a technology combination defined by the specified key. If the key is
        /// not found in the database, an empty value will be returned.
        /// </summary>
        /// <param name="key">The technology key for which to obtain vehicle simulation results.</param>
        /// <returns>Vehicle simulation results for a technology combination defined by the specified key.</returns>
        public static ISimulationResult GetValue(TechKey key)
        {
            if (!TryGetValue(key, out var value)) {
                value = SimulationResult.Empty;
            }
            return value;
        }

        /// <summary>
        /// Searches the database for vehicle simulation results matching the specified query.
        /// </summary>
        /// <param name="query">
        ///     The search query, defining a partial or complete set of technologies to search for.
        /// </param>
        /// <returns>
        /// One or more vehicle simulation results matching the specified query; or an empty list, if no results could
        /// be found.
        /// </returns>
        public static List<ISimulationResult> Search(TechKey query)
        {
            if (!IsInitialized) {
                ThrowHelper();
            }

            var searchResults = new List<ISimulationResult>();
            foreach (var item in DB.Values) {
                if (item.TechKey.Contains(query)) {
                    searchResults.Add(item);
                }
            }
            return searchResults;
        }

        #endregion

        // basic ThrowHelper implementation right now -- extend as needed
        internal static void ThrowHelper()
        {
            throw new InvalidOperationException(
                "The vehicle simulation database was not initialized. The application will not run.");
        }

        internal static void LogHelper(string message, bool silent)
        {
            if (!silent) {
                Console.WriteLine(message);
            }
        }

        /// <summary>
        /// Force garbage collection to run.
        /// </summary>
        private static void ForceGC()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

        #endregion
    }
}
