// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
// 
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Duplicati.Library.Interface;

namespace Duplicati.Library.Main.Volumes
{
    public class FilesetVolumeWriter : VolumeWriterBase
    {
        private readonly Library.Utility.TempFile m_tempFile;
        private readonly Stream m_tempStream;
        private StreamWriter m_streamwriter;
        private readonly JsonWriter m_writer;
        private long m_filecount;
        private long m_foldercount;

        public override RemoteVolumeType FileType { get { return RemoteVolumeType.Files; } }

        public FilesetVolumeWriter(Options options, DateTime timestamp)
            : base(options, timestamp)
        {
            m_tempFile = new Library.Utility.TempFile();
            m_tempStream = File.Open(m_tempFile, FileMode.Create, FileAccess.ReadWrite);
            m_streamwriter = new StreamWriter(m_tempStream, ENCODING);
            m_writer = new JsonTextWriter(m_streamwriter);
            m_writer.WriteStartArray();
        }

        private void WriteMetaProperties(string metahash, long metasize, string metablockhash, IEnumerable<string> metablocklisthashes)
        {
            m_writer.WritePropertyName("metahash");
            m_writer.WriteValue(metahash);
            m_writer.WritePropertyName("metasize");
            m_writer.WriteValue(metasize);

            if (metablocklisthashes != null)
            {
                // Slightly awkward, but we avoid writing if there are no entries.
                using (var en = metablocklisthashes.GetEnumerator())
                {
                    if (en.MoveNext() && !string.IsNullOrEmpty(en.Current))
                    {
                        m_writer.WritePropertyName("metablocklists");
                        m_writer.WriteStartArray();
                        m_writer.WriteValue(en.Current);
                        while (en.MoveNext())
                            m_writer.WriteValue(en.Current);
                        m_writer.WriteEndArray();
                    }
                }
            }
            else if (!string.IsNullOrWhiteSpace(metablockhash))
            {
                m_writer.WritePropertyName("metablockhash");
                m_writer.WriteValue(metablockhash);
            }
        }

        public void AddFile(string name, string filehash, long size, DateTime lastmodified, string metahash, long metasize, string metablockhash, string blockhash, long blocksize, IEnumerable<string> blocklisthashes, IEnumerable<string> metablocklisthashes)
        {
            AddFileEntry(FilelistEntryType.File, name, filehash, size, lastmodified, metahash, metasize, metablockhash, blockhash, blocksize, blocklisthashes, metablocklisthashes);
        }

        public void AddAlternateStream(string name, string filehash, long size, DateTime lastmodified, string metahash, string metablockhash, long metasize, string blockhash, long blocksize, IEnumerable<string> blocklisthashes, IEnumerable<string> metablocklisthashes)
        {
            AddFileEntry(FilelistEntryType.AlternateStream, name, filehash, size, lastmodified, metahash, metasize, metablockhash, blockhash, blocksize, blocklisthashes, metablocklisthashes);
        }

        private void AddFileEntry(FilelistEntryType type, string name, string filehash, long size, DateTime lastmodified, string metahash, long metasize, string metablockhash, string blockhash, long blocksize, IEnumerable<string> blocklisthashes, IEnumerable<string> metablocklisthashes)
        {
            m_filecount++;
            m_writer.WriteStartObject();
            m_writer.WritePropertyName("type");
            m_writer.WriteValue(type.ToString());
            m_writer.WritePropertyName("path");
            m_writer.WriteValue(name);
            m_writer.WritePropertyName("hash");
            m_writer.WriteValue(filehash);
            m_writer.WritePropertyName("size");
            m_writer.WriteValue(size);
            m_writer.WritePropertyName("time");
            m_writer.WriteValue(Library.Utility.Utility.SerializeDateTime(lastmodified));
            if (metahash != null)
                WriteMetaProperties(metahash, metasize, metablockhash, metablocklisthashes);

            if (blocklisthashes != null)
            {
                //Slightly awkward, but we avoid writing if there are no entries 
                using (var en = blocklisthashes.GetEnumerator())
                {
                    if (en.MoveNext() && !string.IsNullOrEmpty(en.Current))
                    {
                        m_writer.WritePropertyName("blocklists");
                        m_writer.WriteStartArray();
                        m_writer.WriteValue(en.Current);
                        while (en.MoveNext())
                            m_writer.WriteValue(en.Current);
                        m_writer.WriteEndArray();
                    }
                }
            }
            else if (!string.IsNullOrWhiteSpace(blockhash))
            {
                m_writer.WritePropertyName("blockhash");
                m_writer.WriteValue(blockhash);
                m_writer.WritePropertyName("blocksize");
                m_writer.WriteValue(blocksize);
            }

            m_writer.WriteEndObject();
        }

        public void AddDirectory(string name, string metahash, long metasize, string metablockhash, IEnumerable<string> metablocklisthashes)
        {
            AddMetaEntry(FilelistEntryType.Folder, name, metahash, metasize, metablockhash, metablocklisthashes);
        }

        public void AddMetaEntry(FilelistEntryType type, string name, string metahash, long metasize, string metablockhash, IEnumerable<string> metablocklisthashes)
        {
            m_foldercount++;
            m_writer.WriteStartObject();
            m_writer.WritePropertyName("type");
            m_writer.WriteValue(type.ToString());
            m_writer.WritePropertyName("path");
            m_writer.WriteValue(name);
            if (metahash != null)
                WriteMetaProperties(metahash, metasize, metablockhash, metablocklisthashes);
            
            m_writer.WriteEndObject();
        }

        public override void Close()
        {
            if (m_streamwriter != null)
            {
                this.AddFilelistFile();
            }

            if (m_tempStream != null)
            {
                m_tempStream.Dispose();
            }

            if (m_tempFile != null)
            {
                m_tempFile.Dispose();
            }

            base.Close();
        }

        private void AddFilelistFile()
        {
            m_writer.WriteEndArray();
            m_writer.Flush();
            m_streamwriter.Flush();

            try
            {
                using (Stream sr = m_compression.CreateFile(FILELIST, CompressionHint.Compressible, DateTime.UtcNow))
                {
                    m_tempStream.Seek(0, SeekOrigin.Begin);
                    m_tempStream.CopyTo(sr);
                    sr.Flush();
                }
            }
            finally
            {
                m_writer.Close();
                m_streamwriter.Dispose();
                m_streamwriter = null;
            }
        }

        public void AddControlFile(string localfile, CompressionHint hint, string filename = null)
        {
            filename = filename ?? System.IO.Path.GetFileName(localfile);
            using (var t = m_compression.CreateFile(CONTROL_FILES_FOLDER + filename, hint, DateTime.UtcNow))
            using (var s = System.IO.File.OpenRead(localfile))
                Library.Utility.Utility.CopyStream(s, t);
        }

        public override void Dispose()
        {
            this.Close();
            base.Dispose();
        }

        public long FileCount { get { return m_filecount; } }
        public long FolderCount { get { return m_foldercount; } }

        public void AddSymlink(string name, string metahash, long metasize, string metablockhash, IEnumerable<string> metablocklisthashes)
        {
            AddMetaEntry(FilelistEntryType.Symlink, name, metahash, metasize, metablockhash, metablocklisthashes);
        }

        public void CreateFilesetFile(bool isFullBackup)
        {
            using (var sr = new StreamWriter(this.m_compression.CreateFile(FILESET_FILENAME, CompressionHint.Compressible, DateTime.UtcNow), ENCODING))
            {
                sr.Write(FilesetData.GetFilesetInstance(isFullBackup));
            }
        }
    }
}
