// Created with MonoDevelop
//
//    Copyright (C) 2010 Hiperscan GmbH Dresden, Germany
//
//    This program 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.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace Hiperscan.Unix
{
    public static class FileIO
    {
        /// <summary>File access is logged when taking longer than this threshold. Negative values disable measurement.</summary>
        public static TimeSpan file_access_threshold = TimeSpan.FromSeconds(1d);

        /// <summary>
        ///     <para>Supposed to be set by function <c>Main</c> from parameter <c>--debug</c></para>
        ///     <para>Logs all file system accesses, even if below <see cref="file_access_threshold"/>.</para>
        /// </summary>
        public static bool is_debug = false;

        public static string CacheDir
        {
            get
            {
                string path = string.Empty;

                switch (Environment.OSVersion.Platform)
                {

                case PlatformID.Unix:
                    path = "/var/cache";
                    break;

                case PlatformID.Win32NT:
                case PlatformID.Win32S:
                case PlatformID.Win32Windows:
                case PlatformID.WinCE:
                case PlatformID.Xbox:
                    path = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
                    break;

                default:
                    throw new NotSupportedException(string.Format(Catalog.GetString("Platform {0} is not supported."),
                                                                  Environment.OSVersion.Platform));
                }

                return path;
            }
        }

        private static Func<string, object> ToFunc(Action<string> action)
        {
            return param =>
            {
                action(param);
                return null;
            };
        }

        /// <summary>Generates a cropped stack trace with frames relevant to the file access reported by <see cref="ProcessMeasurement"/>.</summary>
        private static string StackTrace
        {
            [EnvironmentPermission(SecurityAction.Demand, Unrestricted = true)]
            get
            {
                var stack_trace = new StackTrace(0, false);

                // Have to play it stupid, because all the helpful functions are internal. Thank you, Microsoft! -SIB 2024-06-12
                var lines = stack_trace.ToString().Split(Environment.NewLine.ToCharArray()).
                                       Where(s => String.IsNullOrEmpty(s) == false).
                                       ToArray();
                var my_name = typeof(FileIO).UnderlyingSystemType.FullName;
                var ignorable_names = new [] { "System.Threading" };

                // dynamically drop frames at begin (all but last in FileIO)
                var begin = 1;
                while (lines[begin].Contains(my_name))
                    ++begin;
                --begin;
                // dynamically drop frames at end (all but first in ignorable_names)
                var end = begin + 1;
                while (end < lines.Length && ignorable_names.All(name => false == lines[end].Contains(name)))
                    ++end;
                end = Math.Min(end, lines.Length);

                var elines = lines.Skip(begin).Take(end)
                    // cull file information not collected anyway
                                  .Select(line =>  FileIO.CleanStackTraceLine(line));
                return string.Join(Environment.NewLine, elines);
            }
        }

        private static string CleanStackTraceLine(string line)
        {
            line = line ?? String.Empty;
            int index = line.IndexOf(" in ");

            if (index >= 0)
                return line.Remove(index);

            return line;
        }

        /// <summary>skip measurement if <see cref="file_access_threshold"/> is negative</summary>
        private static bool measuring_disabled => FileIO.file_access_threshold.Duration() != FileIO.file_access_threshold;

        private static void ProcessMeasurement(TimeSpan elapsed, string path) => FileIO.ProcessMeasurement<object>(null, elapsed, path);

        private static TResult ProcessMeasurement<TResult>(TResult result, TimeSpan elapsed, string path)
        {
            string StackTrace() => FileIO.is_debug ? Environment.StackTrace : FileIO.StackTrace;
            if (elapsed > FileIO.file_access_threshold)
                Console.WriteLine("File system access exceeded expected threshold time! Path: '{0}', time: '{1}', threshold: '{2}'{3}{4}",
                                  path, elapsed, FileIO.file_access_threshold, Environment.NewLine, StackTrace());
            else if (FileIO.is_debug)
                Console.WriteLine("File system access within expected threshold time. Path: '{0}', time: '{1}', threshold: '{2}'{3}{4}",
                                  path, elapsed, FileIO.file_access_threshold, Environment.NewLine, StackTrace());
            return result;
        }

        private static void DoTimedFileAccess(Action<string> file_access, string path) => FileIO.DoTimedFileAccess(FileIO.ToFunc(file_access), path);
        private static TResult DoTimedFileAccess<TResult>(Func<string, TResult> file_access, string path) => FileIO.DoTimedFileAccess(file_access, path, path);

        private static TResult DoTimedFileAccess<TInput, TResult>(Func<TInput, TResult> file_access, TInput source, string path)
        {
            if (FileIO.measuring_disabled)
                return file_access(source);

            var watch = new Stopwatch();
            watch.Start();
            var result = file_access(source);
            watch.Stop();

            return FileIO.ProcessMeasurement(result, watch.Elapsed, path);
        }

        private static void DoTimedFileAccess(Action<string, string> file_access, string path_source, string path_destination)
        {
            if (FileIO.measuring_disabled)
            {
                file_access(path_source, path_destination);
                return;
            }

            var watch = new Stopwatch();
            watch.Start();
            file_access(path_source, path_destination);
            watch.Stop();

            var elapsed = watch.Elapsed;
            FileIO.ProcessMeasurement(elapsed, path_source);
            FileIO.ProcessMeasurement(elapsed, path_destination);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static DirectoryInfo CreateDirectory(string path) => FileIO.DoTimedFileAccess(Directory.CreateDirectory, path);

        /// <summary>
        ///     <para>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</para>
        ///     <para>Creates the directory with given attributes, but does not alter any existing directory.</para>
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static DirectoryInfo CreateDirectoryIfNotExists(string path, FileAttributes attributes)
        {
            if (FileIO.ExistsDirectory(path))
                return new DirectoryInfo(path);

            DirectoryInfo Access(string p)
            {
                var directoryInfo = Directory.CreateDirectory(p);
                directoryInfo.Attributes = attributes;
                return directoryInfo;
            }
            return FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void DeleteDirectory(string path, bool recursively)
        {
            void Access(string p) => Directory.Delete(p, recursively);
            FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        /// <param name="quiet">Swallows exceptions from <see cref="File.Delete"/> if <see langword="true"/>.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void DeleteDirectory(string path, bool recursively, bool quiet)
        {
            void AccessQuiet(string p)
            {
                try { Directory.Delete(p, recursively); }
#pragma warning disable RECS0022
                catch { }
#pragma warning restore RECS0022
            }
            FileIO.DoTimedFileAccess(quiet ? (Action<string>)AccessQuiet : Directory.Delete, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static bool ExistsDirectory(string path) => FileIO.DoTimedFileAccess(Directory.Exists, path);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string[] GetDirectories(string path) => FileIO.DoTimedFileAccess(Directory.GetDirectories, path);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string[] GetDirectories(string path, string search_pattern)
        {
            string[] Access(string p) => Directory.GetDirectories(p, search_pattern);
            return FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string[] GetDirectories(string path, string search_pattern, SearchOption search_option)
        {
            string[] Access(string p) => Directory.GetDirectories(p, search_pattern, search_option);
            return FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string[] GetFiles(string path) => FileIO.DoTimedFileAccess(Directory.GetFiles, path);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string[] GetFiles(string path, string search_pattern)
        {
            string[] Access(string p) => Directory.GetFiles(p, search_pattern);
            return FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string[] GetFiles(string path, string search_pattern, SearchOption search_option)
        {
            string[] Access(string p) => Directory.GetFiles(p, search_pattern, search_option);
            return FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void MoveDirectory(string path_source, string path_destination) => FileIO.DoTimedFileAccess(Directory.Move, path_source, path_destination);

        /// <summary>
        ///     <para>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</para>
        ///     <para>First deletes the directory if <paramref name="only_if"/> and it already exists, then creates it anew.</para>
        /// </summary>
        public static DirectoryInfo RecreateDirectory(string path, bool only_if = true)
        {
            if (only_if && FileIO.ExistsDirectory(path))
                FileIO.DeleteDirectory(path, true);
            return FileIO.CreateDirectory(path);
        }

        /// <summary>
        ///     <para>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</para>
        ///     <para>First deletes the directory if <paramref name="only_if"/> and it already exists, then creates it anew.</para>
        ///     <para>Then sets <paramref name="attributes"/> on the resulting <see cref="DirectoryInfo"/></para>
        /// </summary>
        public static DirectoryInfo RecreateDirectory(string path, FileAttributes attributes, bool only_if = true)
        {
            var directoryInfo = FileIO.RecreateDirectory(path, only_if);
            directoryInfo.Attributes = attributes;
            return directoryInfo;
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static FileStream CreateFile(string path) => FileIO.DoTimedFileAccess(File.Create, path);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static StreamWriter CreateTextFile(string path) => FileIO.DoTimedFileAccess(File.CreateText, path);

        /// <summary>
        ///     <para>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</para>
        ///     <para>Generates a log statement for both <paramref name="path_source"/> and <paramref name="path_destination"/>
        ///           as there is no way to know which one was responsible.</para>
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void CopyFile(string path_source, string path_destination, bool replace = false)
        {
            void Access(string s, string d) => File.Copy(s, d, replace);
            FileIO.DoTimedFileAccess(Access, path_source, path_destination);
        }

        public static void DeleteFile(string path) => FileIO.DeleteFile(path, false);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        /// <param name="quiet">Swallows exceptions from <see cref="File.Delete"/> if <see langword="true"/>.</param>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void DeleteFile(string path, bool quiet)
        {
            void AccessQuiet(string p)
            {
                try { File.Delete(p); }
#pragma warning disable RECS0022
                catch {}
#pragma warning restore RECS0022
            }
            FileIO.DoTimedFileAccess(quiet ? (Action<string>)AccessQuiet : File.Delete, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static bool ExistsFile(string path) => FileIO.DoTimedFileAccess(File.Exists, path);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static StreamReader OpenTextFile(string path) => FileIO.DoTimedFileAccess(File.OpenText, path);

        /// <summary>
        ///     <para>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</para>
        ///     <para>Treats file encoding as <see cref="Encoding.UTF8"/>.</para>
        /// </summary>
        public static string ReadFileToEnd(string path) => FileIO.ReadFileToEnd(path, Encoding.UTF8);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static string ReadFileToEnd(string path, Encoding encoding)
        {
            string Access(string p)
            {
                using (var sr = new StreamReader(p, encoding))
                    return sr.ReadToEnd();
            }
            return FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        [Obsolete("BOM is deprecated and 'considered harmful'! Use explicit encoding or utf-8-everywhere! -SIB 2024-05-30")]
        public static string ReadFileToEndWithEncodingFromBOM(string path)
        {
            string Access(string p)
            {
                using (var sr = new StreamReader(p, true))
                    return sr.ReadToEnd();
            }
            return FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static byte[] ReadFileAsBytes(string path) => FileIO.DoTimedFileAccess(File.ReadAllBytes, path);

        /// <summary>
        ///     <para>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</para>
        ///     <para>Prefer more fitting alternatives where possible.</para>
        /// </summary>
        public static FileStream OpenFile(string path, FileMode mode) => FileIO.measuring_disabled
            ? new FileStream(path, mode)
            : new FileStreamWithMeasurement(path, mode);

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void Save(XmlDocument document, string path)
        {
            void Access(string p)
            {
                using (var writer = new StreamWriter(path, false, Encoding.UTF8))
                    document.Save(writer);
            }
            FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void Serialise(object to_be_serialised, string path)
        {
            void Access(string p)
            {
                using (var writer = new StreamWriter(path, false, Encoding.UTF8))
                    new XmlSerializer(to_be_serialised.GetType()).Serialize(writer, to_be_serialised);
            }
            FileIO.DoTimedFileAccess(Access, path);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        public static void Write(string content, string path, bool append, Encoding encoding)
        {
            using (var writer = new FileIO.FileStreamWriter(path, append, encoding))
                writer.Write(content);
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        public static void WriteLines(IEnumerable<string> lines, string path)
        {
            using (var writer = new FileIO.FileStreamWriter(path, false, Encoding.UTF8))
                foreach (var line in lines)
                    writer.WriteLine(line);
        }

        /// <summary>
        ///     Helper class for cases where a Stream from different sources is passed down a hierarchy of functions
        ///     and the original source is no longer reachable at the call site.
        /// </summary>
        public class FileStreamReader : StreamReader
        {
            /// <remarks>
            ///     The default to read BOMs should hopefully eventually disappear from .NET.
            ///     Check every other .NET version or so whether we cannot just use StreamReader(Stream) by then.
            ///     -SIB 2024-08-05
            /// </remarks>
            public FileStreamReader(Stream source, Encoding encoding) : base(source, encoding, false) {}

            /// <remarks>
            ///     The default to read BOMs should hopefully eventually disappear from .NET.
            ///     Check every other .NET version or so whether we cannot just use StreamReader(Stream) by then.
            ///     -SIB 2024-08-05
            /// </remarks>
            public FileStreamReader(Stream source): base(source, false) {}

            [Obsolete("The constructor without explicit encoding uses UTF-8 without BOM - use that wherever possible! -SIB 2024-05-31")]
            public FileStreamReader(Stream source, Encoding encoding, bool bom) : base(source, encoding, bom) {}

            [Obsolete("The constructor without explicit encoding uses UTF-8 without BOM - use that wherever possible! -SIB 2024-05-31")]
            public FileStreamReader(Stream source, bool bom) : base(source, bom) {}

            /// <summary>
            ///     <para>Measures access time and logs warning if a <see cref="FileStream"/> has been passed to the constructor
            ///     and <see cref="file_access_threshold"/> has been exceeded.</para>
            ///     <para>Behaves as an ordinary <see cref="StreamReader"/> for other kinds of <see cref="Stream"/>.</para>
            /// </summary>
            [MethodImpl(MethodImplOptions.NoInlining)]
            public override string ReadToEnd()
            {
                if (this.BaseStream is FileStream)
                {
                    var stream = this.BaseStream as FileStream;
                    return FileIO.DoTimedFileAccess(_ => base.ReadToEnd(), stream, stream.Name);
                }
                return base.ReadToEnd();
            }
        }

        /// <summary>Measures access time and logs warning if <see cref="file_access_threshold"/> has been exceeded.</summary>
        public class FileStreamWriter : StreamWriter
        {
            public FileStreamWriter(string path, bool append = false) : base(path, append) {}

            [Obsolete("The constructor without explicit encoding uses UTF-8 without BOM - use that wherever possible! -SIB 2024-05-31")]
            public FileStreamWriter(string path, bool append, Encoding encoding) : base(path, append, encoding) {}

            [MethodImpl(MethodImplOptions.NoInlining)]
            public override void Write(string value) => FileIO.DoTimedFileAccess(base.Write, value);

            [MethodImpl(MethodImplOptions.NoInlining)]
            public override void WriteLine(string value) => FileIO.DoTimedFileAccess(base.WriteLine, value);
        }

        /// <summary>
        ///     <para>Helper class measuring all reads and writes as one operation</para>
        ///     <para>Unsuited for asynchronous file access and makes no attempt to track it.
        ///           <i>Will</i> report false-positives when used for sporadic access, e.g. as log output.</para>
        ///     <para>Should not generally be combined with the direct functions, lest the same access be measured twice.</para>
        /// </summary>
        public class FileStreamWithMeasurement : FileStream
        {
            private Stopwatch watch = new Stopwatch();

            public FileStreamWithMeasurement(string path, FileMode mode) : base(path, mode) {}

            public override int Read([In, Out] byte[] array, int offset, int count)
            {
                this.watch.Start();
                return base.Read(array, offset, count);
            }

            public override int ReadByte()
            {
                this.watch.Start();
                return base.ReadByte();
            }

            public override void Write(byte[] array, int offset, int count)
            {
                this.watch.Start();
                base.Write(array, offset, count);
            }

            public override void WriteByte(byte value)
            {
                this.watch.Start();
                base.WriteByte(value);
            }

            [MethodImpl(MethodImplOptions.NoInlining)]
            public override void Flush()
            {
                this.watch.Stop();
                FileIO.ProcessMeasurement(this.watch.Elapsed, this.Name);
                this.watch.Reset(); // has to be reset here, as Dispose will generally be called next -SIB 2024-08-06
                base.Flush();
            }

            [MethodImpl(MethodImplOptions.NoInlining)]
            protected override void Dispose(bool disposing)
            {
                this.watch.Stop();
                FileIO.ProcessMeasurement(this.watch.Elapsed, this.Name);
                base.Dispose(disposing);
            }
        }
    }
}
