// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Rulesets;

namespace osu.Game.Online.API.Requests.Responses
{
    public class APIBeatmap : IBeatmapInfo, IBeatmapOnlineInfo
    {
        [JsonProperty(@"id")]
        public int OnlineID { get; set; }

        [JsonProperty(@"beatmapset_id")]
        public int OnlineBeatmapSetID { get; set; }

        [JsonProperty(@"status")]
        public BeatmapOnlineStatus Status { get; set; }

        [JsonProperty("checksum")]
        public string Checksum { get; set; } = string.Empty;

        [JsonProperty(@"user_id")]
        public int AuthorID { get; set; }

        [JsonProperty(@"beatmapset")]
        public APIBeatmapSet? BeatmapSet { get; set; }

        [JsonProperty(@"playcount")]
        public int PlayCount { get; set; }

        [JsonProperty(@"current_user_playcount")]
        public int UserPlayCount { get; set; }

        [JsonProperty(@"passcount")]
        public int PassCount { get; set; }

        [JsonProperty(@"mode_int")]
        public int RulesetID { get; set; }

        [JsonProperty(@"difficulty_rating")]
        public double StarRating { get; set; }

        public int EndTimeObjectCount => SliderCount + SpinnerCount;

        public int TotalObjectCount => CircleCount + SliderCount + SpinnerCount;

        [JsonProperty(@"drain")]
        public float DrainRate { get; set; }

        [JsonProperty(@"cs")]
        public float CircleSize { get; set; }

        [JsonProperty(@"ar")]
        public float ApproachRate { get; set; }

        [JsonProperty(@"accuracy")]
        public float OverallDifficulty { get; set; }

        [JsonIgnore]
        public double Length { get; set; }

        [JsonProperty(@"total_length")]
        private double lengthInSeconds
        {
            get => TimeSpan.FromMilliseconds(Length).TotalSeconds;
            set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds;
        }

        [JsonIgnore]
        public double HitLength { get; set; }

        [JsonProperty(@"hit_length")]
        private double hitLengthInSeconds
        {
            get => TimeSpan.FromMilliseconds(HitLength).TotalSeconds;
            set => HitLength = TimeSpan.FromSeconds(value).TotalMilliseconds;
        }

        [JsonProperty(@"convert")]
        public bool Convert { get; set; }

        [JsonProperty(@"count_circles")]
        public int CircleCount { get; set; }

        [JsonProperty(@"count_sliders")]
        public int SliderCount { get; set; }

        [JsonProperty(@"count_spinners")]
        public int SpinnerCount { get; set; }

        [JsonProperty(@"version")]
        public string DifficultyName { get; set; } = string.Empty;

        [JsonProperty(@"failtimes")]
        public APIFailTimes? FailTimes { get; set; }

        [JsonProperty(@"top_tag_ids")]
        public APIBeatmapTag[]? TopTags { get; set; }

        [JsonProperty(@"current_user_tag_ids")]
        public long[]? OwnTagIds { get; set; }

        [JsonProperty(@"max_combo")]
        public int? MaxCombo { get; set; }

        [JsonProperty(@"last_updated")]
        public DateTimeOffset LastUpdated { get; set; }

        public double BPM { get; set; }

        [JsonProperty(@"owners")]
        public BeatmapOwner[] BeatmapOwners { get; set; } = Array.Empty<BeatmapOwner>();

        /// <summary>
        /// Minimum count of votes required to display a tag on the beatmap's page.
        /// Should match value specified web-side as https://github.com/ppy/osu-web/blob/cae2fdf03cfb8c30c8e332cfb142e03188ceffef/config/osu.php#L59.
        /// </summary>
        public const int MINIMUM_USER_TAG_VOTES_FOR_DISPLAY = 5;

        /// <summary>
        /// Retrieves top user tags for the beatmap, ordered in a way matching osu!web.
        /// Requires <see cref="BeatmapSet"/> to be populated.
        /// </summary>
        /// <param name="confirmedOnly">
        /// If <see langword="true"/>, only tags above <see cref="MINIMUM_USER_TAG_VOTES_FOR_DISPLAY"/> will be shown.
        /// If <see langword="false"/>, all tags regardless of vote count will be shown.
        /// </param>
        public (APITag Tag, int VoteCount)[] GetTopUserTags(bool confirmedOnly = true)
        {
            if (TopTags == null || TopTags.Length == 0 || BeatmapSet?.RelatedTags == null)
                return [];

            var tagsById = BeatmapSet.RelatedTags.ToDictionary(t => t.Id);

            return TopTags
                   .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId)))
                   .Where(t => t.relatedTag != null && (!confirmedOnly || t.topTag.VoteCount >= MINIMUM_USER_TAG_VOTES_FOR_DISPLAY))
                   // see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria
                   .OrderByDescending(t => t.topTag.VoteCount)
                   .ThenBy(t => t.relatedTag!.Name)
                   .Select(t => (t.relatedTag!, t.topTag.VoteCount))
                   .ToArray();
        }

        #region Implementation of IBeatmapInfo

        public IBeatmapMetadataInfo Metadata => (BeatmapSet as IBeatmapSetInfo)?.Metadata ?? new BeatmapMetadata();

        public IBeatmapDifficultyInfo Difficulty => new BeatmapDifficulty
        {
            DrainRate = DrainRate,
            CircleSize = CircleSize,
            ApproachRate = ApproachRate,
            OverallDifficulty = OverallDifficulty
        };

        IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;

        public string MD5Hash => Checksum;

        public IRulesetInfo Ruleset => new APIRuleset { OnlineID = RulesetID };

        [JsonIgnore]
        public string Hash => throw new NotImplementedException();

        #endregion

        public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b);

        public class APIRuleset : IRulesetInfo
        {
            public int OnlineID { get; set; } = -1;

            public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})";

            [JsonIgnore]
            public string ShortName
            {
                get
                {
                    // TODO: this should really not exist.
                    switch (OnlineID)
                    {
                        case 0: return "osu";

                        case 1: return "taiko";

                        case 2: return "fruits";

                        case 3: return "mania";

                        default: throw new ArgumentOutOfRangeException();
                    }
                }
            }

            public string InstantiationInfo => string.Empty;

            public Ruleset CreateInstance() => throw new NotImplementedException();

            public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r);

            public int CompareTo(IRulesetInfo? other)
            {
                if (!(other is APIRuleset ruleset))
                    throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other));

                return OnlineID.CompareTo(ruleset.OnlineID);
            }

            // ReSharper disable once NonReadonlyMemberInGetHashCode
            public override int GetHashCode() => OnlineID;
        }

        public class BeatmapOwner
        {
            [JsonProperty(@"id")]
            public int Id { get; set; }

            [JsonProperty(@"username")]
            public string Username { get; set; } = string.Empty;
        }
    }
}
