/*
 i-net software provides programming examples for illustration only, without warranty
 either expressed or implied, including, but not limited to, the implied warranties
 of merchantability and/or fitness for a particular purpose. This programming example
 assumes that you are familiar with the programming language being demonstrated and
 the tools used to create and debug procedures. i-net software support professionals
 can help explain the functionality of a particular procedure, but they will not modify
 these examples to provide added functionality or construct procedures to meet your
 specific needs.
  
 � i-net software 1998-2013

*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Inet.Viewer.Resources;
using System.Diagnostics;

namespace Inet.Viewer.Data
{
    /// <summary>
    /// Progress class for the export. This class sends the export request to the Engine, receives the chunks and saves it into one or more files.
    /// Also, it displays the status of the export in the progress and status bar.
    /// </summary>
    /// <seealso cref= "Progress"/>
    public class ExportProgress : Progress
    {
        private const string PropFile = "file";
        private const char SlashChar = '/';
        private const char DotChar = '.';

        /// <summary>
        /// The original report data instance. May not be used directly, instead <seealso cref="ReportDataCopy"/> should be used.
        /// </summary>
        private readonly IRenderData originalData;

        // Connection to the exported report
        private IRenderData copiedData;

        // Paramters that have been set in the Export Dialog
        private Dictionary<string, string> exportParameter;

        // Buffer to read the chunks
        private byte[] chunkBuffer;
        private int chunkBufferIndex;
        private int chunkBufferSize;
        private List<string> fileNames;

        /// <summary>
        /// Creates a new export progress and set it to indeterminate. This method use the default value for all not specified properties.
        /// For a list of properties see interface <seealso cref="IRenderData"/> or the
        /// i-net Clear Reports documentation at: https://www.inetsoftware.de/documentation/clear-reports/online-help/features/report-url-parameters.
        /// </summary>
        /// <param name="parent"> IReportView, used for showError </param>
        /// <param name="data"> IReportData for the report that will be exported </param>
        /// <param name="parameters"> Properties specified in the export dialog or with API </param>
        /// <seealso cref= "IReportView.Export(ExportFormat, string)"/>
        /// <seealso cref= "IReportView.Export(Dictionary{string, string})"/>
        public ExportProgress(Action<Exception> parent, IRenderData data, Dictionary<string, string> parameters)
            : base(parent, Progress.ProgressType.Export)
        {
            // First set Progressbar with unknown max-value (Indeterminate=true)
            // When amount of chunks is available switch Progressbar to know max value (Indeterminate=false)
            this.Indeterminate = true;

            // we used to copy here, but this can take long in situations like Ad-Hoc where we are working with a serialized remote ReportData
            // so instead we now copy lazily
            this.originalData = data;
            this.exportParameter = parameters;
            this.ProgressMode = Data.ProgressMode.Continuous;
        }

        /// <summary>
        /// <inheritDoc/>
        /// </summary>
        public override string Name
        {
            get
            {
                return strings.ProgressExport;
            }
        }

        /// <summary>
        /// Stops this export progress and stops the rendering process (export) for this report. </summary>  
        public override void Cancel()
        {
            ReportDataCopy.Stop();
            Status = Progress.ProgressStatus.Canceled;
        }

        /// <summary>
        /// FOR INTERNAL USE ONLY
        /// 
        /// Gets ExportChunkCount and all Chunks from server and saves them accoring to the format and depending on the settings of the format  
        /// </summary>
        protected override void Run()
        {
            OnProgressChanged();
            fileNames = new List<string>();
            FileInfo exportFile; // file that will be created for the xport.  For HTML (if layout was set) and for SVG this is only the first file
            string fileName; // path for the to be created (for all formats). For HTML and SVG includingt the sub-directories for more files            
            try
            {
                IRenderData data = ReportDataCopy;

                string format = exportParameter[URLRenderData.ParameterExportFmt];

                // get amount of chunks. For this the report might need to be rendered on the server it is not in its cache.
                int exportCount = data.GetExportChunkCount(exportParameter);
                if (Status == Progress.ProgressStatus.Canceled)
                {
                    return;
                }

                // change status of the ProgressBar, as now the max value is available
                Indeterminate = false;

                // if length is unknown, remove more chunks until the stream is finished
                if (exportCount == 0)
                {
                    this.ProgressMode = Data.ProgressMode.Continuous;
                    exportCount = int.MaxValue;
                }
                else
                {
                    this.ProgressMode = Data.ProgressMode.Steps;
                }

                // set total amount of steps for ProgressBar
                TotalProgress = exportCount;

                fileName = exportParameter[PropFile];
                if (format.StartsWith("htm") || format.StartsWith("svg"))
                {
                    // HTML / SVG Export
                    exportFile = ExportMultiFile(format, fileName);
                }
                else if (format.Equals(ReportInfo.FormatJPG) || format.Equals(ReportInfo.FormatGIF) || format.Equals(ReportInfo.FormatPNG) || format.Equals(ReportInfo.FormatBMP))
                {
                    exportFile = ExportFilePerPage(fileName, exportCount);
                }
                else
                {
                    // for all other formats (e.g. pdf, rtf, ...)
                    exportFile = ExportOneFile(fileName, data, exportCount);
                }

                Status = Progress.ProgressStatus.Completed;

                // Start application for choosen export format
                if (exportParameter.ContainsKey("exportInApplication") && exportParameter["exportInApplication"] != null && exportParameter["exportInApplication"].Equals("true"))
                {
                    if (exportFile != null)
                    {
                        ExportInApplication(exportFile);
                    }
                }
            }
            catch (Exception t)
            {
                ViewerUtils.PrintStackTrace(t);
                // Don't show error message if was canceled (e.g. after pressing the stop button)
                if (Status == Progress.ProgressStatus.Canceled)
                {
                    ViewerUtils.Log("Export: Export canceled by user");
                }
                else
                {
                    ShowError(t);
                    Status = Progress.ProgressStatus.Error;
                    this.Cancel();
                }
            }
        }

        /// <summary>
        /// Get the generated file names on the export. </summary>
        /// <returns> the file names after the run, else null</returns>
        public virtual List<string> FileNames
        {
            get
            {
                return fileNames;
            }
        }

        /// <summary>
        /// Opens the exported file in a suitable application
        /// </summary>
        /// <param name="file"> the file that should be opened </param>
        private void ExportInApplication(FileInfo file)
        {
            try
            {
                System.Diagnostics.Process.Start(file.FullName);
            }
            catch (Exception)
            {
                try
                {
                    Process.Start("rundll32.exe", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "shell32.dll") + ",OpenAs_RunDLL " + file.FullName);
                }
                catch (Exception e)
                {
                    ShowError(new ViewerException(strings.Export_InApplicationFailed, e));
                }
            }
        }

        /// <summary>
        /// Read the next 4 bytes from idx on and return it as integer value </summary>
        /// <param name="buffer">byte array to read from</param>
        /// <param name="idx">index of the position the specified byte array</param>
        /// <returns>the read integer value</returns>
        private int ReadInt(byte[] buffer, int idx)
        {
            return (buffer[idx + 0] & 0xFF) + ((buffer[idx + 1] & 0xFF) << 8) + ((buffer[idx + 2] & 0xFF) << 16) + (buffer[idx + 3] << 24);
        }

        /// <summary>
        /// Needed for ExportMultiFile to check if there are enough bytes left iin the local buffer.
        /// If not get the next chunk</summary>
        /// <param name="needed"> Amount of needed bytes </param>
        /// <param name="throwException"> When set to true a ViewerException is thrown, if not enough data are available</param>
        /// <exception cref="ViewerException"> Wenn alle Daten gefetcht und nicht gen�gend Daten vorhanden </exception>
        /// <returns> true, if there are more data </returns>
        private bool EnsureReadBufferSize(int needed, bool throwException)
        {
            int oldDataSize = chunkBufferSize - chunkBufferIndex;
            if (oldDataSize < needed)
            {
                byte[] chunk = ReportDataCopy.NextExportChunk();
                this.ProgressCount = this.ProgressCount + 1;
                if (chunk == null)
                {
                    if (throwException)
                    {
                        throw new ViewerException(strings.ErrorEexport_UnexpectedEnd);
                    }
                    else
                    {
                        return false;
                    }
                }
                if (chunkBuffer == null)
                {
                    // first call
                    chunkBuffer = chunk;
                    chunkBufferIndex = 0;
                    chunkBufferSize = chunk.Length;
                }
                else
                {
                    if (chunkBuffer.Length - oldDataSize < chunk.Length)
                    {
                        // increase buffer
                        byte[] newPuffer = new byte[oldDataSize + chunk.Length];
                        Array.Copy(chunkBuffer, chunkBufferIndex, newPuffer, 0, oldDataSize);
                        chunkBuffer = newPuffer;
                    }
                    else
                    {
                        Array.Copy(chunkBuffer, chunkBufferIndex, chunkBuffer, 0, oldDataSize);
                    }
                    chunkBufferIndex = 0;
                    chunkBufferSize = oldDataSize;
                    Array.Copy(chunk, 0, chunkBuffer, chunkBufferSize, chunk.Length);
                    chunkBufferSize += chunk.Length;
                }
            }
            return true;
        }

        /// <summary>
        /// Export formats that contain only one file (e.g. pdf, rtf, ps)
        /// </summary>
        /// <param name="fileName">the exported file name</param>
        /// <param name="data">the report data</param>
        /// <param name="exportCount">the number of chunks</param>
        /// <returns></returns>
        private FileInfo ExportOneFile(string fileName, IRenderData data, int exportCount)
        {
            FileInfo exportFile;
            exportFile = new FileInfo(fileName);
            fileNames.Add(exportFile.FullName);
            FileStream fos = new FileStream(exportFile.FullName, FileMode.Create);
            for (int i = 1; i <= exportCount; i++)
            {
                byte[] pageData = data.NextExportChunk();
                if (pageData != null)
                {
                    fos.Write(pageData, 0, pageData.Length);
                }
                else
                {
                    break;
                }
                // number of the step that was processed for the ProgressBar
                this.ProgressCount = i;
            }
            fos.Close();
            return exportFile;
        }

        /// <summary>
        /// Exports formats that contain more than one file, e.g. export in HTML and SVG
        /// </summary>
        /// <param name="format"> the export format </param>
        /// <param name="outputDir"> path for the to exported files. For HTML und SVG it should also contain the name of the subdirectory for the files
        /// </param>
        /// <returns> First file that was created for HTML or SVG export</returns>
        /// <exception cref="IOException"> If an error with the creation of the file has occurred </exception>
        private FileInfo ExportMultiFile(string format, string outputDir)
        {
            chunkBufferIndex = 0;
            FileInfo firstFile = null;
            FileStream fos;
            string subDirName = string.Empty;
            StringBuilder filePath;
            bool directoryCreated = false;
            bool firstFileCreated = false;

            while (chunkBufferSize > chunkBufferIndex || TotalProgress > ProgressCount)
            {
                // Read filename length
                EnsureReadBufferSize(4, true);
                int length = ReadInt(chunkBuffer, chunkBufferIndex);
                if (length == -1)
                {
                    // eof
                    break;
                }
                if (length > 512)
                {
                    // A problem occured if filename longer than 512
                    string text = System.Text.Encoding.UTF8.GetString(chunkBuffer, 0, chunkBufferSize);
                    throw new ViewerException(0, strings.ErrorMessage_Export_WrongData, format, null, null, null, 0, text);
                }
                chunkBufferIndex += 4; // set index to the beginning of the block, that contains the filename

                // read filename
                EnsureReadBufferSize(length, true);
                // file name was encoded with UTF8, it need to be decoded here with UTF8
                string filename = System.Text.Encoding.UTF8.GetString(chunkBuffer, chunkBufferIndex, length);

                if (!firstFileCreated)
                {
                    string layout = null;
                    exportParameter.TryGetValue("layout", out layout);
                    if (layout == null)
                    {
                        outputDir = outputDir.Substring(0, outputDir.LastIndexOf(SlashChar) + 1) + filename;
                    }

                    DirectoryInfo dir = new DirectoryInfo(outputDir);
                    dir.Parent.Create();
                    firstFile = new FileInfo(outputDir);
                    fileNames.Add(firstFile.FullName);
                    fos = new FileStream(firstFile.FullName, FileMode.Create);
                    // create directory with the name of the first html file (without extensions)                    
                    subDirName = filename.Substring(0, filename.LastIndexOf(DotChar));
                    firstFileCreated = true;
                }
                else
                {
                    // all other files in subdirectory
                    filePath = new StringBuilder(outputDir.Substring(0, outputDir.LastIndexOf(SlashChar) + 1));
                    filePath.Append(subDirName);

                    if (!directoryCreated)
                    {
                        Directory.CreateDirectory(filePath.ToString());
                        directoryCreated = true;
                    }

                    filePath.Append(SlashChar);
                    filePath.Append(filename);
                    string strFilePath = filePath.ToString();
                    fileNames.Add(strFilePath);
                    fos = new FileStream(strFilePath, FileMode.Create);
                }

                chunkBufferIndex += length; // set index to the beginning of the block which contains the length of the file content

                // read file length
                EnsureReadBufferSize(4, true);
                length = ReadInt(chunkBuffer, chunkBufferIndex);

                chunkBufferIndex += 4; // set index to the beginning of the block which contains the file content

                // Write file
                while (length > 0)
                {
                    EnsureReadBufferSize(1, true); // There must be at least one byte in the buffer to avoid a endless loop                    
                    int available = Math.Min(chunkBufferSize - chunkBufferIndex, length);
                    fos.Write(chunkBuffer, chunkBufferIndex, available);
                    chunkBufferIndex += available;
                    length -= available;
                }
                fos.Close();
            }
            return firstFile;
        }

        /// <summary>
        /// Export for formats where for each page a file will be created like the Image Renderer. </summary>
        /// <param name="fileName">name of the output file to export</param>
        /// <param name="exportCount"> amount of chunks from IReportData </param>
        /// <returns>first file that was created for html or svg export </returns>
        /// <exception cref="IOException"> If an error with the creation of the file has occurred </exception>
        /// <seealso cref="ExportMultiFile"/>
        private FileInfo ExportFilePerPage(string fileName, int exportCount)
        {
            IRenderData data = ReportDataCopy;
            byte[] pageData = data.NextExportChunk();

            if (pageData != null && pageData.Length > 2 && pageData[0] == 'P' && pageData[1] == 'K' && !fileName.ToLower().EndsWith(".zip"))
            {
                string zipFileName = fileName + ".zip";
                if (File.Exists(zipFileName))
                {
                    int c = 1;
                    do
                    {
                        zipFileName = fileName + "(" + (c++) + ").zip";
                    }
                    while (File.Exists(zipFileName));
                }
                fileName = zipFileName;
            }

            FileInfo exportFile = new FileInfo(fileName);
            fileNames.Add(exportFile.FullName);

            FileStream output = new FileStream(exportFile.FullName, FileMode.Create);

            for (int i = 1; i <= exportCount; i++)
            {
                if (pageData == null)
                {
                    break;
                }
                output.Write(pageData, 0, pageData.Length);
                this.ProgressCount++;
                if (i >= exportCount)
                {
                    break;
                }
                pageData = data.NextExportChunk();
            }
            output.Close();

            return exportFile;
        }

        /// <summary>
        /// Gets the ReportData copy (copy so we can do more than one export at the same time ).
        /// synchronized so we never create more than the one copy for this instance </summary>
        /// <returns> the copy of the IReportData we are using </returns>    
        private IRenderData ReportDataCopy
        {
            get
            {
                if (copiedData == null)
                {
                    copiedData = (IRenderData)originalData.Clone();
                }
                return copiedData;
            }
        }

        /// <summary>
        /// Converts the ExportFormat enm into strings for the paramter url.
        /// </summary>
        /// <param name="format">the export format</param>
        /// <returns>the export format as string</returns>
        public static string ExportFormatToString(ExportFormat format)
        {
            switch (format)
            {
                case ExportFormat.BMP:
                    return "bmp";
                case ExportFormat.CSV:
                    return "csv";
                case ExportFormat.DATA:
                    return "csv";
                case ExportFormat.GIF:
                    return "gif";
                case ExportFormat.HTML:
                    return "htm";
                case ExportFormat.JPG:
                    return "jpg";
                case ExportFormat.ODS:
                    return "ods";
                case ExportFormat.PDF:
                    return "pdf";
                case ExportFormat.PNG:
                    return "png";
                case ExportFormat.PS:
                    return "ps";
                case ExportFormat.PS2:
                    return "ps2";
                case ExportFormat.PS3:
                    return "ps3";
                case ExportFormat.RTF:
                    return "rtf";
                case ExportFormat.SVG:
                    return "svg";
                case ExportFormat.TXT:
                    return "txt";
                case ExportFormat.XLS:
                    return "xls";
                case ExportFormat.XML:
                    return "xml";
                case ExportFormat.None:
                    return string.Empty;
                default:
                    return null;
            }
        }
    }
}