using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Radzen.Blazor.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Radzen.Blazor
{
    /// <summary>
    /// A file upload component with progress tracking, multiple file support, and drag-and-drop capability.
    /// RadzenUpload provides a full-featured file upload interface with automatic or manual upload, server communication, and comprehensive event handling.
    /// Handles file selection and upload to a server endpoint with automatic upload on file selection or manual triggering via Upload() method,
    /// real-time upload progress with percentage and bytes loaded, file type restriction via Accept property (MIME types or extensions),
    /// custom authentication or HTTP headers, Complete/Error/Progress/Change events for tracking upload lifecycle,
    /// automatic image preview for image files, file removal support, and built-in drag-and-drop for file selection.
    /// Uploads files to the URL endpoint via HTTP POST multipart/form-data. Server-side endpoint must accept file uploads and return appropriate responses.
    /// </summary>
    /// <example>
    /// Basic upload with auto-upload:
    /// <code>
    /// &lt;RadzenUpload Url="api/upload" Change=@OnChange Complete=@OnComplete /&gt;
    /// @code {
    ///     void OnChange(UploadChangeEventArgs args)
    ///         => Console.WriteLine($"Selected {args.Files?.Count() ?? 0} files");
    ///
    ///     void OnComplete(UploadCompleteEventArgs args)
    ///         => Console.WriteLine(args.Cancelled ? "Upload cancelled" : "Upload complete");
    /// }
    /// </code>
    /// Manual upload with progress tracking:
    /// <code>
    /// &lt;RadzenUpload @ref=upload Url="api/upload" Auto="false" Multiple="true" Progress=@OnProgress /&gt;
    /// &lt;RadzenButton Text="Upload" Click=@(() => upload.Upload()) /&gt;
    /// @code {
    ///     RadzenUpload upload;
    ///     void OnProgress(UploadProgressArgs args)
    ///     {
    ///         Console.WriteLine($"Progress: {args.Progress}% ({args.Loaded}/{args.Total} bytes)");
    ///     }
    /// }
    /// </code>
    /// Upload with file type filtering:
    /// <code>
    /// &lt;RadzenUpload Url="api/upload/images" Accept="image/*" MaxFileSize="5000000" /&gt;
    /// </code>
    /// </example>
    public partial class RadzenUpload : RadzenComponent
    {
        IJSObjectReference? _jsRef;

        /// <inheritdoc />
        public override void Dispose()
        {
            base.Dispose();
            _jsRef?.InvokeVoidAsync("dispose");
            _jsRef?.DisposeAsync();
            GC.SuppressFinalize(this);
        }

        private string? imageAlternateText;

        /// <summary>
        /// Gets or sets the text.
        /// </summary>
        /// <value>The text.</value>
        [Parameter]
        public string ImageAlternateText { get => imageAlternateText ?? Localize(nameof(RadzenStrings.Upload_ImageAlternateText)); set => imageAlternateText = value; }

        /// <summary>
        /// Specifies additional custom attributes that will be rendered by the input.
        /// </summary>
        /// <value>The attributes.</value>
        [Parameter]
        public IReadOnlyDictionary<string, object>? InputAttributes { get; set; }

        /// <summary>
        /// Gets file input reference.
        /// </summary>
        protected ElementReference fileUpload;

        /// <summary>
        /// Gets or sets the name.
        /// </summary>
        /// <value>The name.</value>
        [Parameter]
        public string? Name { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="RadzenUpload"/> upload is automatic.
        /// </summary>
        /// <value><c>true</c> if upload automatic; otherwise, <c>false</c>.</value>
        [Parameter]
        public bool Auto { get; set; } = true;

        private string? chooseText;

        /// <summary>
        /// Gets or sets the choose button text.
        /// </summary>
        /// <value>The choose button text.</value>
        [Parameter]
        public string ChooseText { get => chooseText ?? Localize(nameof(RadzenStrings.Upload_ChooseText)); set => chooseText = value; }

        private string? deleteText;

        /// <summary>
        /// Gets or sets the choose button text.
        /// </summary>
        /// <value>The choose button text.</value>
        [Parameter]
        public string DeleteText { get => deleteText ?? Localize(nameof(RadzenStrings.Upload_DeleteText)); set => deleteText = value; }

        /// <summary>
        /// Gets or sets the URL.
        /// </summary>
        /// <value>The URL.</value>
        [Parameter]
        public string? Url { get; set; }

        /// <summary>
        /// Specifies the HTTP method used for uploading files to the defined <see cref="Url"/> endpoint.
        /// <para>
        /// Common values are <c>POST</c> (default) and <c>PUT</c>.
        /// </para>
        /// <para>
        /// If the <see cref="Url"/> parameter is not set, this property is ignored.
        /// </para>
        /// <para>
        /// Defaults to <c>POST</c>.
        /// </para>
        /// </summary>
        /// <value>The HTTP method for file upload requests.</value>
        [Parameter]
        public string Method { get; set; } = "POST";
        
        /// <summary>
        /// Enables streaming upload mode for large files to the specified <see cref="Url"/>.
        /// <para>
        /// When <c>true</c>, files are uploaded as raw binary streams instead of <c>multipart/form-data</c>.
        /// Only a single file can be uploaded at a time in streaming mode.
        /// </para>
        /// <para>
        /// When <c>false</c> (default), files are uploaded as <c>multipart/form-data</c> (standard form upload),
        /// and multiple files can be uploaded simultaneously if <see cref="Multiple"/> is enabled.
        /// </para>
        /// <para>
        /// This property is ignored if <see cref="Url"/> is not set.
        /// </para>
        /// </summary>
        /// <value><c>true</c> to stream file data directly; otherwise, <c>false</c> (default).</value>
        [Parameter]
        public bool Stream { get; set; }
        
        /// <summary>
        /// Gets or sets the parameter name. If not set 'file' parameter name will be used for single file and 'files' for multiple files.
        /// </summary>
        /// <value>The parameter name.</value>
        [Parameter]
        public string? ParameterName { get; set; }

        /// <summary>
        /// Gets or sets the accepted MIME types.
        /// </summary>
        /// <value>The accepted MIME types.</value>
        [Parameter]
        public string? Accept { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="RadzenUpload"/> is multiple.
        /// </summary>
        /// <value><c>true</c> if multiple; otherwise, <c>false</c>.</value>
        [Parameter]
        public bool Multiple { get; set; }

        /// <summary>
        /// Gets or sets the icon.
        /// </summary>
        /// <value>The icon.</value>
        [Parameter]
        public string? Icon { get; set; }

        /// <summary>
        /// Gets or sets the icon color.
        /// </summary>
        /// <value>The icon color.</value>
        [Parameter]
        public string? IconColor { get; set; }

        /// <summary>
        /// Gets or sets the maximum number of files.
        /// </summary>
        /// <value>The maximum number of files.</value>
        [Parameter]
        public int MaxFileCount { get; set; } = 10;

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="RadzenUpload"/> is disabled.
        /// </summary>
        /// <value><c>true</c> if disabled; otherwise, <c>false</c>.</value>
        [Parameter]
        public bool Disabled { get; set; }

        /// <summary>
        /// Gets or sets the tab order index for keyboard navigation.
        /// Controls the order in which the Choose button receives focus when the user presses the Tab key.
        /// Lower values receive focus first. Use -1 to exclude from tab navigation.
        /// </summary>
        /// <value>The tab index. Default is 0 (natural tab order).</value>
        [Parameter]
        public int TabIndex { get; set; } = 0;

        /// <summary>
        /// Gets the choose class list.
        /// </summary>
        /// <value>The choose class list.</value>
        string ChooseClassList => ClassList.Create("rz-fileupload-choose rz-button")
                                           .AddDisabled(Disabled)
                                           .ToString();

        /// <summary>
        /// Gets the button class list.
        /// </summary>
        /// <value>The button class list.</value>
        string ButtonClassList => ClassList.Create("rz-button rz-button-icon-only rz-base rz-shade-default")
                                            .AddDisabled(Disabled)
                                            .ToString();

        /// <summary>
        /// Gets or sets the child content.
        /// </summary>
        /// <value>The child content.</value>
        [Parameter]
        public RenderFragment? ChildContent { get; set; }

        /// <summary>
        /// Uploads this instance selected files.
        /// </summary>
        public async Task Upload()
        {
            if (JSRuntime == null)
            {
                return;
            }

            await JSRuntime.InvokeAsync<string>("Radzen.upload", fileUpload, Url, Multiple, false, ParameterName);
        }

        readonly IDictionary<string, string> headers = new Dictionary<string, string>();

        internal void AddHeader(string name, string value)
        {
            if (name != null)
            {
                headers.Add(name, value);
            }
        }

        internal void RemoveHeader(string name)
        {
            if (name != null)
            {
                headers.Remove(name);
            }
        }

        private bool visibleChanged;
        private bool firstRender = true;

        /// <inheritdoc />
        bool _jsParamsChanged;

        /// <inheritdoc />
        public override async Task SetParametersAsync(ParameterView parameters)
        {
            visibleChanged = parameters.DidParameterChange(nameof(Visible), Visible);

            if (parameters.DidParameterChange(nameof(Url), Url) ||
                parameters.DidParameterChange(nameof(Auto), Auto) ||
                parameters.DidParameterChange(nameof(Multiple), Multiple) ||
                parameters.DidParameterChange(nameof(ParameterName), ParameterName) ||
                parameters.DidParameterChange(nameof(Method), Method) ||
                parameters.DidParameterChange(nameof(Stream), Stream))
            {
                _jsParamsChanged = true;
            }

            await base.SetParametersAsync(parameters);

            if (visibleChanged && !firstRender)
            {
                if (Visible == false)
                {
                    Dispose();
                }
            }
        }

        /// <inheritdoc />
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            await base.OnAfterRenderAsync(firstRender);

            this.firstRender = firstRender;

            if (firstRender || visibleChanged || _jsParamsChanged)
            {
                visibleChanged = false;

                if (Visible && JSRuntime != null)
                {
                    await JSRuntime.InvokeVoidAsync("Radzen.uploads", Reference, Name ?? GetId());

                    if (_jsParamsChanged || firstRender)
                    {
                        _jsParamsChanged = false;

                        if (_jsRef != null)
                        {
                            await _jsRef.InvokeVoidAsync("dispose");
                            await _jsRef.DisposeAsync();
                        }

                        _jsRef = await JSRuntime.InvokeAsync<IJSObjectReference>(
                            "Radzen.createUpload", Element,
                            !string.IsNullOrEmpty(Url) ? Url : null, Auto, Multiple,
                            ParameterName, Method, Stream);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the headers.
        /// </summary>
        /// <returns>IDictionary&lt;System.String, System.String&gt;.</returns>
        [JSInvokable("GetHeaders")]
        public IDictionary<string, string> GetHeaders()
        {
            return headers;
        }

        /// <summary>
        /// Gets or sets the change callback.
        /// </summary>
        /// <value>The change callback.</value>
        [Parameter]
        public EventCallback<UploadChangeEventArgs> Change { get; set; }

        /// <summary>
        /// Gets or sets the progress callback.
        /// </summary>
        /// <value>The progress callback.</value>
        [Parameter]
        public EventCallback<UploadProgressArgs> Progress { get; set; }

        /// <summary>
        /// Gets or sets the complete callback.
        /// </summary>
        /// <value>The complete callback.</value>
        [Parameter]
        public EventCallback<UploadCompleteEventArgs> Complete { get; set; }

        /// <summary>
        /// Gets or sets the error callback.
        /// </summary>
        /// <value>The error callback.</value>
        [Parameter]
        public EventCallback<UploadErrorEventArgs> Error { get; set; }

        List<PreviewFileInfo> files = Enumerable.Empty<PreviewFileInfo>().ToList();

        /// <summary>
        /// Gets a value indicating whether this instance has value.
        /// </summary>
        /// <value><c>true</c> if this instance has value; otherwise, <c>false</c>.</value>
        public bool HasValue
        {
            get
            {
                return files.Count > 0;
            }
        }

        /// <summary>
        /// Clear selected file(s) from the upload selection
        /// </summary>
        public async System.Threading.Tasks.Task ClearFiles()
        {
            while(files.Count > 0)
            {
                await OnRemove(files[0], false);
            }

            await Change.InvokeAsync(CreateUploadChangeEventArgs(files));
        }

        /// <summary>
        /// Called on file remove.
        /// </summary>
        /// <param name="fileName">The name of the file to remove.</param>
        /// <param name="ignoreCase">Specify true is file name casing should be ignored (default: false)</param>
        public async System.Threading.Tasks.Task RemoveFile(string fileName, bool ignoreCase = false)
        {
            var comparisonMethod = ignoreCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
            var fileInfo = files.FirstOrDefault(f => string.Equals(f.Name, fileName, comparisonMethod));
            if (fileInfo != null)
            {
                await OnRemove(fileInfo);
            }
        }

        /// <summary>
        /// Called on file remove.
        /// </summary>
        /// <param name="file">The file.</param>
        /// <param name="fireChangeEvent">If the linked <see cref="Change" /> event should be fired as a result of this removal (default: true)</param>
        protected async System.Threading.Tasks.Task OnRemove(PreviewFileInfo file, bool fireChangeEvent = true)
        {
            ArgumentNullException.ThrowIfNull(file);
            files.Remove(file);

            if (JSRuntime != null)
            {
                await JSRuntime.InvokeVoidAsync("Radzen.removeFileFromUpload", Reference, file.Name, Name ?? GetId());
            }

            if (fireChangeEvent)
            {
                await Change.InvokeAsync(CreateUploadChangeEventArgs(files));
            }
        }

        /// <summary>
        /// Called on file change.
        /// </summary>
        /// <param name="files">The files.</param>
        [JSInvokable("RadzenUpload.OnChange")]
        public async System.Threading.Tasks.Task OnChange(IEnumerable<PreviewFileInfo> files)
        {
            var fileList = files?.ToList();

            if (fileList == null || fileList.Count == 0)
            {
                return;
            }

            this.files = fileList;

            await Change.InvokeAsync(CreateUploadChangeEventArgs(files ?? Enumerable.Empty<FileInfo>()));

            await InvokeAsync(StateHasChanged);
        }

        /// <summary>
        /// Called on progress.
        /// </summary>
        /// <param name="progress">The progress.</param>
        /// <param name="loaded">The loaded.</param>
        /// <param name="total">The total.</param>
        /// <param name="files">The files.</param>
        /// <param name="cancel">The cancelled state.</param>
        [JSInvokable("RadzenUpload.OnProgress")]
        public async System.Threading.Tasks.Task<bool> OnProgress(int progress, long loaded, long total, IEnumerable<FileInfo> files, bool cancel)
        {
            var args = new UploadProgressArgs() { Progress = progress, Loaded = loaded, Total = total, Files = files, Cancel = cancel };
            await Progress.InvokeAsync(args); ;

            return args.Cancel;
        }

        /// <summary>
        /// Called when upload is complete.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <param name="cancelled">Flag indicating if the upload was cancelled</param>
        [JSInvokable("RadzenUpload.OnComplete")]
        public async System.Threading.Tasks.Task OnComplete(string response, bool cancelled)
        {
            System.Text.Json.JsonDocument? doc = null;

            if (!string.IsNullOrEmpty(response))
            {
                try
                {
                    doc = System.Text.Json.JsonDocument.Parse(response);
                }
                catch (System.Text.Json.JsonException)
                {
                    //
                }
            }

            await Complete.InvokeAsync(new UploadCompleteEventArgs() { RawResponse = response, JsonResponse = doc, Cancelled = cancelled });
        }

        /// <summary>
        /// Called on upload error.
        /// </summary>
        /// <param name="error">The error.</param>
        [JSInvokable("RadzenUpload.OnError")]
        public async System.Threading.Tasks.Task OnError(string error)
        {
            await Error.InvokeAsync(new UploadErrorEventArgs() { Message = error });
        }

        /// <inheritdoc />
        protected override string GetComponentCssClass()
        {
            return "rz-fileupload";
        }

        async Task OnInputChange(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs args)
        {
            if (Disabled)
            {
                return;
            }

            IEnumerable<FileInfo> files = Enumerable.Empty<FileInfo>();

            if (Multiple)
            {
                try
                {
                    files = args.GetMultipleFiles(MaxFileCount).Select(f => new FileInfo(f));
                }
                catch
                {
                    await Error.InvokeAsync(new UploadErrorEventArgs() { Message = $"Maximum number of files exceeded. Maximum allowed is {MaxFileCount}." });
                    return;
                }
            }
            else
            {
                files = new FileInfo[] { new FileInfo(args.File) };
            }

            this.files = files.Where(f => f.Source != null).Select(f => new PreviewFileInfo(f.Source!) { Name = f.Name, Size = f.Size }).ToList();

            await Change.InvokeAsync(CreateUploadChangeEventArgs(files));

            await InvokeAsync(StateHasChanged);
        }

        /// <summary>
        /// Creates the upload change event args.
        /// </summary>
        /// <param name="files"></param>
        /// <returns></returns>
        public UploadChangeEventArgs CreateUploadChangeEventArgs(IEnumerable<FileInfo> files)
           => new UploadChangeEventArgs() { Files = files };
    }
}
