// 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 System.Linq.Expressions;
using osu.Framework.Platform;

namespace osu.Game.Database
{
    /// <summary>
    /// A typed store which supports basic addition, deletion and updating for soft-deletable models.
    /// </summary>
    /// <typeparam name="T">The databased model.</typeparam>
    public abstract class MutableDatabaseBackedStore<T> : DatabaseBackedStore
        where T : class, IHasPrimaryKey, ISoftDelete
    {
        public event Action<T> ItemAdded;
        public event Action<T> ItemRemoved;

        protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
            : base(contextFactory, storage)
        {
        }

        /// <summary>
        /// Access items pre-populated with includes for consumption.
        /// </summary>
        public IQueryable<T> ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set<T>());

        /// <summary>
        /// Add a <typeparamref name="T"/> to the database.
        /// </summary>
        /// <param name="item">The item to add.</param>
        public void Add(T item)
        {
            using (var usage = ContextFactory.GetForWrite())
            {
                var context = usage.Context;
                context.Attach(item);
            }

            ItemAdded?.Invoke(item);
        }

        /// <summary>
        /// Update a <typeparamref name="T"/> in the database.
        /// </summary>
        /// <param name="item">The item to update.</param>
        public void Update(T item)
        {
            using (var usage = ContextFactory.GetForWrite())
                usage.Context.Update(item);

            ItemRemoved?.Invoke(item);
            ItemAdded?.Invoke(item);
        }

        /// <summary>
        /// Delete a <typeparamref name="T"/> from the database.
        /// </summary>
        /// <param name="item">The item to delete.</param>
        public bool Delete(T item)
        {
            using (ContextFactory.GetForWrite())
            {
                Refresh(ref item);

                if (item.DeletePending) return false;

                item.DeletePending = true;
            }

            ItemRemoved?.Invoke(item);
            return true;
        }

        /// <summary>
        /// Restore a <typeparamref name="T"/> from a deleted state.
        /// </summary>
        /// <param name="item">The item to undelete.</param>
        public bool Undelete(T item)
        {
            using (ContextFactory.GetForWrite())
            {
                Refresh(ref item, ConsumableItems);

                if (!item.DeletePending) return false;

                item.DeletePending = false;
            }

            ItemAdded?.Invoke(item);
            return true;
        }

        /// <summary>
        /// Allow implementations to add database-side includes or constraints when querying for consumption of items.
        /// </summary>
        /// <param name="query">The input query.</param>
        /// <returns>A potentially modified output query.</returns>
        protected virtual IQueryable<T> AddIncludesForConsumption(IQueryable<T> query) => query;

        /// <summary>
        /// Allow implementations to add database-side includes or constraints when deleting items.
        /// Included properties could then be subsequently deleted by overriding <see cref="Purge"/>.
        /// </summary>
        /// <param name="query">The input query.</param>
        /// <returns>A potentially modified output query.</returns>
        protected virtual IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) => query;

        /// <summary>
        /// Called when removing an item completely from the database.
        /// </summary>
        /// <param name="items">The items to be purged.</param>
        /// <param name="context">The write context which can be used to perform subsequent deletions.</param>
        protected virtual void Purge(List<T> items, OsuDbContext context) => context.RemoveRange(items);

        public override void Cleanup()
        {
            base.Cleanup();
            PurgeDeletable();
        }

        /// <summary>
        /// Purge items in a pending delete state.
        /// </summary>
        /// <param name="query">An optional query limiting the scope of the purge.</param>
        public void PurgeDeletable(Expression<Func<T, bool>> query = null)
        {
            using (var usage = ContextFactory.GetForWrite())
            {
                var context = usage.Context;

                var lookup = context.Set<T>().Where(s => s.DeletePending);

                if (query != null) lookup = lookup.Where(query);

                lookup = AddIncludesForDeletion(lookup);

                var purgeable = lookup.ToList();

                if (!purgeable.Any()) return;

                Purge(purgeable, context);
            }
        }
    }
}
