#if UNMANAGED
using System.Runtime.InteropServices;

namespace SevenZip;

public record CompressStreamEntry(Stream? Stream, DateTimeOffset? CreateTime, DateTimeOffset? LastWrite, DateTimeOffset? LastAccess)
{
    public CompressStreamEntry(Stream? Stream, DateTimeOffset? fileTime) : this(Stream, fileTime, fileTime, fileTime)
    {
    }
}

internal sealed class ArchiveUpdateCallbackWithFileTime : CallbackBase, IArchiveUpdateCallback, ICryptoGetTextPassword2,
    IDisposable
{
    #region Fields
    /// <summary>
    /// _files.Count if do not count directories
    /// </summary>
    private int _actualFilesCount;

    /// <summary>
    /// For Compressing event.
    /// </summary>
    private long _bytesCount;

    private long _bytesWritten;
    private long _bytesWrittenOld;
    private SevenZipCompressor _compressor;

    /// <summary>
    /// No directories.
    /// </summary>
    private bool _directoryStructure;

    /// <summary>
    /// Rate of the done work from [0, 1]
    /// </summary>
    private float _doneRate;

    /// <summary>
    /// The names of the archive entries
    /// </summary>
    private string[] _entries;

    /// <summary>
    /// Array of files to pack
    /// </summary>
    private FileInfo[]? _files;

    private InStreamWrapper _fileStream;

    private uint _indexInArchive;
    private uint _indexOffset;

    /// <summary>
    /// Common root of file names length.
    /// </summary>
    private int _rootLength;

    /// <summary>
    /// Input streams to be compressed.
    /// </summary>
    private CompressStreamEntry[]? _streamEntries;


    private UpdateData _updateData;
    private List<InStreamWrapper> _wrappersToDispose;

    /// <summary>
    /// Gets or sets the default item name used in MemoryStream compression.
    /// </summary>
    public string DefaultItemName { private get; set; }

    /// <summary>
    /// Gets or sets the value indicating whether to compress as fast as possible, without calling events.
    /// </summary>
    public bool FastCompression { private get; set; }

    private int _memoryPressure;

    #endregion

    #region Constructors

    /// <summary>
    /// Initializes a new instance of the ArchiveUpdateCallback class
    /// </summary>
    /// <param name="files">Array of files to pack</param>
    /// <param name="rootLength">Common file names root length</param>
    /// <param name="compressor">The owner of the callback</param>
    /// <param name="updateData">The compression parameters.</param>
    /// <param name="directoryStructure">Preserve directory structure.</param>
    public ArchiveUpdateCallbackWithFileTime(
        FileInfo[] files, int rootLength,
        SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
    {
        Init(files, rootLength, compressor, updateData, directoryStructure);
    }

    /// <summary>
    /// Initializes a new instance of the ArchiveUpdateCallback class
    /// </summary>
    /// <param name="files">Array of files to pack</param>
    /// <param name="rootLength">Common file names root length</param>
    /// <param name="password">The archive password</param>
    /// <param name="compressor">The owner of the callback</param>
    /// <param name="updateData">The compression parameters.</param>
    /// <param name="directoryStructure">Preserve directory structure.</param>
    public ArchiveUpdateCallbackWithFileTime(
        FileInfo[] files, int rootLength, string password,
        SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
        : base(password)
    {
        Init(files, rootLength, compressor, updateData, directoryStructure);
    }

    /// <summary>
    /// Initializes a new instance of the ArchiveUpdateCallback class
    /// </summary>
    /// <param name="stream">The input stream</param>
    /// <param name="compressor">The owner of the callback</param>
    /// <param name="updateData">The compression parameters.</param>
    /// <param name="directoryStructure">Preserve directory structure.</param>
    public ArchiveUpdateCallbackWithFileTime(
        Stream stream, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
    {
        Init(stream, compressor, updateData, directoryStructure);
    }

    /// <summary>
    /// Initializes a new instance of the ArchiveUpdateCallback class
    /// </summary>
    /// <param name="stream">The input stream</param>
    /// <param name="password">The archive password</param>
    /// <param name="compressor">The owner of the callback</param>
    /// <param name="updateData">The compression parameters.</param>
    /// <param name="directoryStructure">Preserve directory structure.</param>
    public ArchiveUpdateCallbackWithFileTime(
        Stream stream, string password, SevenZipCompressor compressor, UpdateData updateData,
        bool directoryStructure)
        : base(password)
    {
        Init(stream, compressor, updateData, directoryStructure);
    }

    /// <summary>
    /// Initializes a new instance of the ArchiveUpdateCallback class
    /// </summary>
    /// <param name="streamDict">Dictionary&lt;file stream, name of the archive entry&gt;</param>
    /// <param name="compressor">The owner of the callback</param>
    /// <param name="updateData">The compression parameters.</param>
    /// <param name="directoryStructure">Preserve directory structure.</param>
    public ArchiveUpdateCallbackWithFileTime(
        IDictionary<string, CompressStreamEntry> streamDict,
        SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
    {
        Init(streamDict, compressor, updateData, directoryStructure);
    }

    /// <summary>
    /// Initializes a new instance of the ArchiveUpdateCallback class
    /// </summary>
    /// <param name="streamDict">Dictionary&lt;file stream, name of the archive entry&gt;</param>
    /// <param name="password">The archive password</param>
    /// <param name="compressor">The owner of the callback</param>
    /// <param name="updateData">The compression parameters.</param>
    /// <param name="directoryStructure">Preserve directory structure.</param>
    public ArchiveUpdateCallbackWithFileTime(
        IDictionary<string, CompressStreamEntry> streamDict, string password,
        SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
        : base(password)
    {
        Init(streamDict, compressor, updateData, directoryStructure);
    }

    private void CommonInit(SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
    {
        _compressor = compressor;
        _indexInArchive = updateData.FilesCount;
        _indexOffset = updateData.Mode != InternalCompressionMode.Append ? 0 : _indexInArchive;
        if (_compressor.ArchiveFormat == OutArchiveFormat.Zip)
        {
            _wrappersToDispose = new List<InStreamWrapper>();
        }
        _updateData = updateData;
        _directoryStructure = directoryStructure;
        DefaultItemName = "default";
    }

    private void Init(
        FileInfo[] files, int rootLength, SevenZipCompressor compressor,
        UpdateData updateData, bool directoryStructure)
    {
        _files = files;
        _rootLength = rootLength;
        if (files != null)
        {
            foreach (var fi in files)
            {
                if (fi.Exists)
                {
                    _bytesCount += fi.Length;
                    if ((fi.Attributes & FileAttributes.Directory) == 0)
                    {
                        _actualFilesCount++;
                    }
                }
            }
        }
        CommonInit(compressor, updateData, directoryStructure);
    }

    private void Init(
        Stream stream, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
    {
        _fileStream = new InStreamWrapper(stream, false);
        _fileStream.BytesRead += IntEventArgsHandler;
        _actualFilesCount = 1;

        try
        {
            _bytesCount = stream.Length;
        }
        catch (NotSupportedException)
        {
            _bytesCount = -1;
        }
        try
        {
            stream.Seek(0, SeekOrigin.Begin);
        }
        catch (NotSupportedException)
        {
            _bytesCount = -1;
        }
        CommonInit(compressor, updateData, directoryStructure);
    }

    private void Init(
        IDictionary<string, CompressStreamEntry> streamDict,
        SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure)
    {
        _streamEntries = new CompressStreamEntry[streamDict.Count];

        streamDict.Values.ToArray().CopyTo(_streamEntries, 0);

        _entries = new string[streamDict.Count];
        streamDict.Keys.CopyTo(_entries, 0);
        _actualFilesCount = streamDict.Count;
        foreach (var entry in _streamEntries)
        {
            if (entry.Stream != null)
            {
                _bytesCount += entry.Stream.Length;
            }
        }
        CommonInit(compressor, updateData, directoryStructure);
    }

    #endregion

    /// <summary>
    /// Gets or sets the dictionary size.
    /// </summary>
    public float DictionarySize
    {
        set
        {
            _memoryPressure = (int)(value * 1024 * 1024);
            GC.AddMemoryPressure(_memoryPressure);
        }
    }

    /// <summary>
    /// Raises events for the GetStream method.
    /// </summary>
    /// <param name="index">The current item index.</param>
    /// <returns>True if not cancelled; otherwise, false.</returns>
    private bool EventsForGetStream(uint index)
    {
        if (!FastCompression)
        {
            if (_fileStream != null)
            {
                _fileStream.BytesRead += IntEventArgsHandler;
            }
            _doneRate += 1.0f / _actualFilesCount;
            var fiea = new FileNameEventArgs(_files != null ? _files[index].Name : _entries[index],
                PercentDoneEventArgs.ProducePercentDone(_doneRate));
            OnFileCompression(fiea);

            if (fiea.Cancel)
            {
                Canceled = true;
                return false;
            }
        }
        return true;
    }

    #region Events

    /// <summary>
    /// Occurs when the next file is going to be packed.
    /// </summary>
    /// <remarks>Occurs when 7-zip engine requests for an input stream for the next file to pack it</remarks>
    public event EventHandler<FileNameEventArgs> FileCompressionStarted;

    /// <summary>
    /// Occurs when data are being compressed.
    /// </summary>
    public event EventHandler<ProgressEventArgs> Compressing;

    /// <summary>
    /// Occurs when the current file was compressed.
    /// </summary>
    public event EventHandler FileCompressionFinished;

    private void OnFileCompression(FileNameEventArgs e)
    {
        FileCompressionStarted?.Invoke(this, e);
    }

    private void OnCompressing(ProgressEventArgs e)
    {
        Compressing?.Invoke(this, e);
    }

    private void OnFileCompressionFinished(EventArgs e)
    {
        FileCompressionFinished?.Invoke(this, e);
    }

    #endregion

    #region IArchiveUpdateCallback Members

    public void SetTotal(ulong total) { }

    public void SetCompleted(ref ulong completeValue) { }

    public int GetUpdateItemInfo(uint index, ref int newData, ref int newProperties, ref uint indexInArchive)
    {
        switch (_updateData.Mode)
        {
            case InternalCompressionMode.Create:
                newData = 1;
                newProperties = 1;
                indexInArchive = uint.MaxValue;
                break;
            case InternalCompressionMode.Append:
                if (index < _indexInArchive)
                {
                    newData = 0;
                    newProperties = 0;
                    indexInArchive = index;
                }
                else
                {
                    newData = 1;
                    newProperties = 1;
                    indexInArchive = uint.MaxValue;
                }
                break;
            case InternalCompressionMode.Modify:
                newData = 0;
                newProperties = Convert.ToInt32(_updateData.FileNamesToModify.ContainsKey((int)index)
                                                && _updateData.FileNamesToModify[(int)index] != null);
                if (_updateData.FileNamesToModify.ContainsKey((int)index)
                    && _updateData.FileNamesToModify[(int)index] == null)
                {
                    indexInArchive = (uint)_updateData.ArchiveFileData.Count;

                    foreach (var pairModification in _updateData.FileNamesToModify)
                    {
                        if (pairModification.Key <= index && (pairModification.Value == null))
                        {
                            do
                            {
                                indexInArchive--;
                            } while (indexInArchive > 0
                                     && _updateData.FileNamesToModify.ContainsKey((int)indexInArchive)
                                     && _updateData.FileNamesToModify[(int)indexInArchive] == null);
                        }
                    }
                }
                else
                {
                    indexInArchive = index;
                }
                break;
        }

        return 0;
    }

    public int GetProperty(uint index, ItemPropId propID, ref PropVariant value)
    {
        index -= _indexOffset;
        try
        {
            switch (propID)
            {
                case ItemPropId.IsAnti:
                    value.VarType = VarEnum.VT_BOOL;
                    value.UInt64Value = 0;
                    break;
                case ItemPropId.Path:
                    #region Path

                    value.VarType = VarEnum.VT_BSTR;
                    string val = DefaultItemName;

                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        if (_files == null)
                        {
                            if (_entries != null)
                            {
                                val = _entries[index];
                            }
                        }
                        else
                        {
                            if (_directoryStructure)
                            {
                                if (_rootLength > 0)
                                {
                                    val = _files[index].FullName.Substring(_rootLength);
                                }
                                else
                                {
                                    val = _files[index].FullName[0] + _files[index].FullName.Substring(2);
                                }
                            }
                            else
                            {
                                val = _files[index].Name;
                            }
                        }
                    }
                    else
                    {
                        val = _updateData.FileNamesToModify[(int)index];
                    }
                    value.Value = Marshal.StringToBSTR(val);
                    #endregion
                    break;
                case ItemPropId.IsDirectory:
                    value.VarType = VarEnum.VT_BOOL;
                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        if (_files == null)
                        {
                            if (_streamEntries == null)
                            {
                                value.UInt64Value = 0;
                            }
                            else
                            {
                                value.UInt64Value = (ulong)(_streamEntries[index] == null ? 1 : 0);
                            }
                        }
                        else
                        {
                            value.UInt64Value = (byte)(_files[index].Attributes & FileAttributes.Directory);
                        }
                    }
                    else
                    {
                        value.UInt64Value = Convert.ToUInt64(_updateData.ArchiveFileData[(int)index].IsDirectory);
                    }
                    break;
                case ItemPropId.Size:
                    #region Size

                    value.VarType = VarEnum.VT_UI8;
                    ulong size;
                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        if (_files == null)
                        {
                            if (_streamEntries == null)
                            {
                                size = _bytesCount > 0 ? (ulong)_bytesCount : 0;
                            }
                            else
                            {
                                size = (ulong)(_streamEntries[index].Stream == null ? 0 : _streamEntries[index].Stream.Length);
                            }
                        }
                        else
                        {
                            size = (_files[index].Attributes & FileAttributes.Directory) == 0
                                ? (ulong)_files[index].Length
                                : 0;
                        }
                    }
                    else
                    {
                        size = _updateData.ArchiveFileData[(int)index].Size;
                    }
                    value.UInt64Value = size;

                    #endregion
                    break;
                case ItemPropId.Attributes:
                    value.VarType = VarEnum.VT_UI4;
                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        if (_files == null)
                        {
                            if (_streamEntries == null)
                            {
                                value.UInt32Value = (uint)FileAttributes.Normal;
                            }
                            else
                            {
                                value.UInt32Value = (uint)(_streamEntries[index].Stream == null ? FileAttributes.Directory : FileAttributes.Normal);
                            }
                        }
                        else
                        {
                            value.UInt32Value = (uint)_files[index].Attributes;
                        }
                    }
                    else
                    {
                        value.UInt32Value = _updateData.ArchiveFileData[(int)index].Attributes;
                    }
                    break;
                #region Times
                case ItemPropId.CreationTime:
                    value.VarType = VarEnum.VT_FILETIME;
                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        if (_streamEntries != null && _streamEntries[index].CreateTime.HasValue)
                            value.Int64Value = _streamEntries[index].CreateTime.Value.ToFileTime();
                        else if (_files != null)
                            value.Int64Value = _files[index].CreationTime.ToFileTime();
                        else
                            value.Int64Value = DateTime.Now.ToFileTime();
                    }
                    else
                    {
                        value.Int64Value = _updateData.ArchiveFileData[(int)index].CreationTime.ToFileTime();
                    }
                    break;
                case ItemPropId.LastAccessTime:
                    value.VarType = VarEnum.VT_FILETIME;
                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        if (_streamEntries != null && _streamEntries[index].LastAccess.HasValue)
                            value.Int64Value = _streamEntries[index].LastAccess.Value.ToFileTime();
                        else if (_files != null)
                            value.Int64Value = _files[index].LastAccessTime.ToFileTime();
                        else
                            value.Int64Value = DateTime.Now.ToFileTime();
                    }
                    else
                    {
                        value.Int64Value = _updateData.ArchiveFileData[(int)index].LastAccessTime.ToFileTime();
                    }
                    break;
                case ItemPropId.LastWriteTime:
                    value.VarType = VarEnum.VT_FILETIME;
                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        if (_streamEntries != null && _streamEntries[index].LastWrite.HasValue)
                            value.Int64Value = _streamEntries[index].LastWrite.Value.ToFileTime();
                        else if (_files != null)
                            value.Int64Value = _files[index].LastWriteTime.ToFileTime();
                        else
                            value.Int64Value = DateTime.Now.ToFileTime();
                    }
                    else
                    {
                        value.Int64Value = _updateData.ArchiveFileData[(int)index].LastWriteTime.ToFileTime();
                    }
                    break;
                #endregion
                case ItemPropId.Extension:
                    #region Extension

                    value.VarType = VarEnum.VT_BSTR;
                    if (_updateData.Mode != InternalCompressionMode.Modify)
                    {
                        try
                        {
                            val = _files != null
                                ? _files[index].Extension.Substring(1)
                                : _entries == null
                                    ? ""
                                    : Path.GetExtension(_entries[index]);
                            value.Value = Marshal.StringToBSTR(val);
                        }
                        catch (ArgumentException)
                        {
                            value.Value = Marshal.StringToBSTR("");
                        }
                    }
                    else
                    {
                        val = Path.GetExtension(_updateData.ArchiveFileData[(int)index].FileName);
                        value.Value = Marshal.StringToBSTR(val);
                    }

                    #endregion
                    break;
            }
        }
        catch (Exception e)
        {
            AddException(e);
        }
        return 0;
    }

    /// <summary>
    /// Gets the stream for 7-zip library.
    /// </summary>
    /// <param name="index">File index</param>
    /// <param name="inStream">Input file stream</param>
    /// <returns>Zero if Ok</returns>
    public int GetStream(uint index, out ISequentialInStream inStream)
    {
        index -= _indexOffset;

        if (_files != null)
        {
            _fileStream = null;

            try
            {
                if (File.Exists(_files[index].FullName))
                {
                    _fileStream = new InStreamWrapper(
                        new FileStream(_files[index].FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite),
                        true);
                }
            }
            catch (Exception e)
            {
                AddException(e);
                inStream = null;
                return -1;
            }

            inStream = _fileStream;

            if (!EventsForGetStream(index))
            {
                return -1;
            }
        }
        else
        {
            if (_streamEntries == null)
            {
                inStream = _fileStream;
            }
            else
            {
                _fileStream = new InStreamWrapper(_streamEntries[index].Stream, true);
                inStream = _fileStream;
                if (!EventsForGetStream(index))
                {
                    return -1;
                }
            }
        }

        return 0;
    }

    public long EnumProperties(IntPtr enumerator)
    {
        //Not implemented HRESULT
        return 0x80004001L;
    }

    public void SetOperationResult(OperationResult operationResult)
    {
        if (operationResult != OperationResult.Ok && ReportErrors)
        {
            switch (operationResult)
            {
                case OperationResult.CrcError:
                    AddException(new ExtractionFailedException("File is corrupted. Crc check has failed."));
                    break;
                case OperationResult.DataError:
                    AddException(new ExtractionFailedException("File is corrupted. Data error has occurred."));
                    break;
                case OperationResult.UnsupportedMethod:
                    AddException(new ExtractionFailedException("Unsupported method error has occurred."));
                    break;
                case OperationResult.Unavailable:
                    AddException(new ExtractionFailedException("File is unavailable."));
                    break;
                case OperationResult.UnexpectedEnd:
                    AddException(new ExtractionFailedException("Unexpected end of file."));
                    break;
                case OperationResult.DataAfterEnd:
                    AddException(new ExtractionFailedException("Data after end of archive."));
                    break;
                case OperationResult.IsNotArc:
                    AddException(new ExtractionFailedException("File is not archive."));
                    break;
                case OperationResult.HeadersError:
                    AddException(new ExtractionFailedException("Archive headers error."));
                    break;
                case OperationResult.WrongPassword:
                    AddException(new ExtractionFailedException("Wrong password."));
                    break;
                default:
                    AddException(new ExtractionFailedException($"Unexpected operation result: {operationResult}"));
                    break;
            }
        }
        if (_fileStream != null)
        {
            _fileStream.BytesRead -= IntEventArgsHandler;

            //Specific Zip implementation - can not Dispose files for Zip.
            if (_compressor.ArchiveFormat != OutArchiveFormat.Zip)
            {
                try
                {
                    _fileStream.Dispose();
                }
                catch (ObjectDisposedException) { }
            }
            else
            {
                _wrappersToDispose.Add(_fileStream);
            }

            _fileStream = null;
        }

        OnFileCompressionFinished(EventArgs.Empty);
    }

    #endregion

    #region ICryptoGetTextPassword2 Members

    public int CryptoGetTextPassword2(ref int passwordIsDefined, out string password)
    {
        passwordIsDefined = string.IsNullOrEmpty(Password) ? 0 : 1;
        password = Password;

        return 0;
    }

    #endregion

    #region IDisposable Members

    public void Dispose()
    {
        GC.RemoveMemoryPressure(_memoryPressure);

        if (_fileStream != null)
        {
            try
            {
                _fileStream.Dispose();
            }
            catch (ObjectDisposedException) { }
        }

        if (_wrappersToDispose == null)
        {
            return;
        }

        foreach (var wrapper in _wrappersToDispose)
        {
            try
            {
                wrapper.Dispose();
            }
            catch (ObjectDisposedException) { }
        }
    }

    #endregion

    private void IntEventArgsHandler(object sender, IntEventArgs e)
    {
        var lockObject = ((object)_files ?? _streamEntries) ?? _fileStream;

        lock (lockObject)
        {
            var pOld = (byte)(_bytesWrittenOld * 100 / _bytesCount);
            _bytesWritten += e.Value;
            byte pNow;

            if (_bytesCount < _bytesWritten) //Holy shit, this check for ZIP is golden
            {
                pNow = 100;
            }
            else
            {
                pNow = (byte)((_bytesWritten * 100) / _bytesCount);
            }

            if (pNow > pOld)
            {
                _bytesWrittenOld = _bytesWritten;
                OnCompressing(new ProgressEventArgs(pNow, (byte)(pNow - pOld)));
            }
        }
    }
}
#endif