// Created with MonoDevelop
//
//    Copyright (C) 2017 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.IO;
using System.Text;

using Hiperscan.Unix;

using ICSharpCode.SharpZipLib.Zip;


namespace Hiperscan.Common.IO
{

    public delegate bool ZipCondition(string path);    
    public delegate void ParentDirectoryChangedHandler(string directory);

    public class Zip : System.IDisposable
    {
        public event EventHandler FileAdded = null;

        private readonly ZipOutputStream zip_output_stream;
        private readonly string zip_filename;
        private readonly FileStream zip_fstream;

        public ZipCondition AddToArchiveCondition { get; set; }


        public Zip(string zip_fname, bool overwrite)
        {
            if (Path.GetExtension(zip_fname).ToLower() != ".zip")
                zip_fname = Path.ChangeExtension(zip_fname, ".zip");

            this.zip_filename = zip_fname;

            if (FileIO.ExistsFile(this.zip_filename))
            {
                if (overwrite == false)
                    throw new Exception(Catalog.GetString("Archive file already exists."));

                FileIO.DeleteFile(this.zip_filename);
            }

            this.zip_fstream = FileIO.CreateFile(zip_fname);
            this.zip_output_stream = new ZipOutputStream(this.zip_fstream)
            {
                IsStreamOwner = true
            };

            this.CompressionLevel = 3;
        }

        public Zip(string zip_fname) : this(zip_fname, false)
        {
        }

        ~Zip()
        {
            this.Finish();
        }

        public void Dispose()
        {
            this.Finish();
        }

        public void Add(string path)
        {
            this.Add(path, Path.GetFileName(path));
        }

        public void Add(List<string> paths)
        {
            foreach (string path in paths)
            {
                this.Add(path);
            }
        }

        public void Add(FileInfo file)
        {
            this.Add(file.FullName);
        }

        public void Add(List<FileInfo> files)
        {
            foreach (FileInfo file in files)
            {
                this.Add(file);
            }
        }

        public void Add(string path, string target)
        {
            if (FileIO.ExistsDirectory(path))
                this.AddDirectoryContent(path, target);
            else if (FileIO.ExistsFile(path))
                this.AddFile(path, target);
            else
                throw new FileNotFoundException(Catalog.GetString("Cannot find file or directory:") + " " + path);
        }

        public void AddDirectoryContent(string dir_path)
        {
            this.AddDirectoryContent(dir_path, string.Empty);
        }

        public void AddDirectoryContent(string dir_path, string target)
        {
            if (false == FileIO.ExistsDirectory(dir_path))
                throw new FileNotFoundException(Catalog.GetString("Cannot find directory:") + " " + dir_path);

            foreach (string path in FileIO.GetDirectories(dir_path, "*", SearchOption.TopDirectoryOnly))
            {
                if (string.IsNullOrEmpty(target))
                    this.Add(path, Path.GetFileName(path));
                else
                    this.Add(path, Path.Combine(target, Path.GetFileName(path)));
            }

            foreach (string path in FileIO.GetFiles(dir_path, "*", SearchOption.TopDirectoryOnly))
            {
                if (string.IsNullOrEmpty(target))
                    this.Add(path, Path.GetFileName(path));
                else
                    this.Add(path, Path.Combine(target, Path.GetFileName(path)));
            }
        }

        private void AddFile(string path, string target)
        {
            if (this.zip_fstream == null)
                throw new Exception(Catalog.GetString("Cannot add file to closed archive file."));

            if (this.AddToArchiveCondition != null)
            {
                if (this.AddToArchiveCondition(path) == false)
                    return;
            }

            FileInfo file = new FileInfo(path);
            if(file.Attributes.HasFlag(FileAttributes.ReparsePoint))
            {
                path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString());
                FileIO.CopyFile(file.FullName, path, true);
                this.TmpFiles.Add(path);
            }

            path = path.Replace("\\", "/");
            target = target.Replace("\\", "/");

            ZipEntry zip_entry = new ZipEntry(FileIOHelper.ConvertToValidPathName(target));

            file = new FileInfo(path);
            zip_entry.DateTime = file.LastWriteTime;
            zip_entry.Size = file.Length;

            this.zip_output_stream.PutNextEntry(zip_entry);
            using (FileStream stream = File.OpenRead(path)) 
            {
                stream.CopyTo(this.zip_output_stream);
            }

            this.zip_output_stream.CloseEntry();

            this.FileAdded?.Invoke(this, EventArgs.Empty);
        }

        public void Finish()
        {
            lock (this)
            { 
                if (this.IsDisposed)
                    return;

                this.IsDisposed = true;
                this.zip_output_stream.Finish();
                this.zip_output_stream.Flush();
                this.zip_fstream.Close();

                foreach (string file in TmpFiles)
                    FileIO.DeleteFile(file);
            }
        }

        public static void Decompress(string zip_fname, string out_path, string dirname)
        {
            string path = Path.Combine(out_path, dirname);
            FileIO.RecreateDirectory(path);
            Zip.Decompress(zip_fname, path);
        }

        public static void Decompress(string zip_fname, string out_path)
        {
            FileStream stream = File.OpenRead(zip_fname);
            
            ZipFile zip_file = new ZipFile(stream);

            foreach (ZipEntry zip_entry in zip_file) 
            {
                string entry_name = zip_entry.Name;
                if (zip_entry.IsFile == true)
                {
                    string fname = Path.Combine(out_path, entry_name);
                    FileInfo file = new FileInfo(fname);

                    if (file.Directory.Exists == false)
                        file.Directory.Create();

                    Stream zip_stream = zip_file.GetInputStream(zip_entry);

                    using (FileStream file_stream = FileIO.CreateFile(fname))
                    {
                        zip_stream.CopyTo(file_stream);
                    }
                }
            }

            zip_file.Close();
        }

        private static string ConvertZipEntryEncoding(string zip_entry_name)
        {
            byte[] utf8_encoding_bytes = Encoding.UTF8.GetBytes(zip_entry_name);
                    
              Encoding default_encoding = Encoding.Default;
             byte[] default_bytes = Encoding.Convert(Encoding.UTF8, default_encoding, utf8_encoding_bytes);                    
              char[] default_chars = new char[default_encoding.GetCharCount(default_bytes, 0, default_bytes.Length)];
                    
              default_encoding.GetChars(default_bytes, 0, default_bytes.Length, default_chars, 0);

             return new string(default_chars);
        }

        private bool IsDisposed { get; set; } = false;

        public int CompressionLevel
        {
            get
            {
                return this.zip_output_stream.GetLevel();
            }
            set
            {
                if (this.zip_output_stream != null)
                    this.zip_output_stream.SetLevel(value);
            }
        }

        public HashSet<string> TmpFiles { get; set; } = new HashSet<string>();
    }
}

