#region

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
using Hearthstone_Deck_Tracker.Annotations;
using Hearthstone_Deck_Tracker.Enums;
using Hearthstone_Deck_Tracker.Hearthstone;
using Hearthstone_Deck_Tracker.Utility.Extensions;

#endregion

namespace Hearthstone_Deck_Tracker.Stats.CompiledStats
{
	public class ConstructedStats : INotifyPropertyChanged
	{
		public static ConstructedStats Instance { get; } = new ConstructedStats();
		public IEnumerable<GameStats> FilteredGames => GetFilteredGames();

		public event PropertyChangedEventHandler? PropertyChanged;

		public IEnumerable<GameStats> GetFilteredGames(bool archived = true, bool playerClass = true, bool region = true,
													   bool timeFrame = true, bool mode = true, bool league = true, bool rank = true, bool format = true, 
													   bool turns = true, bool coin = true, bool result = true, bool oppClass = true, 
													   bool oppName = true, bool note = true, bool tags = true, bool includeNoDeck = true)
		{
			var decks = Config.Instance.ConstructedStatsActiveDeckOnly && DeckList.Instance.ActiveDeck != null ? new[] {DeckList.Instance.ActiveDeck} : DeckList.Instance.Decks.ToArray();
			return GetFilteredGames(decks, archived, playerClass, region, timeFrame, mode, league, rank, format, turns, coin, result,
									oppClass, oppName, note, tags, includeNoDeck);
		}

		public IEnumerable<GameStats> GetFilteredGames(IEnumerable<Deck> decks, bool archived = true, bool playerClass = true, bool region = true,
													   bool timeframe = true, bool mode = true, bool league = true, bool rank = true, 
													   bool format = true, bool turns = true, bool coin = true,
													   bool result = true, bool oppClass = true, bool oppName = true,
													   bool note = true, bool tags = true, bool includeNoDeck = true)
		{
			decks = decks.Where(x => !x.IsArenaDeck);

			if(archived && !Config.Instance.ConstructedStatsIncludeArchived && !Config.Instance.ConstructedStatsActiveDeckOnly)
				decks = decks.Where(x => !x.Archived);

			if(tags && Config.Instance.ConstructedStatsApplyTagFilters && !Config.Instance.SelectedTags.Contains("All") && !Config.Instance.ConstructedStatsActiveDeckOnly)
				decks = decks.Where(d => d.Tags.Any(t => Config.Instance.SelectedTags.Contains(t)));

			var filtered = decks.SelectMany(x => x.DeckStats.Games).Where(x => !x.IsClone);

			if(!Config.Instance.ConstructedStatsActiveDeckOnly && includeNoDeck)
				filtered = filtered.Concat(DefaultDeckStats.Instance.DeckStats.SelectMany(x => x.Games).Where(x => !x.IsClone));

			if(playerClass && Config.Instance.ConstructedStatsClassFilter != HeroClassStatsFilter.All && !Config.Instance.ConstructedStatsActiveDeckOnly)
				filtered = filtered.Where(x => x.PlayerHero == Config.Instance.ConstructedStatsClassFilter.ToString());
			if(region && Config.Instance.ConstructedStatsRegionFilter != RegionAll.ALL)
			{
				var parsed = (Region)Enum.Parse(typeof(Region), Config.Instance.ConstructedStatsRegionFilter.ToString());
				filtered = filtered.Where(x => x.Region == parsed);
			}
			if(timeframe)
			{
				switch(Config.Instance.ConstructedStatsTimeFrameFilter)
				{
					case DisplayedTimeFrame.AllTime:
						break;
					case DisplayedTimeFrame.CurrentSeason:
						filtered = filtered.Where(g => g.StartTime >= new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1));
						break;
					case DisplayedTimeFrame.LastSeason:
						filtered = filtered.Where(g => g.StartTime >= new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1).AddMonths(-1) 
													&& g.StartTime < new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1));
						break;
					case DisplayedTimeFrame.CustomSeason:
						var current = Helper.CurrentSeason;
						filtered = filtered.Where(g => g.StartTime >= new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1)
																		.AddMonths(Config.Instance.ConstructedStatsCustomSeasonMin - current));
						if(Config.Instance.ConstructedStatsCustomSeasonMax.HasValue)
						{
							filtered = filtered.Where(g => g.StartTime < new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1)
																		.AddMonths(Config.Instance.ConstructedStatsCustomSeasonMax.Value - current + 1));
						}
						break;
					case DisplayedTimeFrame.ThisWeek:
						filtered = filtered.Where(g => g.StartTime > DateTimeHelper.StartOfWeek);
						break;
					case DisplayedTimeFrame.Today:
						filtered = filtered.Where(g => g.StartTime > DateTime.Today);
						break;
					case DisplayedTimeFrame.Custom:
						var start = (Config.Instance.ConstructedStatsTimeFrameCustomStart ?? DateTime.MinValue).Date;
						var end = (Config.Instance.ConstructedStatsTimeFrameCustomEnd ?? DateTime.MaxValue).Date;
						filtered = filtered.Where(g => g.EndTime.Date >= start && g.EndTime.Date <= end);
						break;
				}
			}
			if(mode && Config.Instance.ConstructedStatsModeFilter != GameMode.All)
				filtered = filtered.Where(x => x.GameMode == Config.Instance.ConstructedStatsModeFilter);
			if(league && Config.Instance.ConstructedStatsModeFilter == GameMode.Ranked)
			{
				switch(Config.Instance.ConstructedStatsLeagueFilter)
				{
					case League.All:
						filtered = filtered.Where(g => g.LeagueId >= 5);
						break;
					case League.Bronze:
						filtered = filtered.Where(g => g.LeagueId >= 5 && g.StarLevel >= 1 && g.StarLevel <= 10);
						break;
					case League.Silver:
						filtered = filtered.Where(g => g.LeagueId >= 5 && g.StarLevel >= 11 && g.StarLevel <= 20);
						break;
					case League.Gold:
						filtered = filtered.Where(g => g.LeagueId >= 5 && g.StarLevel >= 21 && g.StarLevel <= 30);
						break;
					case League.Platinum:
						filtered = filtered.Where(g => g.LeagueId >= 5 && g.StarLevel >= 31 && g.StarLevel <= 40);
						break;
					case League.Diamond:
						filtered = filtered.Where(g => g.LeagueId >= 5 && g.StarLevel >= 41 && g.StarLevel <= 50);
						break;
					case League.Legend:
						filtered = filtered.Where(g => g.LeagueId >= 5 && g.HasLegendRank);
						break;
					case League.Legacy:
						filtered = filtered.Where(g => g.LeagueId < 5);
						break;
					case League.LegacyAll:
						break;
				}

				if(rank && Config.Instance.ConstructedStatsLeagueFilter == League.Legacy)
				{
					var min = Config.Instance.ConstructedStatsRankFilterMin;
					if(min != "L1")
					{
						int minValue;
						if(min.StartsWith("L"))
						{
							if(int.TryParse(min.Substring(1), out minValue))
								filtered = filtered.Where(x => !x.HasLegendRank || x.LegendRank >= minValue);
						}
						else if(int.TryParse(min, out minValue))
							filtered = filtered.Where(x => !x.HasLegendRank && (!x.HasRank || x.Rank >= minValue));
						
					}
					var max = Config.Instance.ConstructedStatsRankFilterMax;
					if(!string.IsNullOrEmpty(max))
					{
						int maxValue;
						if(max.StartsWith("L"))
						{
							if(int.TryParse(max.Substring(1), out maxValue))
								filtered = filtered.Where(x => x.HasLegendRank && x.LegendRank <= maxValue);
						}
						else if(int.TryParse(max, out maxValue))
							filtered = filtered.Where(x => x.HasLegendRank || x.HasRank && x.Rank <= maxValue);
						
					}
				}
			}

			if(format && Config.Instance.ConstructedStatsFormatFilter != Format.All
			   && (Config.Instance.ConstructedStatsModeFilter == GameMode.Ranked
			   || Config.Instance.ConstructedStatsModeFilter == GameMode.Casual))
				filtered = filtered.Where(x => x.Format == Config.Instance.ConstructedStatsFormatFilter);
			if(turns)
			{
				if(Config.Instance.ConstructedStatsTurnsFilterMin > 0)
					filtered = filtered.Where(x => x.Turns >= Config.Instance.ConstructedStatsTurnsFilterMin);
				if(Config.Instance.ConstructedStatsTurnsFilterMax < 99)
					filtered = filtered.Where(x => x.Turns <= Config.Instance.ConstructedStatsTurnsFilterMax);
			}
			if(coin && Config.Instance.ConstructedStatsCoinFilter != AllYesNo.All)
				filtered = filtered.Where(x => x.Coin == (Config.Instance.ConstructedStatsCoinFilter == AllYesNo.Yes));
			if(result && Config.Instance.ConstructedStatsResultFilter != GameResultAll.All)
			{
				var parsed = (GameResult)Enum.Parse(typeof(GameResult), Config.Instance.ConstructedStatsResultFilter.ToString());
				filtered = filtered.Where(x => x.Result == parsed);
			}
			if(oppClass && Config.Instance.ConstructedStatsOpponentClassFilter != HeroClassStatsFilter.All)
				filtered = filtered.Where(x => x.OpponentHero == Config.Instance.ConstructedStatsOpponentClassFilter.ToString());
			if(oppName && !string.IsNullOrEmpty(Config.Instance.ConstructedStatsOpponentNameFilter))
				filtered = filtered.Where(x => x.OpponentName?.ToUpperInvariant().Contains(Config.Instance.ConstructedStatsOpponentNameFilter.ToUpperInvariant()) ?? false);
			if(note && !string.IsNullOrEmpty(Config.Instance.ConstructedStatsNoteFilter))
				filtered = filtered.Where(x => x.Note?.ToUpperInvariant().Contains(Config.Instance.ConstructedStatsNoteFilter.ToUpperInvariant()) ?? false);

			return filtered.OrderByDescending(x => x.StartTime);
		}

		public IEnumerable<ChartStats> PlayedClassesPercent
		{
			get
			{
				var games = GetFilteredGames().ToList();
				return
					games.GroupBy(x => ClassSelector(x.PlayerHero))
						 .OrderBy(x => x.Key)
						 .Select(x =>
								 new ChartStats
								 {
									 Name = x.Key + " (" + Math.Round(100.0 * x.Count() / games.Count) + "%)",
									 Value = x.Count(),
									 Brush = new SolidColorBrush(Helper.GetClassColor(x.Key, true))
								 });
			}
		}

		public IEnumerable<ChartStats> Winrate
		{
			get
			{
				var games = GetFilteredGames().ToList();
				var wins = games.Where(x => x.Result == GameResult.Win).ToList();
				return wins.Count > 0
						   ? wins.Select(x => new ChartStats {Name = "Wins", Value = Math.Round(100.0 * wins.Count() / games.Count)})
						   : EmptyChartStats("Wins");
			}
		}

		public IEnumerable<ChartStats> WinrateByCoin
		{
			get
			{
				var games = GetFilteredGames(coin: false).ToList();
				var wins = games.Where(x => x.Result == GameResult.Win).ToList();
				var gamesCoin = games.Where(x => x.Coin);
				var winsCoin = wins.Where(x => x.Coin).ToList();
				var gamesNoCoin = games.Where(x => !x.Coin);
				var winsNoCoin = wins.Where(x => !x.Coin).ToList();
				var total = wins.Count > 0
								? wins.Select(x => new ChartStats {Name = "Total", Value = Math.Round(100.0 * wins.Count() / games.Count)})
								: EmptyChartStats("Wins");
				var coin = winsCoin.Count > 0
								? winsCoin.Select(x => new ChartStats {Name = "With Coin", Value = 100.0 * winsCoin.Count() / gamesCoin.Count()})
								: EmptyChartStats("With Coin");
				var noCoin = winsNoCoin.Count > 0
								 ? winsNoCoin.Select(x => new ChartStats {Name = "Without Coin", Value = 100.0 * winsNoCoin.Count() / gamesNoCoin.Count()})
								 : EmptyChartStats("Without Coin");
				return total.Concat(coin).Concat(noCoin);
			}
		}

		private ChartStats[] EmptyChartStats(string name) => new[] {new ChartStats() {Name = name, Value = 0}};

		public IEnumerable<ChartStats> OpponentClassesPercent
		{
			get
			{
				var games = GetFilteredGames(oppClass: false).ToList();
				return
					games.GroupBy(x => ClassSelector(x.OpponentHero))
						 .OrderBy(x => x.Key)
						 .Select(g =>
								 new ChartStats
								 {
									 Name = g.Key + " (" + Math.Round(100.0 * g.Count() / games.Count) + "%)",
									 Value = g.Count(),
									 Brush = new SolidColorBrush(Helper.GetClassColor(g.Key, true))
								 });
			}
		}

		private static string ClassSelector(string? heroClass) => (heroClass != null && Enum.GetNames(typeof(HeroClass)).Any(c => c == heroClass) ? heroClass : "Other");

		public IEnumerable<ChartStats> AvgWinratePerClass
			=> GetFilteredGames(DeckList.Instance.Decks, playerClass: false)
					.GroupBy(x => ClassSelector(x.PlayerHero))
					.OrderBy(x => x.Key)
					.Select(x =>
							new ChartStats
							{
								Name = x.Key,
								Value = Math.Round(100.0 * x.Count(g => g.Result == GameResult.Win) / x.Count(), 1),
								Brush = new SolidColorBrush(Helper.GetClassColor(x.Key, true))
							});

		public IEnumerable<ChartStats> WinrateAgainstClass
			=> GetFilteredGames()
					.GroupBy(x => ClassSelector(x.OpponentHero))
					.OrderBy(x => x.Key)
					.Select(x =>
							new ChartStats
							{
								Name = x.Key,
								Value = Math.Round(100.0 * x.Count(g => g.Result == GameResult.Win) / x.Count(), 1),
								Brush = new SolidColorBrush(Helper.GetClassColor(x.Key, true))
							});

		public Visibility NeutralVisiblity => Config.Instance.ConstructedStatsModeFilter == GameMode.Duels
			? Visibility.Visible
			: Visibility.Hidden;

		public IEnumerable<ConstructedMatchup> Matchups
		{
			get
			{
				var games = GetFilteredGames(playerClass: false, oppClass: false).ToList();
				var isDuels = Config.Instance.ConstructedStatsModeFilter == GameMode.Duels;

				foreach(var c in Enum.GetValues(typeof(HeroClassNeutral)).Cast<HeroClassNeutral>())
					if (c != HeroClassNeutral.Neutral || isDuels)
						yield return isDuels ? new ConstructedDuelsMatchup(c, games) : new ConstructedMatchup(c, games);
				yield return isDuels ? new ConstructedDuelsMatchup(games) : new ConstructedMatchup(games);
			}
		}

		public IEnumerable<ConstructedDeckDetails> DeckDetails
		{
			get
			{
				var deck = DeckList.Instance.ActiveDeck;
				if(deck == null)
					yield break;
				var games = GetFilteredGames(new[] {deck}, playerClass: false, oppClass: false).ToList();
				if(deck.HasVersions)
					yield return new ConstructedDeckDetails("All", games);
				foreach(var v in deck.VersionsIncludingSelf)
					yield return new ConstructedDeckDetails(v.ShortVersionString, games.Where(x => x.BelongsToDeckVerion(deck.GetVersion(v))));
			}
		} 


		public void UpdateConstructedStats()
		{
			OnPropertyChanged(nameof(FilteredGames));
			OnPropertyChanged(nameof(PlayedClassesPercent));
			OnPropertyChanged(nameof(OpponentClassesPercent));
			OnPropertyChanged(nameof(Winrate));
			OnPropertyChanged(nameof(DeckStatsTotal));
			OnPropertyChanged(nameof(HighestRank));
			if(!Config.Instance.ConstructedStatsActiveDeckOnly)
			{
				OnPropertyChanged(nameof(DeckStatsMostPlayed));
				OnPropertyChanged(nameof(DeckStatsBest));
				OnPropertyChanged(nameof(DeckStatsFastest));
				OnPropertyChanged(nameof(DeckStatsSlowest));
			}
			UpdateMatchups();
		}

		public void UpdateConstructedCharts()
		{
			OnPropertyChanged(nameof(WinrateByCoin));
			OnPropertyChanged(nameof(AvgWinratePerClass));
			OnPropertyChanged(nameof(WinrateAgainstClass));
		}

		public void UpdateMatchups()
		{
			if(Config.Instance.ConstructedStatsActiveDeckOnly)
				OnPropertyChanged(nameof(DeckDetails));
			else
			{
				OnPropertyChanged(nameof(Matchups));
				OnPropertyChanged(nameof(NeutralVisiblity));
			}
		}

		[NotifyPropertyChangedInvocator]
		protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
		{
			PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
		}

		public void UpdateGames() => OnPropertyChanged(nameof(FilteredGames));

		public ConstructedDeckStats? DeckStatsBest
			=> GetFilteredGames(includeNoDeck: false)
					.GroupBy(x => x.DeckId)
					.Select(x => new { grouping = x, DeckPerformance = DeckPerformanceIndex(x) })
					.OrderByDescending(x => x.DeckPerformance)
					.FirstOrDefault(x => x.grouping.Any())?
					.grouping?.ToConstructedDeckStats();

		public ConstructedDeckStats? DeckStatsMostPlayed
			=> GetFilteredGames(includeNoDeck: false)
					.GroupBy(x => x.DeckId)
					.Select(x => new { grouping = x, Count = x.Count() })
					.OrderByDescending(x => x.Count)
					.FirstOrDefault(x => x.grouping.Any())?
					.grouping?.ToConstructedDeckStats();

		public ConstructedDeckStats? DeckStatsFastest
			=> GetFilteredGames(includeNoDeck: false)
					.GroupBy(x => x.DeckId)
					.Where(x => x.Count() > 1)
					.OrderBy(x => x.Average(g => g.Turns))
					.FirstOrDefault()?.ToConstructedDeckStats();

		public ConstructedDeckStats? DeckStatsSlowest
			=> GetFilteredGames(includeNoDeck: false)
					.GroupBy(x => x.DeckId)
					.Where(x => x.Count() > 1)
					.OrderByDescending(x => x.Average(g => g.Turns))
					.FirstOrDefault()?.ToConstructedDeckStats();

		public ConstructedDeckStats? DeckStatsTotal
		{
			get
			{
				var games = GetFilteredGames().ToArray();
				return games.Length > 0 ? new ConstructedDeckStats(games) : null;
			}
		}

		public string? HighestRank => GetFilteredGames().OrderBy(x => x.SortableRank).FirstOrDefault()?.RankString;

		private double DeckPerformanceIndex(IEnumerable<GameStats> matches)
		{
			if(matches == null)
				return 0;

			var savedDeckMatches = matches.ToArray();
			
			double performanceIndex = (savedDeckMatches.Count(g => g.Result == GameResult.Win) + savedDeckMatches.Count(g => g.Result == GameResult.Loss));
			performanceIndex = (performanceIndex / (savedDeckMatches.Count(g => g.Result == GameResult.Loss) + 1)) * Math.Min(1, performanceIndex / 10);

			return performanceIndex;
		}
	}
}
