using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;

namespace MediaBrowser.Providers.Books.ComicInfo;

/// <summary>
/// ComicInfo reader.
/// </summary>
public static class ComicInfoReader
{
    /// <summary>
    /// Filename to check for comic metadata either next to the comic file or inside the archive.
    /// </summary>
    public const string ComicRackMetaFile = "ComicInfo.xml";

    /// <summary>
    /// Read comic book metadata.
    /// </summary>
    /// <param name="xml">The XDocument to read for comic metadata.</param>
    /// <returns>The resulting book.</returns>
    public static Book? ReadComicBookMetadata(XDocument xml)
    {
        var book = new Book();
        var hasFoundMetadata = false;

        // this value is only used internally since Jellyfin has no manga flag
        var isManga = false;

        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Title", title => book.Name = title);
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Manga", manga => isManga = manga.Equals("Yes", StringComparison.OrdinalIgnoreCase));
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Series", series => book.SeriesName = series);
        hasFoundMetadata |= ReadIntInto(xml, "ComicInfo/Number", issue => book.IndexNumber = issue);
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Summary", summary => book.Overview = summary);
        hasFoundMetadata |= ReadIntInto(xml, "ComicInfo/Year", year => book.ProductionYear = year);
        hasFoundMetadata |= ReadThreePartDateInto(xml, "ComicInfo/Year", "ComicInfo/Month", "ComicInfo/Day", dateTime => book.PremiereDate = dateTime);
        hasFoundMetadata |= ReadCommaSeparatedStringsInto(xml, "ComicInfo/Genre", genres =>
        {
            foreach (var genre in genres)
            {
                book.AddGenre(genre);
            }
        });
        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Publisher", publisher => book.SetStudios([publisher]));

        hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/AlternateSeries", title =>
        {
            if (isManga)
            {
                // Software like ComicTagger (https://github.com/comictagger/comictagger) will use
                // this field for the series name in the original language when tagging manga.
                book.OriginalTitle = title;
            }
            else
            {
                // Some US comics can be part of cross-over story arcs. This field is then used to
                // specify an alternate series.
            }
        });

        return hasFoundMetadata ? book : null;
    }

    /// <summary>
    /// Read people metadata.
    /// </summary>
    /// <param name="xml">The XDocument to read for people metadata.</param>
    /// <param name="metadataResult">The metadata result to update.</param>
    public static void ReadPeopleMetadata(XDocument xml, MetadataResult<Book> metadataResult)
    {
        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Writer", authors =>
        {
            foreach (var p in authors)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Author });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Penciller", pencillers =>
        {
            foreach (var p in pencillers)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Penciller });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Inker", inkers =>
        {
            foreach (var p in inkers)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Inker });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Letterer", letterers =>
        {
            foreach (var p in letterers)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Letterer });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/CoverArtist", artists =>
        {
            foreach (var p in artists)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.CoverArtist });
            }
        });

        ReadCommaSeparatedStringsInto(xml, "ComicInfo/Colourist", colorists =>
        {
            foreach (var p in colorists)
            {
                metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Colorist });
            }
        });
    }

    /// <summary>
    /// Read culture information.
    /// </summary>
    /// <param name="xml">the XDocument to read for metadata.</param>
    /// <param name="xPath">The path to search.</param>
    /// <param name="commitResult">The action to take after parsing all metadata.</param>
    public static void ReadCultureInfoInto(XDocument xml, string xPath, Action<CultureInfo> commitResult)
    {
        string? culture = null;

        if (!ReadStringInto(xml, xPath, value => culture = value))
        {
            return;
        }

        // culture cannot be null here as the method would have returned earlier
        commitResult(new CultureInfo(culture!));
    }

    private static bool ReadStringInto(XDocument xml, string xPath, Action<string> commitResult)
    {
        var resultElement = xml.XPathSelectElement(xPath);

        if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.Value))
        {
            commitResult(resultElement.Value);
            return true;
        }

        return false;
    }

    private static bool ReadCommaSeparatedStringsInto(XDocument xml, string xPath, Action<IEnumerable<string>> commitResult)
    {
        var resultElement = xml.XPathSelectElement(xPath);

        if (resultElement is null || string.IsNullOrWhiteSpace(resultElement.Value))
        {
            return false;
        }

        try
        {
            var splits = resultElement.Value.Split(",").Select(p => p.Trim()).ToArray();
            if (splits.Length < 1)
            {
                return false;
            }

            commitResult(splits);
            return true;
        }
        catch (ArgumentNullException)
        {
            return false;
        }
    }

    private static bool ReadIntInto(XDocument xml, string xPath, Action<int> commitResult)
    {
        var resultElement = xml.XPathSelectElement(xPath);

        if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.Value))
        {
            return ParseInt(resultElement.Value, commitResult);
        }

        return false;
    }

    private static bool ReadThreePartDateInto(XDocument xml, string yearXPath, string monthXPath, string dayXPath, Action<DateTime> commitResult)
    {
        int year = 0;
        int month = 0;
        int day = 0;
        var parsed = false;

        parsed |= ReadIntInto(xml, yearXPath, num => year = num);
        parsed |= ReadIntInto(xml, monthXPath, num => month = num);
        parsed |= ReadIntInto(xml, dayXPath, num => day = num);

        if (!parsed)
        {
            return false;
        }

        try
        {
            var dateTime = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Unspecified);

            commitResult(dateTime);
            return true;
        }
        catch (ArgumentOutOfRangeException)
        {
            return false;
        }
    }

    private static bool ParseInt(string input, Action<int> commitResult)
    {
        if (int.TryParse(input, out var parsed))
        {
            commitResult(parsed);
            return true;
        }

        return false;
    }
}
