#region header
// ========================================================================
// Copyright (c) 2018 - Julien Caillon (julien.caillon@gmail.com)
// This file (ImageLoadHandler.cs) is part of YamuiFramework.
// 
// YamuiFramework is a 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.
// 
// YamuiFramework 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 YamuiFramework. If not, see <http://www.gnu.org/licenses/>.
// ========================================================================
#endregion
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using YamuiFramework.HtmlRenderer.Core.Adapters;
using YamuiFramework.HtmlRenderer.Core.Adapters.Entities;
using YamuiFramework.HtmlRenderer.Core.Core.Entities;
using YamuiFramework.HtmlRenderer.Core.Core.Utils;

namespace YamuiFramework.HtmlRenderer.Core.Core.Handlers {
    /// <summary>
    /// Handler for all loading image logic.<br/>
    /// <p>
    /// Loading by <see cref="HtmlImageLoadEventArgs"/>.<br/>
    /// Loading by file path.<br/>
    /// Loading by URI.<br/>
    /// </p>
    /// </summary>
    /// <remarks>
    /// <para>
    /// Supports sync and async image loading.
    /// </para>
    /// <para>
    /// If the image object is created by the handler on calling dispose of the handler the image will be released, this
    /// makes release of unused images faster as they can be large.<br/>
    /// Disposing image load handler will also cancel download of image from the web.
    /// </para>
    /// </remarks>
    internal sealed class ImageLoadHandler : IDisposable {
        #region Fields and Consts

        /// <summary>
        /// the container of the html to handle load image for
        /// </summary>
        private readonly HtmlContainerInt _htmlContainer;

        /// <summary>
        /// callback raised when image load process is complete with image or without
        /// </summary>
        private readonly ActionInt<RImage, RRect, bool> _loadCompleteCallback;

        /// <summary>
        /// the web client used to download image from URL (to cancel on dispose)
        /// </summary>
        private WebClient _client;

        /// <summary>
        /// Must be open as long as the image is in use
        /// </summary>
        private FileStream _imageFileStream;

        /// <summary>
        /// the image instance of the loaded image
        /// </summary>
        private RImage _image;

        /// <summary>
        /// the image rectangle restriction as returned from image load event
        /// </summary>
        private RRect _imageRectangle;

        /// <summary>
        /// to know if image load event callback was sync or async raised
        /// </summary>
        private bool _asyncCallback;

        /// <summary>
        /// flag to indicate if to release the image object on box dispose (only if image was loaded by the box)
        /// </summary>
        private bool _releaseImageObject;

        /// <summary>
        /// is the handler has been disposed
        /// </summary>
        private bool _disposed;

        #endregion

        /// <summary>
        /// Init.
        /// </summary>
        /// <param name="htmlContainer">the container of the html to handle load image for</param>
        /// <param name="loadCompleteCallback">callback raised when image load process is complete with image or without</param>
        public ImageLoadHandler(HtmlContainerInt htmlContainer, ActionInt<RImage, RRect, bool> loadCompleteCallback) {
            ArgChecker.AssertArgNotNull(htmlContainer, "htmlContainer");
            ArgChecker.AssertArgNotNull(loadCompleteCallback, "loadCompleteCallback");

            _htmlContainer = htmlContainer;
            _loadCompleteCallback = loadCompleteCallback;
        }

        /// <summary>
        /// the image instance of the loaded image
        /// </summary>
        public RImage Image {
            get { return _image; }
        }

        /// <summary>
        /// the image rectangle restriction as returned from image load event
        /// </summary>
        public RRect Rectangle {
            get { return _imageRectangle; }
        }

        /// <summary>
        /// Set image of this image box by analyzing the src attribute.<br/>
        /// LoadFromRaw the image from inline base64 encoded string.<br/>
        /// Or from calling property/method on the bridge object that returns image or URL to image.<br/>
        /// Or from file path<br/>
        /// Or from URI.
        /// </summary>
        /// <remarks>
        /// File path and URI image loading is executed async and after finishing calling <see cref="ImageLoadComplete"/>
        /// on the main thread and not thread-pool.
        /// </remarks>
        /// <param name="src">the source of the image to load</param>
        /// <param name="attributes">the collection of attributes on the element to use in event</param>
        /// <returns>the image object (null if failed)</returns>
        public void LoadImage(string src, Dictionary<string, string> attributes) {
            try {
                var args = new HtmlImageLoadEventArgs(src, attributes, OnHtmlImageLoadEventCallback);
                _htmlContainer.RaiseHtmlImageLoadEvent(args);
                _asyncCallback = !_htmlContainer.AvoidAsyncImagesLoading;

                if (!args.Handled) {
                    if (!string.IsNullOrEmpty(src)) {
                        if (src.StartsWith("data:image", StringComparison.CurrentCultureIgnoreCase)) {
                            SetFromInlineData(src);
                        } else {
                            SetImageFromPath(src);
                        }
                    } else {
                        ImageLoadComplete(false);
                    }
                }
            } catch (Exception ex) {
                _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Exception in handling image source", ex);
                ImageLoadComplete(false);
            }
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose() {
            _disposed = true;
            ReleaseObjects();
        }

        #region Private methods

        /// <summary>
        /// Set the image using callback from load image event, use the given data.
        /// </summary>
        /// <param name="path">the path to the image to load (file path or uri)</param>
        /// <param name="image">the image to load</param>
        /// <param name="imageRectangle">optional: limit to specific rectangle of the image and not all of it</param>
        private void OnHtmlImageLoadEventCallback(string path, object image, RRect imageRectangle) {
            if (!_disposed) {
                _imageRectangle = imageRectangle;

                if (image != null) {
                    _image = _htmlContainer.Adapter.ConvertImage(image);
                    ImageLoadComplete(_asyncCallback);
                } else if (!string.IsNullOrEmpty(path)) {
                    SetImageFromPath(path);
                } else {
                    ImageLoadComplete(_asyncCallback);
                }
            }
        }

        /// <summary>
        /// LoadFromRaw the image from inline base64 encoded string data.
        /// </summary>
        /// <param name="src">the source that has the base64 encoded image</param>
        private void SetFromInlineData(string src) {
            _image = GetImageFromData(src);
            if (_image == null)
                _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed extract image from inline data");
            _releaseImageObject = true;
            ImageLoadComplete(false);
        }

        /// <summary>
        /// Extract image object from inline base64 encoded data in the src of the html img element.
        /// </summary>
        /// <param name="src">the source that has the base64 encoded image</param>
        /// <returns>image from base64 data string or null if failed</returns>
        private RImage GetImageFromData(string src) {
            var s = src.Substring(src.IndexOf(':') + 1).Split(new[] {','}, 2);
            if (s.Length == 2) {
                int imagePartsCount = 0, base64PartsCount = 0;
                foreach (var part in s[0].Split(';')) {
                    var pPart = part.Trim();
                    if (pPart.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase))
                        imagePartsCount++;
                    if (pPart.Equals("base64", StringComparison.InvariantCultureIgnoreCase))
                        base64PartsCount++;
                }

                if (imagePartsCount > 0) {
                    byte[] imageData = base64PartsCount > 0 ? Convert.FromBase64String(s[1].Trim()) : new UTF8Encoding().GetBytes(Uri.UnescapeDataString(s[1].Trim()));
                    return _htmlContainer.Adapter.ImageFromStream(new MemoryStream(imageData));
                }
            }
            return null;
        }

        /// <summary>
        /// LoadFromRaw image from path of image file or URL.
        /// </summary>
        /// <param name="path">the file path or uri to load image from</param>
        private void SetImageFromPath(string path) {
            var uri = CommonUtils.TryGetUri(path);
            if (uri != null && uri.Scheme != "file") {
                SetImageFromUrl(uri);
            } else {
                var fileInfo = CommonUtils.TryGetFileInfo(uri != null ? uri.AbsolutePath : path);
                if (fileInfo != null) {
                    SetImageFromFile(fileInfo);
                } else {
                    _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed load image, invalid source: " + path);
                    ImageLoadComplete(false);
                }
            }
        }

        /// <summary>
        /// LoadFromRaw the image file on thread-pool thread and calling <see cref="ImageLoadComplete"/> after.
        /// </summary>
        /// <param name="source">the file path to get the image from</param>
        private void SetImageFromFile(FileInfo source) {
            if (source.Exists) {
                if (_htmlContainer.AvoidAsyncImagesLoading)
                    LoadImageFromFile(source);
                else
                    ThreadPool.QueueUserWorkItem(state => LoadImageFromFile(source));
            } else {
                ImageLoadComplete();
            }
        }

        /// <summary>
        /// LoadFromRaw the image file on thread-pool thread and calling <see cref="ImageLoadComplete"/> after.<br/>
        /// Calling <see cref="ImageLoadComplete"/> on the main thread and not thread-pool.
        /// </summary>
        /// <param name="source">the file path to get the image from</param>
        private void LoadImageFromFile(FileInfo source) {
            try {
                if (source.Exists) {
                    _imageFileStream = File.Open(source.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                    _image = _htmlContainer.Adapter.ImageFromStream(_imageFileStream);
                    _releaseImageObject = true;
                }
                ImageLoadComplete();
            } catch (Exception ex) {
                _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image from disk: " + source, ex);
                ImageLoadComplete();
            }
        }

        /// <summary>
        /// LoadFromRaw image from the given URI by downloading it.<br/>
        /// Create local file name in temp folder from the URI, if the file already exists use it as it has already been downloaded.
        /// If not download the file using <see cref="DownloadImageFromUrlAsync"/>.
        /// </summary>
        private void SetImageFromUrl(Uri source) {
            var filePath = CommonUtils.GetLocalfileName(source);
            if (filePath.Exists && filePath.Length > 0) {
                SetImageFromFile(filePath);
            } else {
                if (_htmlContainer.AvoidAsyncImagesLoading)
                    DownloadImageFromUrl(source, filePath);
                else
                    ThreadPool.QueueUserWorkItem(DownloadImageFromUrlAsync, new KeyValuePair<Uri, FileInfo>(source, filePath));
            }
        }

        /// <summary>
        /// Download the requested file in the URI to the given file path.<br/>
        /// Use async sockets API to download from web, <see cref="OnDownloadImageCompleted(object,System.ComponentModel.AsyncCompletedEventArgs)"/>.
        /// </summary>
        private void DownloadImageFromUrl(Uri source, FileInfo filePath) {
            try {
                using (var client = _client = new WebClient()) {
                    client.DownloadFile(source, filePath.FullName);
                    OnDownloadImageCompleted(false, null, filePath, client);
                }
            } catch (Exception ex) {
                OnDownloadImageCompleted(false, ex, null, null);
            }
        }

        /// <summary>
        /// Download the requested file in the URI to the given file path.<br/>
        /// Use async sockets API to download from web, <see cref="OnDownloadImageCompleted(object,System.ComponentModel.AsyncCompletedEventArgs)"/>.
        /// </summary>
        /// <param name="data">key value pair of URL and file info to download the file to</param>
        private void DownloadImageFromUrlAsync(object data) {
            var uri = ((KeyValuePair<Uri, FileInfo>) data).Key;
            var filePath = ((KeyValuePair<Uri, FileInfo>) data).Value;

            try {
                _client = new WebClient();
                _client.DownloadFileCompleted += OnDownloadImageCompleted;
                _client.DownloadFileAsync(uri, filePath.FullName, filePath);
            } catch (Exception ex) {
                OnDownloadImageCompleted(false, ex, null, null);
            }
        }

        /// <summary>
        /// On download image complete to local file use <see cref="LoadImageFromFile"/> to load the image file.<br/>
        /// If the download canceled do nothing, if failed report error.
        /// </summary>
        private void OnDownloadImageCompleted(object sender, AsyncCompletedEventArgs e) {
            try {
                using (var client = (WebClient) sender) {
                    client.DownloadFileCompleted -= OnDownloadImageCompleted;
                    OnDownloadImageCompleted(e.Cancelled, e.Error, (FileInfo) e.UserState, client);
                }
            } catch (Exception ex) {
                OnDownloadImageCompleted(false, ex, null, null);
            }
        }

        /// <summary>
        /// On download image complete to local file use <see cref="LoadImageFromFile"/> to load the image file.<br/>
        /// If the download canceled do nothing, if failed report error.
        /// </summary>
        private void OnDownloadImageCompleted(bool cancelled, Exception error, FileInfo filePath, WebClient client) {
            if (!cancelled && !_disposed) {
                if (error == null) {
                    filePath.Refresh();
                    var contentType = CommonUtils.GetResponseContentType(client);
                    if (contentType != null && contentType.StartsWith("image", StringComparison.OrdinalIgnoreCase)) {
                        LoadImageFromFile(filePath);
                    } else {
                        _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image, not image content type: " + contentType);
                        ImageLoadComplete();
                        filePath.Delete();
                    }
                } else {
                    _htmlContainer.ReportError(HtmlRenderErrorType.Image, "Failed to load image from URL: " + client.BaseAddress, error);
                    ImageLoadComplete();
                }
            }
        }

        /// <summary>
        /// Flag image load complete and request refresh for re-layout and invalidate.
        /// </summary>
        private void ImageLoadComplete(bool async = true) {
            // can happen if some operation return after the handler was disposed
            if (_disposed)
                ReleaseObjects();
            else
                _loadCompleteCallback(_image, _imageRectangle, async);
        }

        /// <summary>
        /// Release the image and client objects.
        /// </summary>
        private void ReleaseObjects() {
            if (_releaseImageObject && _image != null) {
                _image.Dispose();
                _image = null;
            }
            if (_imageFileStream != null) {
                _imageFileStream.Dispose();
                _imageFileStream = null;
            }
            if (_client != null) {
                _client.CancelAsync();
                _client.Dispose();
                _client = null;
            }
        }

        #endregion
    }
}