using Microsoft.Win32;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace Json2CSharpCodeGenerator.Lib.WinForms;

public partial class MainForm : Form
{
    private bool _PreventReentrancy = false;

    public MainForm()
    {
        // `IconTitleFont` is what WinForms *should* be using by default.
        // Need to set `Font` first, before `InitializeComponent();` to ensure font inheritance by controls in the form.
        Font = SystemFonts.IconTitleFont;

        InitializeComponent();

        ResetFonts();

        // Also: https://docs.microsoft.com/en-us/dotnet/desktop/winforms/how-to-respond-to-font-scheme-changes-in-a-windows-forms-application?view=netframeworkdesktop-4.8
        SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;

        //

        openButton.Click += OpenButton_Click;

        optAttribJP.CheckedChanged += OnAttributesModeCheckedChanged;
        optAttribJpn.CheckedChanged += OnAttributesModeCheckedChanged;
        //optAttribNone        .CheckedChanged += OnAttributesModeCheckedChanged;

        optMemberFields.CheckedChanged += OnMemberModeCheckedChanged;
        optMemberProps.CheckedChanged += OnMemberModeCheckedChanged;

        optTypesMutablePoco.CheckedChanged += OnOutputTypeModeCheckedChanged;
        optTypesImmutablePoco.CheckedChanged += OnOutputTypeModeCheckedChanged;
        optTypesRecords.CheckedChanged += OnOutputTypeModeCheckedChanged;

        optsPascalCase.CheckedChanged += OnOptionsChanged;

        wrapText.CheckedChanged += WrapText_CheckedChanged;

        copyOutput.Click += CopyOutput_Click;
        copyOutput.Enabled = false;

        jsonInputTextbox.TextChanged += JsonInputTextbox_TextChanged;
        jsonInputTextbox.DragDrop += JsonInputTextbox_DragDrop;
        jsonInputTextbox.DragOver += JsonInputTextbox_DragOver;
        //jsonInputTextbox.paste // annoyingly, it isn't (easily) feasible to hook/detect TextBox paste events, even globally... grrr.

        // Invoke event-handlers to set initial toolstrip text:
        optsAttributeMode.Tag = optsAttributeMode.Text + ": {0}";
        optMembersMode.Tag = optMembersMode.Text + ": {0}";
        optTypesMode.Tag = optTypesMode.Text + ": {0}";

        OnAttributesModeCheckedChanged(optAttribJP, EventArgs.Empty);
        OnMemberModeCheckedChanged(optMemberProps, EventArgs.Empty);
        OnOutputTypeModeCheckedChanged(optTypesMutablePoco, EventArgs.Empty);
    }

    private void WrapText_CheckedChanged(object sender, EventArgs e)
    {
        ToolStripButton tsb = (ToolStripButton)sender;

        // For some reason, toggling WordWrap causes a text selection in `jsonInputTextbox`. So, doing this:
        try
        {
            jsonInputTextbox.HideSelection = true;
            // ayayayay: https://stackoverflow.com/questions/1140250/how-to-remove-the-focus-from-a-textbox-in-winforms
            ActiveControl = toolStrip;

#if WINFORMS_TEXTBOX_GET_SCROLL_POSITION_WORKS_ARGH // It's non-trivial: https://stackoverflow.com/questions/4494162/change-scrollbar-position-in-textbox
            //int idx1 = jsonInputTextbox.GetFirstCharIndexOfCurrentLine(); // but what is the "current line"?
            int firstLineCharIndex = -1;
            if( jsonInputTextbox.Height > 10 )
            {
                // https://stackoverflow.com/questions/10175400/maintain-textbox-scroll-position-while-adding-line
                jsonInputTextbox.GetCharIndexFromPosition( new Point( 3, 3 ) );
            }
#endif

            jsonInputTextbox.WordWrap = tsb.Checked;
            csharpOutputTextbox.WordWrap = tsb.Checked;

#if WINFORMS_TEXTBOX_GET_SCROLL_POSITION_WORKS_ARGH
            if( firstLineCharIndex > 0 ) // Greater than zero, not -1, because `GetCharIndexFromPosition` returns a meaningless zero sometimes.
            {
                jsonInputTextbox.SelectionStart = firstLineCharIndex;
                jsonInputTextbox.ScrollToCaret();
            }
#endif
        }
        finally
        {
            jsonInputTextbox.HideSelection = false;
        }
    }

    #region WinForms Taxes

    private static Font GetMonospaceFont(float emFontSizePoints)
    {
        // See if Consolas or Lucida Sans Typewriter is available before falling-back:
        string[] preferredFonts = new[] { "Consolas", "Lucida Sans Typewriter" };
        foreach (string fontName in preferredFonts)
        {
            if (TestFont(fontName, emFontSizePoints))
            {
                return new Font(fontName, emFontSizePoints, FontStyle.Regular);
            }
        }

        // Fallback:
        return new Font(FontFamily.GenericMonospace, emSize: emFontSizePoints);
    }

    private static bool TestFont(string fontName, float emFontSizePoints)
    {
        try
        {
            using Font test = new(fontName, emFontSizePoints, FontStyle.Regular);
            return test.Name == fontName;
        }
        catch
        {
            return false;
        }
    }

    private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
    {
        switch (e.Category)
        {
            case UserPreferenceCategory.Accessibility:
            case UserPreferenceCategory.Window:
            case UserPreferenceCategory.VisualStyle:
            case UserPreferenceCategory.Menu:
                ResetFonts();
                break;
        }
    }

    private void ResetFonts()
    {
        Font = SystemFonts.IconTitleFont;

        Font monospaceFont = GetMonospaceFont(emFontSizePoints: SystemFonts.IconTitleFont.SizeInPoints);
        jsonInputTextbox.Font = monospaceFont;
        csharpOutputTextbox.Font = monospaceFont;
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        base.OnFormClosing(e);

        if (!e.Cancel)
        {
            SystemEvents.UserPreferenceChanged -= new UserPreferenceChangedEventHandler(SystemEvents_UserPreferenceChanged);
        }
    }

    #endregion

    #region Methods to ensure only a single checkbox-style menu item is checked at-a-time, and that the ToolStripDropDownButton's text indicates the currently selected option:

    private void OnAttributesModeCheckedChanged(object sender, EventArgs e)
    {
        EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText((ToolStripMenuItem)sender, defaultItem: optAttribJP, parent: optsAttributeMode);

        GenerateCSharp();
    }

    private void OnMemberModeCheckedChanged(object sender, EventArgs e)
    {
        EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText((ToolStripMenuItem)sender, defaultItem: optMemberProps, parent: optMembersMode);

        GenerateCSharp();
    }

    private void OnOutputTypeModeCheckedChanged(object sender, EventArgs e)
    {
        EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText((ToolStripMenuItem)sender, defaultItem: optTypesMutablePoco, parent: optTypesMode);

        GenerateCSharp();
    }

    private void EnsureSingleCheckedDropDownItemAndUpdateToolStripItemText(ToolStripMenuItem subject, ToolStripMenuItem defaultItem, ToolStripDropDownButton parent)
    {
        if (_PreventReentrancy)
            return;
        try
        {
            _PreventReentrancy = true;

            ToolStripMenuItem singleCheckedItem;
            if (subject.Checked)
            {
                singleCheckedItem = subject;
                UncheckOthers(subject, parent);
            }
            else
            {
                EnsureAtLeast1IsCheckedAfterItemWasUnchecked(subject, defaultItem, parent);
                singleCheckedItem = parent.DropDownItems.Cast<ToolStripMenuItem>().Single(item => item.Checked);
            }

            string parentTextFormat = (string)parent.Tag;
            parent.Text = string.Format(format: parentTextFormat, arg0: singleCheckedItem.Text);
        }
        finally
        {
            _PreventReentrancy = false;
        }
    }

    private static void UncheckOthers(ToolStripMenuItem sender, ToolStripDropDownButton parent)
    {
        foreach (ToolStripMenuItem menuItem in parent.DropDownItems.Cast<ToolStripMenuItem>()) // I really hate old-style IEnumerable, *grumble*
        {
            if (!ReferenceEquals(menuItem, sender))
            {
                menuItem.Checked = false;
            }
        }
    }

    private static void EnsureAtLeast1IsCheckedAfterItemWasUnchecked(ToolStripMenuItem subject, ToolStripMenuItem defaultItem, ToolStripDropDownButton parent)
    {
        int countChecked = parent.DropDownItems.Cast<ToolStripMenuItem>().Count(item => item.Checked);

        if (countChecked == 1)
        {
            // Is exactly 1 checked already? If so, then NOOP.
        }
        else if (countChecked > 1)
        {
            // If more than 1 are checked, then check only the default:
            defaultItem.Checked = true;
            UncheckOthers(sender: defaultItem, parent);
        }
        else
        {
            // If none are checked, then *if* the unchecked item is NOT the default item, then check the default item:
            if (!ReferenceEquals(subject, defaultItem))
            {
                defaultItem.Checked = true;
            }
            else
            {
                // Otherwise, check the first non-default item:
                ToolStripMenuItem nextBestItem = parent.DropDownItems.Cast<ToolStripMenuItem>().First(item => item != defaultItem);
                nextBestItem.Checked = true;
            }
        }
    }

    #endregion

    private void OnOptionsChanged(object sender, EventArgs e) => GenerateCSharp();

    #region Drag and Drop

    private void JsonInputTextbox_DragOver(object sender, DragEventArgs e)
    {
        bool acceptable =
            e.Data.GetDataPresent(DataFormats.FileDrop) ||
            //              e.Data.GetDataPresent( DataFormats.Text ) ||
            //              e.Data.GetDataPresent( DataFormats.OemText ) ||
            e.Data.GetDataPresent(DataFormats.UnicodeText, autoConvert: true)// ||
                                                                             //              e.Data.GetDataPresent( DataFormats.Html ) ||
                                                                             //              e.Data.GetDataPresent( DataFormats.StringFormat ) ||
                                                                             //              e.Data.GetDataPresent( DataFormats.Rtf )
        ;

        if (acceptable)
        {
            e.Effect = DragDropEffects.Copy;
        }
        else
        {
            e.Effect = DragDropEffects.None;
        }
    }

    private void JsonInputTextbox_DragDrop(object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop))
        {
            string[] fileNames = (string[])e.Data.GetData(DataFormats.FileDrop);
            if (fileNames.Length >= 1)
            {
                // hmm, maybe allow multiple files by concatenating them all into a horrible JSON array? :D
                TryLoadJsonFile(fileNames[0]);
            }
        }
        else if (e.Data.GetDataPresent(DataFormats.UnicodeText, autoConvert: true))
        {
            statusStrip.Text = "";

            string text = (string)e.Data.GetData(DataFormats.UnicodeText, autoConvert: true);
            if (text != null)
            {
                jsonInputTextbox.Text = text; // This will invoke `GenerateCSharp()`.

                statusStrip.Text = "Loaded JSON from drag and drop data.";
            }
        }
    }

    /// <summary>This regex won't match <c>\r\n</c>, only <c>\n</c>.</summary>
    private static readonly Regex _OnlyUnixLineBreaks = new("(?<!\r)\n", RegexOptions.Compiled); // Don't use `[^\r]?\n` because it *will* match `\r\n`, and don't use `[^\r]\n` because it won't match a leading `$\n` in a file.

    private static string RepairLineBreaks(string text)
    {
        if (_OnlyUnixLineBreaks.IsMatch(text))
        {
            return _OnlyUnixLineBreaks.Replace(text, replacement: "\r\n");
        }

        return text;
    }

    #endregion

    #region Open JSON file

    private void OpenButton_Click(object sender, EventArgs e)
    {
        if (ofd.ShowDialog(owner: this) == DialogResult.OK)
        {
            TryLoadJsonFile(ofd.FileName);
        }
    }

    private void TryLoadJsonFile(string filePath)
    {
        if (string.IsNullOrWhiteSpace(filePath))
        {
            csharpOutputTextbox.Text = "Error: an empty file path was specified.";
        }
        //          else if ( filePath.IndexOfAny( Path.GetInvalidFileNameChars() ) > -1 )
        //          {
        //              const String fmt = "Invalid file path: \"{0}\"";
        //              csharpOutputTextbox.Text = String.Format( CultureInfo.CurrentCulture, fmt, filePath );
        //          }
        else
        {
            FileInfo jsonFileInfo;
            try
            {
                jsonFileInfo = new FileInfo(filePath);
            }
            catch (Exception ex)
            {
                const string fmt = "Invalid file path: \"{0}\"\r\n{1}";
                csharpOutputTextbox.Text = string.Format(CultureInfo.CurrentCulture, fmt, filePath, ex.ToString());
                return;
            }

            TryLoadJsonFile(jsonFileInfo);
        }
    }

    private void TryLoadJsonFile(FileInfo jsonFile)
    {
        if (jsonFile is null)
            return;

        statusStrip.Text = "";

        try
        {
            jsonFile.Refresh();
            if (jsonFile.Exists)
            {
                string jsonText = File.ReadAllText(jsonFile.FullName);
                jsonInputTextbox.Text = jsonText; // This will invoke `GenerateCSharp()`.

                statusStrip.Text = "Loaded \"" + jsonFile.FullName + "\" successfully.";
            }
            else
            {
                csharpOutputTextbox.Text = string.Format(CultureInfo.CurrentCulture, "Error: File \"{0}\" does not exist.", jsonFile.FullName);
            }
        }
        catch (Exception ex)
        {
            const string fmt = "Error loading file: \"{0}\"\r\n{1}";

            csharpOutputTextbox.Text = string.Format(CultureInfo.CurrentCulture, fmt, jsonFile.FullName, ex.ToString());
        }
    }

    #endregion

    private void ConfigureGenerator(IJsonClassGeneratorConfig config)
    {
        config.UsePascalCase = optsPascalCase.Checked;
        config.UseThisKeyWord = false;
        //

        if (optAttribJP.Checked)
        {
            config.AttributeLibrary = JsonLibrary.NewtonsoftJson;
        }
        else// implicit: ( optAttribJpn.Checked )
        {
            config.AttributeLibrary = JsonLibrary.SystemTextJson;
        }

        //

        if (optMemberProps.Checked)
        {
            config.MutableClasses.Members = OutputMembers.AsProperties;
        }
        else// implicit: ( optMemberFields.Checked )
        {
            config.MutableClasses.Members = OutputMembers.AsPublicFields;
        }

        //

        if (optTypesImmutablePoco.Checked)
        {
            config.OutputType = OutputTypes.ImmutableClass;
        }
        else if (optTypesMutablePoco.Checked)
        {
            config.OutputType = OutputTypes.MutableClass;
        }
        else// implicit: ( optTypesRecords.Checked )
        {
            config.OutputType = OutputTypes.ImmutableRecord;
        }
    }

    private void JsonInputTextbox_TextChanged(object sender, EventArgs e)
    {
        if (_PreventReentrancy)
            return;
        _PreventReentrancy = true;
        try
        {
            jsonInputTextbox.Text = RepairLineBreaks(jsonInputTextbox.Text);

            GenerateCSharp();
        }
        finally
        {
            _PreventReentrancy = false;
        }
    }

    private void GenerateCSharp()
    {
        copyOutput.Enabled = false;

        string jsonText = jsonInputTextbox.Text;
        if (string.IsNullOrWhiteSpace(jsonText))
        {
            csharpOutputTextbox.Text = string.Empty;
            return;
        }

        JsonClassGenerator generator = new();
        ConfigureGenerator(generator);

        try
        {
            StringBuilder sb = generator.GenerateClasses(jsonText, errorMessage: out string errorMessage);
            if (!string.IsNullOrWhiteSpace(errorMessage))
            {
                csharpOutputTextbox.Text = "Error:\r\n" + errorMessage;
            }
            else
            {
                csharpOutputTextbox.Text = sb.ToString();
                copyOutput.Enabled = true;
            }
        }
        catch (Exception ex)
        {
            csharpOutputTextbox.Text = "Error:\r\n" + ex.ToString();
        }
    }

    private void CopyOutput_Click(object sender, EventArgs e)
    {
        if (csharpOutputTextbox.Text?.Length > 0)
        {
            Clipboard.SetText(csharpOutputTextbox.Text, TextDataFormat.UnicodeText);
        }
    }
}