using EscapeRoomEngine.Engine.Runtime.Modules;
using EscapeRoomEngine.Engine.Runtime.UI;
using Realms;
using UnityEngine;
using Logger = EscapeRoomEngine.Engine.Runtime.Utilities.Logger;
using LogType = EscapeRoomEngine.Engine.Runtime.Utilities.LogType;

namespace EscapeRoomEngine.Engine.Runtime.Measurements
{
    /// <summary>
    /// The storage engine that manages the data in a realm database.
    /// </summary>
    public class PuzzleStorage : MonoBehaviour
    {
        /// <summary>
        /// The schema version must be updated whenever the schema in the database changed.
        /// </summary>
        private const int SchemaVersion = 2;
        
        /// <summary>
        /// The active instance of the storage engine.
        /// </summary>
        public static PuzzleStorage Instance { get; private set; }

        [Tooltip("The path of the database relative to where the engine stores persistent data.")]
        [SerializeField]
        private string databasePath = "measurements.realm";
        
        private Realm _realm;

        private void OnEnable()
        {
            // configure special actions during specific migrations of the database
            var config = new RealmConfiguration
            {
                SchemaVersion = SchemaVersion,
                MigrationCallback = (migration, oldSchemaVersion) =>
                {
                    if (oldSchemaVersion < 1)
                    {
                        // migration from version 0 to 1
                        // nothing to do
                    }
                    if (oldSchemaVersion < 2)
                    {
                        // migration from version 1 to 2
                        // nothing to do
                    }
                    
                    Logger.Log($"Migrated database to version {SchemaVersion}", LogType.Measuring);
                }
            };
            _realm = Realm.GetInstance(config.ConfigWithPath(databasePath));
        }

        private void Awake()
        {
            Instance = this;
        }

        private void Start()
        {
            Logger.Log($"Using realm database at {_realm.Config.DatabasePath}", LogType.Measuring);
        }

        private void OnDisable()
        {
            _realm?.Dispose();
        }

        #region Session

        /// <summary>
        /// End a session and store it to the database.
        /// </summary>
        public void EndSession(Session session, float time)
        {
            session.Time = time;
            
            _realm.Write(() => _realm.Add(session));
        }

        #endregion

        #region Puzzles

        /// <summary>
        /// Create a new puzzle for a specific description and store it in the database.
        /// </summary>
        /// <remarks>This requires that the puzzle does not yet exist in the database.</remarks>
        private Puzzle New(PuzzleModuleDescription puzzle)
        {
            Puzzle created = null;
            
            _realm.Write(() => created = _realm.Add(new Puzzle(puzzle)));
            
            return created;
        }

        /// <summary>
        /// Load a specific puzzle from the database or create it if it wasn't found.
        /// </summary>
        private Puzzle LoadOrNew(PuzzleModuleDescription puzzle) => _realm.Find<Puzzle>(puzzle.Id) ?? New(puzzle);

        /// <summary>
        /// Load a specific puzzle from the database.
        /// </summary>
        /// <returns>The puzzle or null if it wasn't found in the database.</returns>
        public Puzzle Load(PuzzleModuleDescription puzzle) => _realm.Find<Puzzle>(puzzle.Id);
        
        /// <summary>
        /// Delete all records concerning a specific puzzle from the database irreversibly.
        /// </summary>
        public void Delete(PuzzleModuleDescription puzzle)
        {
            var found = Load(puzzle);
            if (found != null)
            {
                _realm.Write(() => _realm.Remove(found));
            }
        }

        /// <summary>
        /// End the measurement for a specific puzzle and store it in the database. Also stores the results of the current puzzle plan.
        /// </summary>
        public void EndMeasurement(Session session, PuzzleModuleDescription puzzle, PuzzleMeasurement measurement)
        {
            var found = LoadOrNew(puzzle);
            
            _realm.Write(() =>
            {
                measurement.TimeSolved = Time.time;
                found.Measurements.Add(measurement);
                
                // add solved puzzle to session
                session.PuzzlesSolved.Add(found);
                
                // add plan result to session
                session.PlanResults.Add(new PlanResult(
                    GameControl.Instance.TargetTime,
                    found.Distribution.Cumulative(measurement.Time),
                    GameControl.Instance.EstimatedTime));
            });
        }

        #endregion
    }
}