@page "/Learn"
@using System.Text;
@using CCharLearn.ExtensionMethods;
@using UnidecodeSharpFork;

@inject NavigationManager navigator
@inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject HttpClient httpClient
@inject ISnackbar Snackbar
@inject SpeechSynthesis SpeechSynthesis

<style>
    .LargeCharecter{
        width: 0;
        height: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 125px;
    }

    .PinyinButtons{
        width: 125px;
        font-size: 20px;
    }
</style>

<MudContainer Class="justify-center d-flex ma-0 overflow-hidden" Style="height: 100%;">
    <MudStack Class="d-flex">
        @*True if debug, false for not debug. (TODO: Use an extensíonMethod: https://stackoverflow.com/questions/4696175/preprocessor-directives-in-razor)*@
        @if (false)
        {
            <MudStack Row=true Class="pt-2 d-flex justify-center">
                @if (!Answers.Any(x => x == null))
                {
                    <MudButton Disabled="@(!(Answers.Any(x=>x.isSelected)))" OnClick="()=>RemoveCCharFromSelection()" Variant="Variant.Outlined">Avoid</MudButton>
                }
            </MudStack>
        }
        <MudContainer Class="pt-12">
            <MudPaper Class="pa-16 ma-2 rounded-xl mud-dark" Elevation="1">
                <MudContainer Style="width: 100px; height: 100px" Class="pa-8 ma-4 d-flex justify-center align-center">
                    @if (!Answers.Any(x => x == null))
                    {
                        if (useChagingFonts)
                        {
                            <p class="LargeCharecter" style="font-family: '@currentFont';">@GetDisplayChar()</p>
                        }
                        else
                        {
                            <p class="LargeCharecter">@GetDisplayChar()</p>
                        }
                    }
                </MudContainer>
            </MudPaper>
        </MudContainer>
        <MudContainer Class="pa-8 pt-10 justify-center align-center d-flex"
        Style="max-width:300px">
            <MudGrid Spacing="10" Class="align-center justify-center d-flex">
                @if (!Answers.Any(x=>x == null))
                {
                    @for (int i = 0; i < Answers.Length; i++)
                    {
                        int buttonIndex = i;
                        <MudButton Class="PinyinButtons ma-3"
                           Variant="Variant.Outlined"
                           @onclick="() => SelectButton(buttonIndex)"
                           Color="@(Answers[buttonIndex].isSelected ? Color.Primary : Color.Default)">
                           <MudText>
                                @Answers[buttonIndex].cchar.pinyin.ToTitleCase()
                           </MudText>
                       </MudButton>
                    }
                }
            </MudGrid>
        </MudContainer>
        <MudSpacer/>
        <MudContainer Class="justify-center d-flex py-4">
            <MudStack Row=true>
                <MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="ShowMeaning">Meaning</MudButton>
                <MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="SayCorrectChar">Speech</MudButton>
            </MudStack>
        </MudContainer>
        <MudContainer Class="pb-8 justify-center align-center d-flex">
            <MudStack Spacing="0">
                @if (!Answers.Any(x => x == null))
                {
                    <MudButton Disabled="@(!(Answers.Any(x=>x.isSelected)))" OnClick="Submit" Class="px-8 py-3" Variant="Variant.Filled" Size="Size.Large" Color="Color.Success" Style="font-size: 20px;"> Submit</MudButton>
                }
                @if (ignoreTone)
                {
                    <MudText Typo="Typo.caption">• Easy mode enabled</MudText>
                }
            </MudStack>
        </MudContainer>
    </MudStack>
</MudContainer>

@*Used to load all fonts into memory instead of loading from http each time*@
@if (useChagingFonts && !hasLoadedFontsToMemory)
{
    @for (int i = 0; i < extraFonts.Length; i++)
    {
        <p style="font-family: '@extraFonts[i]';">i</p>
    }
}

@code {
    bool isSavedLocally = false;
    bool ignoreTone = false;

    bool selectedCorrect = false;

    bool useChagingFonts = false;
    string? currentFont = null;
    string[] extraFonts = { "HanyiSentyRubber", "mini-jian-caocuyuan", "myoungheihk", "UnboundedSans", "wt064", "XiaolaiSC-Regular", "chinese1", "chinese2" };
    bool hasLoadedFontsToMemory = false;

    public Answer[] Answers = new Answer[4];
    public void SelectButton(int selectedIndex)
    {
        for (int i = 0; i < Answers.Length; i++)
        {
            Answers[i].isSelected = (i == selectedIndex);
        }

        selectedCorrect = Answers[selectedIndex].isCorrect;
    }

    private CChar[]? _charecters;
    public CChar[]? Charecters
    {
        get
        {
            if (_charecters == null) throw new Exception("Loaded dataset empty?");

            return _charecters;
        }
        set { _charecters = value; }
    }

    public List<CCharStats>? DontSkipTheseCChar;

    private async Task<string> GetFileContentsAsync(string filePath)
    {
        var fileResponse = await httpClient.GetByteArrayAsync(filePath);
        return Encoding.UTF8.GetString(fileResponse);
    }

    protected override async Task OnInitializedAsync()
    {
        Program.UpdateUiEvent += OnUiUpdate;
        if (await localStorage.ContainKeyAsync("IgnoreTone"))
        {
            ignoreTone = await localStorage.GetItemAsync<bool>("IgnoreTone");
        }

        if (await localStorage.ContainKeyAsync("ChangingFonts"))
        {
            useChagingFonts = await localStorage.GetItemAsync<bool>("ChangingFonts");
        }

        bool continueLast = 
               await localStorage.ContainKeyAsync("ContinueLearning")
            && await localStorage.ContainKeyAsync("ContinueData")
            && await localStorage.GetItemAsync<bool>("ContinueLearning");

        if (continueLast)
        {
            await LoadCharectersFromContinue();
        }
        else
        {
            await LoadCharectersFromChunk();            
        }

        if (ignoreTone)
            DontSkipTheseCChar.ForEach(x => x.cchar.pinyin = x.cchar.pinyin.Unidecode());

        if (useChagingFonts)
        {
            ChangeFont();
        }

        Program.CCharsLeft = DontSkipTheseCChar.Count;
        Program.InvokeUiUpdate();

        GenerateQuestion();

        await Task.Delay(100);
        hasLoadedFontsToMemory = true;
    }

    async Task LoadCharectersFromChunk()
    {
        isSavedLocally = await localStorage.ContainKeyAsync("Normalized_chunk_001.json");

        int selectedChunk = await localStorage.GetItemAsync<int>("SelectedChunk");

        if (!isSavedLocally)
            Charecters = await httpClient.GetFromJsonAsync<CChar[]>($"Data/Normalized_chunk_{selectedChunk.ToString("000")}.json");
        else
        {
            string json = await localStorage.GetItemAsync<string>($"Normalized_chunk_{selectedChunk.ToString("000")}.json");
            Charecters = JsonConvert.DeserializeObject<CChar[]>(json);
        }

        DontSkipTheseCChar = Charecters.Select(x => new CCharStats(x)).ToList();
    }

    async Task LoadCharectersFromContinue()
    {
        string continueDataJson = await localStorage.GetItemAsStringAsync("ContinueData");
        CCharStats[] charectersLeft = JsonConvert.DeserializeObject<CCharStats[]>(continueDataJson);

        DontSkipTheseCChar = charectersLeft.ToList();
    }

    void OnUiUpdate() => StateHasChanged();

    void GenerateQuestion()
    {
        if (DontSkipTheseCChar.Count < 5) 
        {
            FinishAndKickBackToLearn();
            return;
        }

        int correctIndex = Random.Shared.Next(0, Answers.Length);

        for (int i = 0; i < Answers.Length; i++)
        {
            bool isCorrect = i == correctIndex;
            CChar randomCChar;

        repickRandomCChar:
            randomCChar = DontSkipTheseCChar[Random.Shared.Next(0, DontSkipTheseCChar.Count)].cchar;
            if (Answers.Any(x =>x != null && x.cchar == randomCChar)) goto repickRandomCChar;

            Answers[i] = new Answer(randomCChar, isCorrect);
        }

        string correctPinyin = GetCorrectCChar().pinyin;

        for (int i = 0; i < Answers.Length; i++)
        {
            if (Answers[i].isCorrect) continue;
            if (Answers[i].cchar.pinyin != correctPinyin) continue;

            Answers[i].isCorrect = true;
        }

        StateHasChanged();
    }

    async void FinishAndKickBackToLearn()
    {
        Snackbar.Add("Congrats, you have compleated this chunk!", Severity.Success, config => 
        { 
            config.RequireInteraction = true;
            config.CloseAfterNavigation = false;
        });
        Program.UpdateUiEvent -= OnUiUpdate;
        Program.CCharsLeft = 0;

        await localStorage.RemoveItemAsync("ContinueData");

        navigator.NavigateTo("");
    }

    async void ChangeFont()
    {
        int randomIndex = Random.Shared.Next(0, extraFonts.Length);
        currentFont = extraFonts[randomIndex];
    }

    async void Submit()
    {
        bool isCorrect = Answers.Any(x => x.isCorrect && x.isSelected);
        CCharStats correctCCharStats = GetCorrectCCharStats();
        CChar correctCChar = correctCCharStats.cchar;

        if (isCorrect)
        {
            Snackbar.Add($"<b>Definition:</b> {correctCChar.definition}", Severity.Success, config => { config.VisibleStateDuration = 1000; });
            increaseCCharStat(GetCorrectCCharStats(), StatType.NumOfCorrects);
            ChangeFont();
        }
        else
        {
            Snackbar.Add(   
                @<div>
                    <h3>Incorrect!</h3>
                    <ul>
                        <li>CChar: @correctCChar.charcter</li>
                        <li>Correct answer: @correctCChar.pinyin.ToTitleCase()</li>
                        <li>Meaning: @correctCChar.definition</li>
                    </ul>
        </div>
    , Severity.Error);
            increaseCCharStat(GetCorrectCCharStats(), StatType.NumOfWrongs);
        }

        if (correctCCharStats.TotalAnswers >= 3)
        {
            if (correctCCharStats.Accuracy > 0.7f)
            {
                RemoveCCharFromSelection(true);

                Snackbar.Add($"CChar '{correctCChar.charcter}' compleated!", Severity.Success);
            }
        }

        await SaveContinueData();

        GenerateQuestion();
    }

    async Task SaveContinueData()
    {
        CCharStats[] charectersLeft = DontSkipTheseCChar.ToArray();
        string continueDataJson = JsonConvert.SerializeObject(charectersLeft);

        await localStorage.SetItemAsStringAsync("ContinueData", continueDataJson);
        await localStorage.SetItemAsync("ContinueCharectersLeft", charectersLeft.Length);
    }

    public class Answer
    {
        public Answer()
        {
        }

        public Answer(CChar cchar, bool isCorrect)
        {
            this.cchar = cchar;
            this.isCorrect = isCorrect;
        }

        public CChar cchar { get; set; }
        public bool isCorrect { get; set; } = false;
        public bool isSelected { get; set; } = false;
    }

    public char GetDisplayChar()
    {
        char? cc = GetCorrectCChar().charcter;
        if (cc == null) return ' ';
        else return (char)cc;
    }

    public CChar GetCorrectCChar()
    {
        CChar cchar = Answers.FirstOrDefault(x => x.isCorrect)?.cchar;
        return cchar;
    }

    public CCharStats GetCorrectCCharStats()
    {
        CChar correctChar = GetCorrectCChar();
        CCharStats correctCCharStats = DontSkipTheseCChar.First(x => x.cchar == correctChar);
        return correctCCharStats;
    }

    public void increaseCCharStat(CCharStats stats, StatType statType)
    {
        int correctStatsIndex = DontSkipTheseCChar.IndexOf(stats);
        switch (statType)
        {
            case StatType.NumOfCorrects:
                DontSkipTheseCChar[correctStatsIndex].NumOfCorrects++;
                break;

            case StatType.NumOfWrongs:
                DontSkipTheseCChar[correctStatsIndex].NumOfWrongs++;
                break;
        }
    }

    public async void RemoveCCharFromSelection(bool ignoreSelection = false)
    {
        CCharStats correctCCharStats = GetCorrectCCharStats();


        if (!selectedCorrect && !ignoreSelection)
        {
            Snackbar.Add("Selected is wrong. Try again!", Severity.Error);
            return;
        }
        else if (!ignoreSelection)// Hacky way to only display this snackbar when is manually removed
        {
            Snackbar.Add($"Removed '{correctCCharStats.cchar.charcter}' from selection", Severity.Info, config => config.VisibleStateDuration = 3000);
        }

        DontSkipTheseCChar.Remove(correctCCharStats);
        Console.WriteLine("Remaining CChars: " + DontSkipTheseCChar);
        Program.CCharsLeft = DontSkipTheseCChar.Count;
        Program.InvokeUiUpdate();

        await SaveContinueData();

        GenerateQuestion();
    }

    public void ShowMeaning()
    {
        Snackbar.Add($"<b>Definition:</b> {GetCorrectCChar().definition}", Severity.Info);
    }

    public void ShowPinyin()
    {
        Snackbar.Add($"<b>Pinyin:</b> {GetCorrectCChar().pinyin.ToTitleCase()}", Severity.Info);
    }

    SpeechSynthesisVoice? SpeechVoice;

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Gets chinese voice on windows or iphone.
            this.SpeechVoice = ((IEnumerable<SpeechSynthesisVoice>)(await this.SpeechSynthesis.GetVoicesAsync())).FirstOrDefault(v => v.Name.Contains("Yaoyao") || v.Name.Contains("Ting-Ting") || v.Name.ToLower().Contains("cn"));
            this.StateHasChanged();
        }
    }

    async Task SayCorrectChar()
    {
        if (SpeechVoice == null)
        {
            Snackbar.Add("Couldn't play sound.\nLikely reason: Chinese speech not installed", Severity.Error);
            return;
        }

        var utterancet = new SpeechSynthesisUtterance
            {
                Text = this.GetCorrectCChar().charcter.ToString(),
                Voice = this.SpeechVoice,
                Rate = 0.75f
            };

        await this.SpeechSynthesis.SpeakAsync(utterancet); // 👈 Speak with "Haruka"'s voice!
    }
}
