@using Iceshrimp.Assets.PhosphorIcons
@using Iceshrimp.Frontend.Components.Note
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Core.Services.NoteStore
@using Iceshrimp.Frontend.Localization
@using Iceshrimp.MfmSharp
@using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@inject IJSRuntime                     Js
@inject ApiService                     ApiService
@inject ComposeService                 ComposeService
@inject EmojiService                   EmojiService
@inject SessionService                 SessionService
@inject MetadataService                MetadataService
@inject IStringLocalizer<Localization> Loc;
@inject GlobalComponentSvc             GlobalComponentSvc
@inject SettingsService                Settings;
@inject NoteActions                    NoteActions;
<dialog @onkeydown="HandleKeyDown" class="dialog" @ref="Dialog">
    <div class="compose">
        <div class="header">
            <button class="btn" title="@Loc["Close"]" @onclick="CloseDialog" aria-label="close" disabled="@(SendLock || UploadingFiles != 0)">
                <Icon Name="Icons.X"/>
            </button>
            <Dropdown TBind="NoteVisibility" Elements="@DropDownCreate()" @bind-Value="NoteDraft.Visibility"/>
            <StateButton OnClick="SendNote" @ref="SendButton" ExtraClasses="post-btn" AriaLabel="post"
                         Disabled="@SendingDisabled()">
                <Initial>
                    @Loc["ComposeNote"]<Icon Name="Icons.PaperPlaneRight"/>
                </Initial>
                <Loading>
                    @Loc["Sending"]<LoadingSpinner />
                </Loading>
                <Success>
                    @Loc["Done"]<Icon Name="Icons.Check"/>
                </Success>
                <Failed>
                    @Loc["Retry"]<Icon Name="Icons.X"/>
                </Failed>
            </StateButton>
        </div>
        @if (ReplyOrQuote != null && !Preview)
        {
            <div class="reply-or-quote">
                <NoteComponent Note="ReplyOrQuote" AsQuote="true" OpenNote="false"/>
            </div>
        }
        @if (NoteDraft.Cw != null)
        {
            <input @bind="NoteDraft.Cw" @bind:event="oninput" class="input cw-field" placeholder="Content Warning"
                   aria-label="content warning"/>
            <hr class="separator"/>
        }
        <textarea @ref="Textarea" @bind="NoteDraft.Text" @bind:event="oninput" class="textarea"
                  placeholder="@TextPlaceholder" rows="5"
                  cols="35" autofocus="autofocus" aria-label="note text"></textarea>
        <div class="footer">
            <div class="buttons">
                <button class="btn" title="@Loc["Upload file"]" @onclick="OpenUpload" aria-label="upload">
                    <Icon Name="Icons.Upload" Size="1.3rem"></Icon>
                </button>
                <div class="file-input">
                    <InputFile @ref="UploadInput" OnChange="Upload">Upload!</InputFile>
                </div>
                <button class="btn" title="@Loc["Poll"]" @onclick="TogglePoll" aria-label="poll">
                    <Icon Name="Icons.MicrophoneStage" Size="1.3rem"></Icon>
                </button>
                <button class="btn" title="@Loc["Content warning"]" @onclick="ToggleCw" aria-label="content warning">
                    <Icon Name="Icons.EyeSlash" Size="1.3rem"></Icon>
                </button>
                <button @ref="EmojiButton" class="btn" title="@Loc["Emoji"]" @onclick="ToggleEmojiPicker"
                        aria-label="emoji">
                    <Icon Name="Icons.Smiley" Size="1.3rem"></Icon>
                </button>
                @if (NoteDraft.RenoteId == null && AttachedQuote == null)
                {
                    <button class="btn" title="@Loc["Add quote"]" @onclick="AddQuote" aria-label="add quote">
                        <Icon Name="Icons.Quotes" Size="1.3rem"></Icon>
                    </button>
                }
            </div>
            <div class="buttons">
                <span title="@Loc["Character limit"]"
                      aria-label="character limit">@(NoteLength - ((NoteDraft.Cw?.Length ?? 0) + NoteDraft.Text.Length))</span>
                <button class="btn" title="@Loc["Preview"]" @onclick="() => Preview = !Preview"
                        aria-label="preview">
                    <Icon Name="Icons.Binoculars" Size="1.3rem"></Icon>
                </button>
            </div>
        </div>
        @if (AttachedQuote != null)
        {
            <div>
                <span>
                    <Icon Name="Icons.Quotes"/>
                    <a href="@($"/notes/{AttachedQuote}")" target="_blank">@Loc["Attached quote"]</a>
                    <button class="btn" title="@Loc["Remove quote"]" @onclick="RemoveQuote" aria-label="remove quote">
                        <Icon Name="Icons.Trash" Size="1rem"></Icon>
                    </button>
                </span>
            </div>
        }
        @if (UploadingFiles != 0)
        {
            <div>
                <span><LoadingSpinner/> @(UploadingFiles == 1 ? Loc["Uploading file"] : Loc["Uploading {0} files", UploadingFiles])</span>
            </div>
        }
        @if (Attachments.Count != 0)
        {
            <div class="attachments">
                @foreach (var attachment in Attachments)
                {
                    <ComposeAttachment File="@attachment" AddInlineMedia="AddInlineMedia" RemoveAttachment="RemoveAttachment"/>
                }
            </div>
        }
        @if (NoteDraft.Poll != null)
        {
            <div class="poll">
                <div class="choices">
                    @foreach (var entry in PollChoices)
                    {
                        <div class="choice">
                            <input class="input" placeholder="@Loc["Choice"]" @bind="@entry.Value"/>
                            <button class="button" title="@Loc["Delete Choice"]" @onclick="() => DeletePollChoice(entry)">
                                <Icon Name="Icons.Trash"/>
                            </button>
                        </div>
                    }
                    @if (PollChoices.Count < 10)
                    {
                        <div class="choice">
                            <input class="input" placeholder="@Loc["Choice"]" @bind="@PollEntry"/>
                            <button class="button" title="@Loc["Add Choice"]" @onclick="AddPollChoice">
                                <Icon Name="Icons.Plus"/>
                            </button>
                        </div>
                    }
                </div>
                <input type="checkbox" id="poll-multiple" @bind="NoteDraft.Poll.Multiple"/>
                <label for="poll-multiple">@Loc["Multiple choice"]</label>
                <div class="expire">
                    <div>
                        <label for="poll-expires">@Loc["End poll"]</label>
                        <select @bind="PollExpires" id="poll-expires">
                            <option value="@PollExpire.Never">@Loc["Never"]</option>
                            <option value="@PollExpire.ExpiresAt">@Loc["End at"]</option>
                            <option value="@PollExpire.ExpiresAfter">@Loc["End after"]</option>
                        </select>
                    </div>
                    @switch (PollExpires)
                    {
                        case PollExpire.Never:
                            break;
                        case PollExpire.ExpiresAt:
                            <input type="datetime-local" class="input" @bind="PollExpireTime"/>
                            break;
                        case PollExpire.ExpiresAfter:
                            <input type="number" class="input" placeholder="@Loc["Duration"]" min="@(PollExpiresAft == PollExpireAfter.Minutes ? 5 : 1)" @bind="PollExpireLen"/>
                            <select @bind="PollExpiresAft">
                                <option value="@PollExpireAfter.Minutes">@Loc["Minutes"]</option>
                                <option value="@PollExpireAfter.Hours">@Loc["Hours"]</option>
                                <option value="@PollExpireAfter.Days">@Loc["Days"]</option>
                            </select>
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                </div>
            </div>
        }
        @if (Preview)
        {
            <div class="preview">
                @if (string.IsNullOrWhiteSpace(NoteDraft.Text))
                {
                    <span>@Loc["Nothing to preview"]</span>
                }
                else
                {
                    <MfmText Text="@NoteDraft.Text.ReplaceLineEndings("\n")" Emoji="EmojiList"/>
                }
            </div>
        }
        <div @onclick="CloseDialog" class="backdrop"></div>
    </div>
</dialog>


@code {
    private ElementReference         Dialog { get; set; }
    private IJSObjectReference       _module = null!;
    private IList<DriveFileResponse> Attachments     { get; set; } = [];
    private InputFile                UploadInput     { get; set; } = null!;
    private NoteBase?                ReplyOrQuote    { get; set; }
    private string?                  TextPlaceholder { get; set; }
    private ElementReference         Textarea        { get; set; }
    private ElementReference         EmojiButton     { get; set; }
    private StateButton              SendButton      { get; set; } = null!;
    private bool                     Preview         { get; set; }
    private List<EmojiResponse>      EmojiList       { get; set; } = [];
    private int                      NoteLength      { get; set; }
    private bool                     SendLock        { get; set; } = false;
    private int                      UploadingFiles  { get; set; }
    private string?                  AttachedQuote   { get; set; }
    private List<Choice>             PollChoices     { get; set; } = [];
    private string                   PollEntry       { get; set; } = "";
    private PollExpire               PollExpires     { get; set; }
    private DateTime                 PollExpireTime  { get; set; }
    private int                      PollExpireLen   { get; set; }
    private PollExpireAfter          PollExpiresAft  { get; set; }

    private NoteCreateRequest NoteDraft { get; set; } = new()
    {
        Text       = "",
        Visibility = NoteVisibility.Followers, // FIXME: Default to visibilty in settings
        Cw         = null
    };

    private Dictionary<string, string> AvailablePlaceholders { get; set; } = new() { { "default", "What's on your mind?" }, { "reply", "Reply goes here!" }, { "quote", "Quote this post!" } };

    private class Choice
    {
        public required string Value { get; set; }
    }

    private enum PollExpire
    {
        Never,
        ExpiresAt,
        ExpiresAfter
    }

    private enum PollExpireAfter
    {
        Minutes,
        Hours,
        Days,
    }

    private bool SendingDisabled() =>
        NoteLength - ((NoteDraft.Cw?.Length ?? 0) + NoteDraft.Text.Length) < 0 || UploadingFiles != 0 || NoteDraft.Poll != null && PollChoices.Count(p => !string.IsNullOrWhiteSpace(p.Value)) < 2;

    private async Task HandleKeyDown(KeyboardEventArgs e)
    {
        if (e is { Code: "Enter", CtrlKey: true } or { Code: "Enter", MetaKey: true })
        {
            if (!SendingDisabled()) await SendNote();
        }
    }

    RenderFragment DropdownIcon(NoteVisibility vis)
    {
        return vis switch
        {
            NoteVisibility.Public => (@<Icon Name="Icons.Globe"/>),
            NoteVisibility.Home => (@<Icon Name="Icons.House"/>),
            NoteVisibility.Followers => (@<Icon Name="Icons.Lock"/>),
            NoteVisibility.Specified => (@<Icon Name="Icons.Envelope"/>),
            _ => throw new ArgumentOutOfRangeException()
        };
    }

    RenderFragment DropdownContent(NoteVisibility vis)
    {
        return vis switch
        {
            NoteVisibility.Public => (@<span class="dropdown-title">Public</span>),
            NoteVisibility.Home => (@<span class="dropdown-title">Unlisted</span>),
            NoteVisibility.Followers => (@<span class="dropdown-title">Followers</span>),
            NoteVisibility.Specified => (@<span class="dropdown-title">Direct</span>),
            _ => throw new ArgumentOutOfRangeException()
        };
    }

    private IList<DropdownElement<NoteVisibility>> DropDownCreate()
    {
        return Enum.GetValues<NoteVisibility>()
                   .Select(vis =>
                               new DropdownElement<NoteVisibility>
                               {
                                   #pragma warning disable BL0005 // Setting this outside the component is fine until this is reworked
                                   Icon = DropdownIcon(vis), Content = DropdownContent(vis), Selection = vis
                                   #pragma warning restore BL0005
                               })
                   .ToList();
    }

    // The <InputFile> Component is hidden, and triggered by a sepperate button.
    // That way we get it's functionality, without the styling limitations of the InputFile component
    private async Task OpenUpload()
    {
        await _module.InvokeVoidAsync("openUpload", UploadInput.Element);
    }

    public async Task OpenDialogRedraft(NoteResponse note)
    {
        await ResetState();
        NoteDraft.Text       = note.Text ?? "";
        NoteDraft.Cw         = note.Cw;
        NoteDraft.Visibility = note.Visibility;
        NoteDraft.MediaIds   = note.Attachments.Select(p => p.Id).ToList();
        NoteDraft.RenoteId   = note.RenoteId;
        NoteDraft.ReplyId    = note.ReplyId;
        UploadingFiles       = 0;
        StateHasChanged();
        await _module.InvokeVoidAsync("openDialog", Dialog);
    }

    public async Task OpenDialog(NoteBase? replyTo = null, NoteBase? quote = null)
    {
        var settings = await Settings.GetUserSettingsAsync();
        if (replyTo != null)
        {
            var mentions = await EnumerateMentions(replyTo);
            await ResetState();
            ReplyOrQuote         = replyTo;
            NoteDraft.ReplyId    = replyTo.Id;
            NoteDraft.Visibility = settings.DefaultNoteVisibility > replyTo.Visibility ? settings.DefaultNoteVisibility : replyTo.Visibility;
            NoteDraft.Cw         = replyTo.Cw;
            TextPlaceholder      = AvailablePlaceholders["reply"];
            foreach (var el in mentions)
            {
                NoteDraft.Text += $"@{el} ";
            }
        }
        else if (quote != null)
        {
            await ResetState();
            ReplyOrQuote         = quote;
            NoteDraft.RenoteId   = quote.Id;
            NoteDraft.Visibility = settings.DefaultNoteVisibility > quote.Visibility ? settings.DefaultNoteVisibility : quote.Visibility;
            TextPlaceholder      = AvailablePlaceholders["quote"];
        }
        else
        {
            await ResetState();
        }

        StateHasChanged();

        await _module.InvokeVoidAsync("openDialog", Dialog);
    }

    private async Task<List<string>> EnumerateMentions(NoteBase noteBase)
    {
        List<string> mentions = [];
        if (noteBase.User.Id != SessionService.Current!.Id)
        {
            var userMention = noteBase.User.Username;
            if (noteBase.User.Host != null)
            {
                userMention += $"@{noteBase.User.Host}";
            }

            mentions.Add(userMention);
        }

        var instance = await MetadataService.Instance.Value;
        var mfmNodes = noteBase.Text != null ? MfmParser.Parse(noteBase.Text) : [];
        foreach (var node in mfmNodes)
        {
            if (node is MfmMentionNode mentionNode)
            {
                mentions.Add(mentionNode.Acct.Replace($"@{instance.AccountDomain}", ""));
            }
        }

        mentions = mentions.Distinct().ToList();
        mentions.Remove(SessionService.Current.Username);
        return mentions;
    }

    private async Task ResetState()
    {
        var settings = await Settings.GetUserSettingsAsync();
        ReplyOrQuote     = null;
        Attachments      = new List<DriveFileResponse>();
        NoteDraft        = new NoteCreateRequest { Text = "", Visibility = settings.DefaultNoteVisibility, Cw = null };
        TextPlaceholder  = AvailablePlaceholders["default"];
        SendButton.State = StateButton.StateEnum.Initial;
        AttachedQuote    = null;
    }

    private async Task CloseDialog()
    {
        await _module.InvokeVoidAsync("closeDialog", Dialog);
    }

    private async Task SendNote()
    {
        if (SendLock) return;
        SendLock         = true;
        SendButton.State = StateButton.StateEnum.Loading;
        if (Attachments.Count > 0)
        {
            NoteDraft.MediaIds = Attachments.Select(x => x.Id).ToList();
        }

        NoteDraft.RenoteId ??= AttachedQuote;

        if (NoteDraft.Poll != null)
        {
            NoteDraft.Poll.Choices = PollChoices.Select(p => p.Value).Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
            NoteDraft.Poll.ExpiresAt = PollExpires switch
            {
                PollExpire.ExpiresAt => PollExpireTime.ToUniversalTime(),
                PollExpire.ExpiresAfter => PollExpiresAft switch
                {
                    PollExpireAfter.Minutes => DateTime.UtcNow.AddMinutes(Math.Min(PollExpireLen, 5)),
                    PollExpireAfter.Hours   => DateTime.UtcNow.AddHours(Math.Min(PollExpireLen, 1)),
                    PollExpireAfter.Days    => DateTime.UtcNow.AddDays(Math.Min(PollExpireLen, 1)),
                    _                       => throw new ArgumentOutOfRangeException()
                },
                _ => null
            };
        }

        try
        {
            await ApiService.Notes.CreateNoteAsync(NoteDraft);
        }
        catch (ApiException e)
        {
            SendButton.State = StateButton.StateEnum.Failed;
            await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred while posting"], NoticeDialog.NoticeType.Error)!;
            return;
        }

        if (ReplyOrQuote != null)
        {
            await NoteActions.RefetchNoteAsync(ReplyOrQuote.Id);
        }

        SendButton.State = StateButton.StateEnum.Success;
        await CloseDialog();
        SendLock         = false;
        SendButton.State = StateButton.StateEnum.Initial;
        // FIXME: Implement timeline refresh and call it here.
    }
    
    private void TogglePoll()
    {
        if (NoteDraft.Poll != null)
        {
            NoteDraft.Poll = null;
        }
        else
        {
            NoteDraft.Poll = new PollRequest
            {
                ExpiresAt = null,
                Multiple  = false,
                Choices   = []
            };
            PollChoices    = [];
            PollEntry      = "";
            PollExpires    = PollExpire.Never;
            PollExpireTime = DateTime.Now;
            PollExpireLen  = 7;
            PollExpiresAft = PollExpireAfter.Days;
        }
    }

    private void ToggleCw()
    {
        NoteDraft.Cw = NoteDraft.Cw == null ? "" : null;
    }

    private async Task Upload(InputFileChangeEventArgs e)
    {
        UploadingFiles += 1;
        var res = await ApiService.Drive.UploadFileAsync(e.File);
        Attachments.Add(res);
        UploadingFiles -= 1;
    }

    private void RemoveAttachment(string id)
    {
        var attachment = Attachments.FirstOrDefault(p => p.Id == id);
        if (attachment == null) return;
        Attachments.Remove(attachment);
        StateHasChanged();
    }

    private void AddPollChoice()
    {
        if (string.IsNullOrWhiteSpace(PollEntry)) return;
        PollChoices.Add(new Choice { Value = PollEntry });
        PollEntry = "";
    }

    private void DeletePollChoice(Choice choice)
    {
        PollChoices.Remove(choice);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _module = await Js.InvokeAsync<IJSObjectReference>("import",
                                                               "./Components/Compose.razor.js");
            EmojiList = await EmojiService.GetEmojiAsync();

            var instance = await MetadataService.Instance.Value;
            NoteLength = instance.Limits.NoteLength;

            ComposeService.ComposeDialog = this;
        }
    }

    private void ToggleEmojiPicker()
    {
        GlobalComponentSvc.EmojiPicker?.Open(EmojiButton, new EventCallback<EmojiResponse>(this, AddEmoji));
    }

    private async Task AddQuote() =>
        await GlobalComponentSvc.PromptDialog?.Prompt(new EventCallback<string?>(this, AddQuoteCallback), Loc["Add quote"], Loc["Link to note"], "")!;

    private async Task AddQuoteCallback(string? url)
    {
        if (url == null) return;

        try
        {
            var res = await ApiService.Search.LookupAsync(url);
            if (res != null)
            {
                if (!res.TargetUrl.StartsWith("/notes/"))
                {
                    await GlobalComponentSvc.NoticeDialog?.Display(Loc["You cannot quote a user profile"], NoticeDialog.NoticeType.Error)!;
                    return;
                }

                AttachedQuote = res.TargetUrl[7..];
            }
        }
        catch (ApiException e)
        {
            await GlobalComponentSvc.NoticeDialog?.Display(e.Response.Message ?? Loc["An unknown error occurred"], NoticeDialog.NoticeType.Error)!;
        }
    }

    private void RemoveQuote()
    {
        AttachedQuote = null;
    }

    private async Task AddEmoji(EmojiResponse emoji)
    {
        var pos         = await _module.InvokeAsync<int>("getSelectionStart", Textarea);
        var text        = NoteDraft.Text;
        var emojiString = $":{emoji.Name}: ";
        NoteDraft.Text = text.Insert(pos, emojiString);
        StateHasChanged();
    }

    private async Task AddInlineMedia(string url)
    {
        var pos         = await _module.InvokeAsync<int>("getSelectionStart", Textarea);
        var text        = NoteDraft.Text;
        var mediaString = $"$[media {url} ] ";
        NoteDraft.Text = text.Insert(pos, mediaString);
        StateHasChanged();
    }

    protected override void OnInitialized()
    {
        TextPlaceholder = AvailablePlaceholders["default"];
    }
}
