/* 
 * Copyright (c)2009-2025 DemiVision, LLC. All Rights Reserved. The information 
 * herein is the CONFIDENTIAL and PROPRIETARY information of DemiVision, LLC. 
 */

using System.Text.Json.Serialization;
using Plugin.Firebase.Firestore;

using DXLib.Data.Model;

namespace iStatVball3;

/*
 * The primary data object for recording statistics. A Stat entry is recorded for every volleyball action and point
 * during a Set. All granular fields required to handle recording state as well as to generate analysis reports are
 * saved here.
 * 
 * Stats are persisted as a sub-collection of the parent Set. They are NOT persisted as a root-level collection or as
 * an embedded map.
 */
public class Stat : DXModel
{
	/* Constants */
	public const string CollectionKey = "Stats";

	/* Properties */
	[FirestoreProperty("Legacy")] public bool Legacy { get; set; }

	// Monotonically increasing index
	[FirestoreProperty("Counter")] public int Counter { get; set; }

	// Wall clock at time of tap
	[FirestoreProperty("Timestamp")] public DateTimeOffset Timestamp { get; set; }

	// Offset (ms) from first serve of set
	[FirestoreProperty("Offset")] public int Offset { get; set; }

	// Video sync offset (ms) from start of file
	[FirestoreProperty("VideoOffset")] public int VideoOffset { get; set; }

	// Jersey number(s)
	[FirestoreProperty("Number")] public string Number { get; set; }
	[FirestoreProperty("Number2")] public string Number2 { get; set; }
	[FirestoreProperty("Number3")] public string Number3 { get; set; }

	// Lineup position(s)
	[FirestoreProperty("Position")] public string Position { get; set; }
	[FirestoreProperty("Position2")] public string Position2 { get; set; }
	[FirestoreProperty("Position3")] public string Position3 { get; set; }

	// Relative to lineup order
	[FirestoreProperty("Rotation")] public int Rotation { get; set; }

	// Action menu selections
	[FirestoreProperty("Action")] public string Action { get; set; }
	[FirestoreProperty("Selector")] public string Selector { get; set; }
	[FirestoreProperty("Modifier")] public string Modifier { get; set; }

	[FirestoreProperty("Rating")] public int? Rating { get; set; }

	// Court info
	[FirestoreProperty("CourtPosition")] public int CourtPosition { get; set; }
	[FirestoreProperty("CourtRow")] public int CourtRow { get; set; }

	// Location related
	[FirestoreProperty("PrevX")] public float? PrevX { get; set; }
	[FirestoreProperty("PrevY")] public float? PrevY { get; set; }

	[FirestoreProperty("StartX")] public float StartX { get; set; }
	[FirestoreProperty("StartY")] public float StartY { get; set; }

	[FirestoreProperty("EndX")] public float EndX { get; set; }
	[FirestoreProperty("EndY")] public float EndY { get; set; }

	[FirestoreProperty("StartZone")] public int StartZone { get; set; }
	[FirestoreProperty("EndZone")] public int EndZone { get; set; }

	// Results
	[FirestoreProperty("Result")] public string Result { get; set; }
	[FirestoreProperty("Error")] public string Error { get; set; }
	[FirestoreProperty("Fault")] public string Fault { get; set; }

	// Earned point?
	[FirestoreProperty("Earned")] public int Earned { get; set; }

	// Timing
	[FirestoreProperty("Tempo")] public int Tempo { get; set; }

	// WHP profiling
	[FirestoreProperty("WHP")] public float? WHP { get; set; }

	// Context specific flag/value
	[FirestoreProperty("Value")] public int? Value { get; set; }

	// Was this stat preceded by an undo?
	[FirestoreProperty("Restored")] public bool Restored { get; set; }

	// Auto generated action (Auto Set)?
	[FirestoreProperty("Auto")] public bool Auto { get; set; }

	// Persisted to Cloud?
	[FirestoreProperty("Persisted")] public bool Persisted { get; set; }
	
	// References (FKs)
	[FirestoreProperty("TeamId")] public string TeamId { get; set; }

	[FirestoreProperty("PlayerId")] public string PlayerId { get; set; }
	[FirestoreProperty("Player2Id")] public string Player2Id { get; set; }
	[FirestoreProperty("Player3Id")] public string Player3Id { get; set; }

	// Children (maps)
	[FirestoreProperty("Point")] public StatPoint Point { get; set; }
	[FirestoreProperty("State")] public StatState State { get; set; }

	// Parent (FK)
	[FirestoreProperty("SetId")] public string SetId { get; set; }

	/* Ignored */
	[JsonIgnore] public bool IsPoint => Action == Stats.PointKey;
	[JsonIgnore] public bool IsSideout => IsPoint && Point is { Sideout: true };

	[JsonIgnore] public bool IsError => Result == Stats.ErrorKey;
	[JsonIgnore] public bool IsFault => IsError && (Error == Stats.FaultKey);

	[JsonIgnore] public bool IsServe => Action == Stats.ServeKey;
	[JsonIgnore] public bool IsFirstServe => IsServe && (Value == 1);
	[JsonIgnore] public bool IsDefense => Action == Stats.DefenseKey;

	[JsonIgnore] public bool IsBlock => Action == Stats.BlockKey;
	[JsonIgnore] public bool IsMultiBlock => IsBlock && (Player2 != null);

	[JsonIgnore] public bool IsLineup => Action == Stats.LineupKey;
	[JsonIgnore] public bool IsUpdate => Action == Stats.UpdateKey;
	[JsonIgnore] public bool IsEnd => IsUpdate && (Selector == Stats.EndKey);

	[JsonIgnore] public bool IsAction => !IsPoint && !IsLineup && !IsUpdate && !IsEnd;

	// References
	[JsonIgnore] public Player Player { get; set; }
	[JsonIgnore] public Player Player2 { get; set; }
	[JsonIgnore] public Player Player3 { get; set; }

	// Parent
	[JsonIgnore] public Set Set { get; set; }

	/* Methods */
	public Stat()
	{
		BaseCollectionKey = CollectionKey;
	}

	// Tests equality based on unique identifier
	public override bool Equals( object obj )
	{
		return (obj is Stat stat) && stat.UniqueId.Equals( UniqueId );
	}

	// Generates unique hash code, required for list de-duping
	public override int GetHashCode()
	{
		return UniqueId.GetHashCode();
	}

	// Returns specified player (1-3) from this stat, if any
	public Player GetPlayer( int number )
	{
		return number switch
		{
			1 => Player,
			2 => Player2,
			3 => Player3,

			_ => null,
		};
	}

	// Returns rotation associated with stat
	public int GetRotation( bool sideout )
	{
		int rotation = Rotation;

		// Special handling for points
		if ( IsPoint )
		{
			// Sideouts use rotation of receiving team (changed 3.8.1)
			if ( sideout )
			{
				rotation = (Point.RecvRotation == 0) ? Point.Rotation : Point.RecvRotation;
			}
			// All others use rotation of team earning point
			else
			{
				rotation = Point.Rotation;
			}
		}

		return rotation;
	}

    // Determines if generic value field equals boolean true
    public bool IsValueTrue()
    {
        return Value.HasValue && (Value != 0);
    }

    // Determines if this stat resulted in an earned point
    public bool IsEarned()
	{
		return IsEarned( Result );
	}

	// Determines if specified result is an earned point
	public static bool IsEarned( string result )
	{
        return result switch
        {
            Stats.AceKey => true,
            Stats.KillKey => true,
            Stats.BlockKey => true,
            Stats.BlockAssistKey => true,
            Stats.BlockAssistsKey => true,

            _ => false,
        };
    }

	// Determines if this stat is action that can become earned point
	public bool IsEarnable()
	{
		// Serve/Block always eligible
		if ( IsServe || IsBlock )
		{
			return true;
		}

		// 2nd/3rd Ball always eligible on attack
		if ( Selector == Stats.AttackKey )
		{
			return true;
		}

		// All other contacts only eligible if resulted in kill
		return (Result == Stats.KillKey);
	}

	// Determines if this stat is action that can become error
	public bool IsErrorable()
	{
		return IsAction;
	}

    // Determines if this stat is any first ball passing contact
    public bool IsPassing()
    {
		return IsPassing( Action );
    }

    // Determines if specified stat action is any first ball passing contact
    public static bool IsPassing( string action )
	{
		if ( !string.IsNullOrEmpty( action ) )
		{
			return action switch
			{
				Stats.ReceiveKey => true,
				Stats.DefenseKey => true,
				Stats.FreeballKey => true,
				Stats.FirstKey => true,
				Stats.PutbackKey => true,

				_ => false
			};
		}

		return false;
    }

	// Determines if this stat is non ball contact action
	public bool IsOtherAction()
	{
        return Action switch
        {
	        Stats.PointKey => true,
	        Stats.LineupKey => true,
	        Stats.UpdateKey => true,

            _ => false,
        };
    }

	// Maps pass rating between 0-4 and 0-3
	public static int MapRating( int? rating, bool passing )
	{
		if ( rating.HasValue )
		{
			// Serve/Set always 0-4
			if ( passing && Shell.Settings.IsPass03 )
			{
				// 0-4 -> 0-3
				switch ( rating )
				{
					case 0: return 0;
					case 1: return 0;
					case 2: return 1;
					case 3: return 2;
					case 4: return 3;
				}
			}

			return (int)rating;
		}

		return -1;
	}

	/* Players */

	// Attributes this stat to specified player
	public void SetPlayer( Player player, Lineup lineup )
	{
		Player = player;
		PlayerId = player.UniqueId;
		Number = player.Number;

		Position = lineup?.GetPosition( player );
	}

	// Attributes this stat to specified player(s)
	public void SetPlayers( List<Player> players, Lineup lineup )
	{
		// No player(s) for opponent
		if ( players.Count > 0 )
		{
			SetPlayer( players[0], lineup );

			int count = players.Count;

			// Possible player 2 (block only)
			Player player2 = (count > 1) ? players[1] : null;

			Player2 = player2;
			Player2Id = player2?.UniqueId;
			Number2 = player2?.Number;

			Position2 = lineup?.GetPosition( player2 );

			// Possible player 3 (block only)
			Player player3 = (count > 2) ? players[2] : null;

			Player3 = player3;
			Player3Id = player3?.UniqueId;
			Number3 = player3?.Number;

			Position3 = lineup?.GetPosition( player3 );
		}
	}

	/* Populate */

	// Populates all FKs (no db queries)
	public void Populate()
	{
		Season season = Set.Match.Season;

		Player = season.GetPlayer( PlayerId );
		Player2 = season.GetPlayer( Player2Id );
		Player3 = season.GetPlayer( Player3Id );

		// Populate children
		Point?.Populate( season );
		State?.Populate( season );
	}

	// Populates all FKs given specified set (no db queries)
	public void Populate( Set set, bool deep = true )
	{
		Set = set;

		// Optionally populate players and sub-objects
		if ( deep )
		{
			Populate();
		}
	}

	/* CRUD */

	// CREATE

	// // Creates new Stat in sub-collection
	// public override async Task Create()
	// {
	// 	await GetSubDocument().SetDataAsync( this );
	// }

	// Creates new Stat in sub-collection (batched)
	public override void Create( IWriteBatch batch )
	{
		batch.SetData( GetSubDocument(), this );
	}

	// UPDATE

	// // Updates entire document for this Stat
	// public override async Task Update()
	// {
	// 	await GetSubDocument().SetDataAsync( this );
	// }

	// Updates entire document for this Stat (batched)
	public override void Update( IWriteBatch batch )
	{
		batch.SetData( GetSubDocument(), this );
	}

	// // Updates State map field of existing Stat (batched)
	// public void UpdateState( IWriteBatch batch )
	// {
	// 	batch.UpdateData( GetSubDocument(), GetData( "State", State ) );
	// }
 //
	// // Updates result field of existing Stat (batched)
	// public void UpdateResult( IWriteBatch batch, string result )
	// {
	// 	Result = result;
 //
	// 	batch.UpdateData( GetSubDocument(), GetData( "Result", Result ) );
	// }
 //
	// // Updates result related fields of existing Stat (batched)
	// public void UpdateResult( IWriteBatch batch, string result, string error )
	// {
	// 	Result = result;
	// 	Error = error;
 //
	// 	var fields  = new Dictionary<object,object> { { "Result", Result }, { "Error", Error } };
	// 	
	// 	batch.UpdateData( GetSubDocument(), fields );
	// }
 //
	// // Updates all result related fields of existing Stat (batched)
	// public void UpdateResult( IWriteBatch batch, string result, string error, int? rating )
	// {
	// 	Result = result;
	// 	Error = error;
	// 	Rating = rating;
	// 	
	// 	var fields  = new Dictionary<object,object> { { "Result", Result }, { "Error", Error }, { "Rating", Rating } };
 //
	// 	batch.UpdateData( GetSubDocument(), fields );
	// }
 //
	// // Updates context specific value field of existing Stat (batched)
	// public void UpdateValue( IWriteBatch batch, int? value )
	// {
	// 	Value = value;
 //
 //        batch.UpdateData( GetSubDocument(), GetData( "Value", Value ) );
 //    }
 //
 //    // Updates auto-generated (Auto Set) status
 //    public void UpdateAuto( IWriteBatch batch )
	// {
	// 	Auto = true;
 //
	// 	batch.UpdateData( GetSubDocument(), GetData( "Auto", Auto ) );
	// }
 //
	// // Updates rating field of existing Stat (batched)
	// public void UpdateRating( IWriteBatch batch, int? rating )
	// {
	// 	Rating = rating;
 //
	// 	batch.UpdateData( GetSubDocument(), GetData( "Rating", Rating ) );
	// }
 //
	// // Updates earned point field of existing Stat
	// public void UpdateEarned( IWriteBatch batch, bool earned )
	// {
	// 	Earned = (earned ? 1 : 0);
 //
	// 	batch.UpdateData( GetSubDocument(), GetData( "Earned", Earned ) );
	// }
 //
	// // Updates post-undo restored field of existing Stat (batched)
	// public void UpdateRestored( IWriteBatch batch, bool restored )
	// {
	// 	Restored = restored;
 //
	// 	batch.UpdateData( GetSubDocument(), GetData( "Restored", Restored ) );
	// }

	// Updates team associated with this stat, used to change opponents (no db)
	public void UpdateTeam( string oldId, string newId )
	{
		if ( (TeamId == oldId) || (TeamId == null) )
		{
			// Update stat
			TeamId = newId;

			// Update point map
			if ( IsPoint )
			{
				Point.UpdateTeam( oldId, newId );
			}

			// Update state map (if still exists)
			State?.UpdateTeam( oldId, newId );
		}
	}

	// DELETE

	// Deletes sub-document for this Stat object
	public override async Task Delete( bool remove = true )
	{
		// No children to delete (maps auto deleted)

		// Optionally remove from parent
		if ( remove )
		{
			Set.StatCache.Remove( this );
		}

		// Delete self
		await Delete( GetSubDocument() );
	}

	// Deletes sub-document for this Stat object (batched)
	public void Delete( IWriteBatch batch, bool remove = true )
	{
		// No children to delete (maps auto deleted)

		// Optionally remove from parent
		if ( remove )
		{
			Set.StatCache.Remove( this );
		}

		// Delete self
		batch.DeleteDocument( GetSubDocument() );
	}

	// Returns document reference for this Stat object within sub-collection
	private IDocumentReference GetSubDocument()
	{
		return GetSubDocument( Set.CollectionKey, Set.UniqueId );
	}
}

//
