using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
namespace Timeline.Services
{
    /// 
    /// A data manager controlling data.
    /// 
    /// 
    /// Identical data will be saved as one copy and return the same tag.
    /// Every data has a ref count. When data is retained, ref count increase.
    /// When data is freed, ref count decease. If ref count is decreased
    /// to 0, the data entry will be destroyed and no longer occupy space.
    /// 
    public interface IDataManager
    {
        /// 
        /// Saves the data to a new entry if it does not exist, 
        /// increases its ref count and returns a tag to the entry.
        /// 
        /// The data. Can't be null.
        /// The tag of the created entry.
        /// Thrown when  is null.
        public Task RetainEntry(byte[] data);
        /// 
        /// Decrease the the ref count of the entry.
        /// Remove it if ref count is zero.
        /// 
        /// The tag of the entry.
        /// Thrown when  is null.
        /// 
        /// It's no-op if entry with tag does not exist.
        /// 
        public Task FreeEntry(string tag);
        /// 
        /// Retrieve the entry with given tag. If not exist, returns null.
        /// 
        /// The tag of the entry.
        /// The data of the entry. If not exist, returns null.
        /// Thrown when  is null.
        public Task GetEntry(string tag);
    }
    public class DataManager : IDataManager
    {
        private readonly DatabaseContext _database;
        private readonly IETagGenerator _eTagGenerator;
        public DataManager(DatabaseContext database, IETagGenerator eTagGenerator)
        {
            _database = database;
            _eTagGenerator = eTagGenerator;
        }
        public async Task RetainEntry(byte[] data)
        {
            if (data == null)
                throw new ArgumentNullException(nameof(data));
            var tag = await _eTagGenerator.Generate(data);
            var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync();
            if (entity == null)
            {
                entity = new DataEntity
                {
                    Tag = tag,
                    Data = data,
                    Ref = 1
                };
                _database.Data.Add(entity);
            }
            else
            {
                entity.Ref += 1;
            }
            await _database.SaveChangesAsync();
            return tag;
        }
        public async Task FreeEntry(string tag)
        {
            if (tag == null)
                throw new ArgumentNullException(nameof(tag));
            var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync();
            if (entity != null)
            {
                if (entity.Ref == 1)
                {
                    _database.Data.Remove(entity);
                }
                else
                {
                    entity.Ref -= 1;
                }
                await _database.SaveChangesAsync();
            }
        }
        public async Task GetEntry(string tag)
        {
            if (tag == null)
                throw new ArgumentNullException(nameof(tag));
            var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync();
            if (entity is null)
                return null;
            return entity.Data;
        }
    }
    public static class DataManagerExtensions
    {
        /// 
        /// Try to get an entry and throw  if not exist.
        /// 
        public static async Task GetEntryAndCheck(this IDataManager dataManager, string tag, string notExistMessage)
        {
            var data = await dataManager.GetEntry(tag);
            if (data is null)
                throw new DatabaseCorruptedException($"Can't get data of tag {tag}. {notExistMessage}");
            return data;
        }
    }
}