//------------------------------------------------------------------------------
// <auto-generated />
// This comment is here to prevent StyleCop from analyzing a file originally from Serilog.
//------------------------------------------------------------------------------

// Copyright 2013-2017 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Modified by OpenTelemetry Authors.

using System.Text;

namespace OpenTelemetry.AutoInstrumentation.Logging
{
    sealed class RollingFileSink : ISink, IDisposable
    {
        readonly PathRoller _roller;
        readonly long _fileSizeLimitBytes;
        readonly int? _retainedFileCountLimit;
        readonly TimeSpan? _retainedFileTimeLimit;
        readonly bool _rollOnFileSizeLimit;

        readonly object _syncRoot = new object();
        bool _isDisposed;
        DateTime? _nextCheckpoint;
#pragma warning disable CS8669
        FileSink? _currentFile;
#pragma warning restore CS8669
        int? _currentFileSequence;

        public RollingFileSink(string path,
                              long fileSizeLimitBytes,
                              int? retainedFileCountLimit,
                              RollingInterval rollingInterval,
                              bool rollOnFileSizeLimit,
                              TimeSpan? retainedFileTimeLimit)
        {
            if (path == null)
            {
                throw new ArgumentNullException(nameof(path));
            }

            if (fileSizeLimitBytes < 1)
            {
                throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte.");
            }

            if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1)
            {
                throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1");
            }

            if (retainedFileTimeLimit.HasValue && retainedFileTimeLimit < TimeSpan.Zero)
            {
                throw new ArgumentException("Negative value provided; retained file time limit must be non-negative.", nameof(retainedFileTimeLimit));
            }

            _roller = new PathRoller(path, rollingInterval);
            _fileSizeLimitBytes = fileSizeLimitBytes;
            _retainedFileCountLimit = retainedFileCountLimit;
            _retainedFileTimeLimit = retainedFileTimeLimit;
            _rollOnFileSizeLimit = rollOnFileSizeLimit;
        }

        public void Write(string message)
        {
            if (message == null)
            {
                throw new ArgumentNullException(nameof(message));
            }

            lock (_syncRoot)
            {
                if (_isDisposed)
                {
                    throw new ObjectDisposedException("The log file has been disposed.");
                }

                var now = DateTime.Now;
                AlignCurrentFileTo(now);

                while (_currentFile?.Write(message) == false && _rollOnFileSizeLimit)
                {
                    AlignCurrentFileTo(now, nextSequence: true);
                }
            }
        }

        void AlignCurrentFileTo(DateTime now, bool nextSequence = false)
        {
            if (!_nextCheckpoint.HasValue)
            {
                OpenFile(now);
            }
            else if (nextSequence || now >= _nextCheckpoint.Value)
            {
                int? minSequence = null;
                if (nextSequence)
                {
                    if (_currentFileSequence == null)
                        minSequence = 1;
                    else
                        minSequence = _currentFileSequence.Value + 1;
                }

                CloseFile();
                OpenFile(now, minSequence);
            }
        }

        void OpenFile(DateTime now, int? minSequence = null)
        {
            var currentCheckpoint = _roller.GetCurrentCheckpoint(now);

            // We only try periodically because repeated failures
            // to open log files REALLY slow an app down.
            _nextCheckpoint = _roller.GetNextCheckpoint(now) ?? now.AddMinutes(30);

            var existingFiles = Enumerable.Empty<string>();
            try
            {
                if (Directory.Exists(_roller.LogFileDirectory))
                {
                    existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
                                        .Select(f => Path.GetFileName(f));
                }
            }
            catch (DirectoryNotFoundException) { }

            var latestForThisCheckpoint = _roller
                .SelectMatches(existingFiles)
                .Where(m => m.DateTime == currentCheckpoint)
                .OrderByDescending(m => m.SequenceNumber)
                .FirstOrDefault();

            var sequence = latestForThisCheckpoint?.SequenceNumber;
            if (minSequence != null)
            {
                if (sequence == null || sequence.Value < minSequence.Value)
                    sequence = minSequence;
            }

            const int maxAttempts = 3;
            for (var attempt = 0; attempt < maxAttempts; attempt++)
            {
                _roller.GetLogFilePath(now, sequence, out var path);

                try
                {
                    _currentFile = new FileSink(path, _fileSizeLimitBytes);

                    _currentFileSequence = sequence;
                }
                catch (IOException)
                {
                    sequence = (sequence ?? 0) + 1;
                    continue;
                }

                ApplyRetentionPolicy(path, now);
                return;
            }
        }

        void ApplyRetentionPolicy(string currentFilePath, DateTime now)
        {
            if (_retainedFileCountLimit == null && _retainedFileTimeLimit == null) return;

            var currentFileName = Path.GetFileName(currentFilePath);

            // We consider the current file to exist, even if nothing's been written yet,
            // because files are only opened on response to an event being processed.
            var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
                .Select(f => Path.GetFileName(f))
                .Union(new[] { currentFileName });

            var newestFirst = _roller
                .SelectMatches(potentialMatches)
                .OrderByDescending(m => m.DateTime)
                .ThenByDescending(m => m.SequenceNumber);

            var toRemove = newestFirst
                .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n.Filename) != 0)
                .SkipWhile((f, i) => ShouldRetainFile(f, i, now))
                .Select(x => x.Filename)
                .ToList();

            foreach (var obsolete in toRemove)
            {
                var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete);
                try
                {
                    System.IO.File.Delete(fullPath);
                }
                catch (Exception)
                {
                    // unable to remove obsolete file
                }
            }
        }

        bool ShouldRetainFile(RollingLogFile file, int index, DateTime now)
        {
            if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value - 1)
                return false;

            if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue &&
                file.DateTime.Value < now.Subtract(_retainedFileTimeLimit.Value))
            {
                return false;
            }

            return true;
        }

        public void Dispose()
        {
            lock (_syncRoot)
            {
                if (_currentFile == null) return;
                CloseFile();
                _isDisposed = true;
            }
        }

        void CloseFile()
        {
            if (_currentFile != null)
            {
                (_currentFile as IDisposable)?.Dispose();
                _currentFile = null;
            }

            _nextCheckpoint = null;
        }

        public void FlushToDisk()
        {
            lock (_syncRoot)
            {
                _currentFile?.FlushToDisk();
            }
        }
    }
}
