// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play;
using osu.Game.Users;

namespace osu.Game.Screens.Select.Leaderboards
{
    /// <summary>
    /// Represents a score shown on a gameplay leaderboard.
    /// The score is expected to update itself as gameplay progresses.
    /// </summary>
    public class GameplayLeaderboardScore
    {
        /// <summary>
        /// The user playing.
        /// </summary>
        public IUser User { get; }

        /// <summary>
        /// Whether the score is being tracked.
        /// Generally understood as true when this score is the score of the local user currently playing.
        /// </summary>
        public bool Tracked { get; }

        /// <summary>
        /// The current total of the score.
        /// </summary>
        public BindableLong TotalScore { get; } = new BindableLong();

        /// <summary>
        /// The current accuracy of the score.
        /// </summary>
        public BindableDouble Accuracy { get; } = new BindableDouble();

        /// <summary>
        /// The combo of the score to display.
        /// Can be either highest combo or current combo, depending on constructor parameters.
        /// </summary>
        public BindableInt Combo { get; } = new BindableInt();

        /// <summary>
        /// Whether the user playing has quit.
        /// </summary>
        public BindableBool HasQuit { get; } = new BindableBool();

        /// <summary>
        /// An optional value to guarantee stable ordering.
        /// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
        /// </summary>
        public long TotalScoreTiebreaker { get; init; }

        /// <summary>
        /// A custom function which handles converting a score to a display score using a provided <see cref="ScoringMode"/>.
        /// </summary>
        /// <remarks>
        /// If no function is provided, <see cref="TotalScore"/> will be used verbatim.
        /// </remarks>
        public Func<ScoringMode, long> GetDisplayScore { get; set; }

        /// <summary>
        /// The colour of the team that the user playing is on, if any.
        /// </summary>
        public Colour4? TeamColour { get; init; }

        /// <summary>
        /// The initial position of the score on the leaderboard.
        /// Mostly used for cases like the local user's best score on the global leaderboard (which will not be contiguous with the other scores).
        /// </summary>
        public int? InitialPosition { get; init; }

        /// <summary>
        /// The displayed rank of the score on the leaderboard.
        /// </summary>
        public Bindable<int?> Position { get; } = new Bindable<int?>();

        /// <summary>
        /// The index of the score on the leaderboard.
        /// This differs from <see cref="Position"/> in that it is required (must always be known)
        /// and that it doesn't represent the score's position on global leaderboards.
        /// It's a property completely local to and relative to all scores provided by the managing <see cref="IGameplayLeaderboardProvider"/>.
        /// </summary>
        public Bindable<long> DisplayOrder { get; } = new BindableLong();

        public GameplayLeaderboardScore(GameplayState gameplayState, bool tracked, ComboDisplayMode comboMode)
        {
            User = gameplayState.Score.ScoreInfo.User;
            Tracked = tracked;

            var scoreProcessor = gameplayState.ScoreProcessor;
            TotalScore.BindTarget = scoreProcessor.TotalScore;
            Accuracy.BindTarget = scoreProcessor.Accuracy;
            Combo.BindTarget = comboMode == ComboDisplayMode.Current ? scoreProcessor.Combo : scoreProcessor.HighestCombo;
            GetDisplayScore = scoreProcessor.GetDisplayScore;
        }

        public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked, ComboDisplayMode comboMode)
        {
            User = user;
            Tracked = tracked;
            TotalScore.BindTarget = scoreProcessor.TotalScore;
            Accuracy.BindTarget = scoreProcessor.Accuracy;
            Combo.BindTarget = comboMode == ComboDisplayMode.Current ? scoreProcessor.Combo : scoreProcessor.HighestCombo;
            GetDisplayScore = scoreProcessor.GetDisplayScore;
        }

        public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked, ComboDisplayMode comboMode)
        {
            User = scoreInfo.User;
            Tracked = tracked;
            TotalScore.Value = scoreInfo.TotalScore;
            Accuracy.Value = scoreInfo.Accuracy;
            Combo.Value = comboMode == ComboDisplayMode.Current ? scoreInfo.Combo : scoreInfo.MaxCombo;
            TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
            GetDisplayScore = scoreInfo.GetDisplayScore;
            InitialPosition = scoreInfo.Position;
        }

        public GameplayLeaderboardScore(MultiplayerScore score, bool tracked, ComboDisplayMode comboMode)
        {
            User = score.User;
            Tracked = tracked;
            TotalScore.Value = score.TotalScore;
            Accuracy.Value = score.Accuracy;
            Combo.Value = comboMode == ComboDisplayMode.Highest ? score.MaxCombo : throw new NotSupportedException($"{comboMode} {nameof(comboMode)} is not supported.");
            TotalScoreTiebreaker = score.ID;
            GetDisplayScore = score.GetDisplayScore;
            InitialPosition = score.Position;
        }

        /// <remarks>
        /// Used for testing.
        /// </remarks>
        internal GameplayLeaderboardScore(IUser user, bool tracked, Bindable<long> displayScore)
        {
            User = user;
            Tracked = tracked;
            TotalScore.BindTarget = displayScore;
            GetDisplayScore = _ => displayScore.Value;
        }

        public enum ComboDisplayMode
        {
            Current,
            Highest,
        }
    }
}
