using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Winbooks.Apis.Services;
using Winbooks.Apis.Services.v2;
using Winbooks.TORM.OM;
using Winbooks.TORM.OM.Accounting;
using Winbooks.TORM.DAL;
using Winbooks.TORM.DAL.Accounting;

namespace Winbooks.Apis.Linker
{
    public class EntityType
    {
        public const string ALDimension = "ALDimension";
        public const string ALAccount = "ALAccount";
        public const string Currency = "Currency";
    }

    /// <summary>
    /// This class implements a cache to retrieve the Id of the entities based on the code (method GetId).
    /// </summary>
    public class Cache
    {
        private static EntitiesServices _folder;
        private static UserCredential _userCredential;

        // Cached Id and entities
        private Dictionary<string, Vat> _vats = new Dictionary<string, Vat>();
        private Dictionary<string, Journal> _journals = new Dictionary<string, Journal>();
        private Dictionary<string, object> _entities = new Dictionary<string, object>();
        private Dictionary<string, Guid> _ids = new Dictionary<string, Guid>();

        private BookPeriod _bookPeriod = null;

        public Cache(EntitiesServices folder, UserCredential userCredential)
        {
            _folder = folder;
            _userCredential = userCredential;
        }

        /// <summary>
        /// Initialize de bookperiod where to post the documents.
        /// Throw an error (System.ArgumentException) if the bookperiod does not exist or is closed.
        /// </summary>
        /// <param name="month"></param>
        /// <param name="year"></param>
        /// <returns></returns>
        public BookPeriod SetBookPeriod(int month, int year)
        {
            _journals.Clear();
            _bookPeriod = null;

            _bookPeriod = GetBookPeriod(year, month);
            if (_bookPeriod == null)
                throw new System.ArgumentException(string.Format("BookPeriod {0}/{1} not found", month, year));

            if (_bookPeriod.IsClosed)
                throw new System.ArgumentException(string.Format("BookPeriod {0} is closed", _bookPeriod.Name));

            return _bookPeriod;
        }

        /// <summary>
        /// Return the book period corresponding to the year (between 2000 and ...) and month (between 1 and 12)
        /// </summary>
        /// <param name="year"></param>
        /// <param name="month"></param>
        /// <returns></returns>
        public BookPeriod GetBookPeriod(int year, int month)
        {
            if (year < 2000 || year > DateTime.Today.Year + 1)
                throw new System.ArgumentException(string.Format("GetBookPeriod: parameter year has to be between {0} and {1}", 2000, DateTime.Today.Year + 1));

            if (month < 1 || month > 12)
                throw new System.ArgumentException(string.Format("GetBookPeriod: parameter month has to be between {0} and {1}", 1, 12));

            var periodCriteria = new BookPeriodDAO().CreateCriteria()
                .Add(Condition.Eq(BookPeriod.Names.RelatedYear, year))
                .Add(Condition.Eq(BookPeriod.Names.RelatedMonth, month))
                .Add(Condition.NotEq(BookPeriod.Names.Number, 0))                    // to avoid opening period
                .Order(BookPeriod.Names.Number, true)                                // to avoid the closing period
                .SetFirstResult(0).SetMaxResults(1);

            BookPeriod bookPeriod = _folder.GetFilter<BookPeriod>(periodCriteria);
            return bookPeriod;
        }

        /// <summary>
        /// Return the next document number of the journal. SetBookPeriod has to be called before.
        /// </summary>
        /// <param name="journalCode">Code of the journal. Example: "VERKP"</param>
        /// <returns></returns>
        public long GetNextDocNumber(string journalCode)
        {
            Journal journal = GetJournal(journalCode); // if success, the journal is stored in _journal

            //GET LAST GLTRANSHEADER DOCUMENT NUMBER
            var lastHeaderCriteria = new GLTransHeaderDAO().CreateCriteria()
                .Add(Condition.Eq(GLTransHeader.Names.Journal_Id, journal.Id))
                .Order(GLTransHeader.Names.DocNumber, false)
                .SetFirstResult(0).SetMaxResults(1);

            GLTransHeader lastHeader = _folder.GetFilter<GLTransHeader>(lastHeaderCriteria);
            long documentNumber = 1;

            if (lastHeader != null)
            {
                documentNumber = lastHeader.DocNumber + 1;
            }
            return documentNumber;
        }

        /// <summary>
        /// Return the default GLAccount for Central Account Type Id
        /// </summary>
        /// <param name="id"></param>
        /// <returns>The default GLAccount for Central Account Type Id</returns>
        public GLAccount GetGLAccountByCentralAccountTypeId(System.Guid id)
        {
            if (id == System.Guid.Empty)
                return null;

            var criteria = new GLAccount_CentralAccountTypeDAO().CreateCriteria()
                .Add(Condition.Eq(GLAccount_CentralAccountType.Names.CentralAccountType_Id, id))
                .Order(GLAccount_CentralAccountType.Names.IsCustomized)
                .SetFirstResult(0).SetMaxResults(1);

            GLAccount_CentralAccountType glAccCentType = _folder.GetFilter<GLAccount_CentralAccountType>(criteria);
            if (glAccCentType == null)
                throw new ArgumentException(string.Format("No account found for CentralAccountType.Id '{0}'", id));

            if (glAccCentType.GLAccount_Id == System.Guid.Empty)
                return null;
            GLAccount glAccount = _folder.GetById<GLAccount>(glAccCentType.GLAccount_Id);
            return glAccount;
        }

        /// <summary>
        /// Return the default GLAccount for a central account type code
        /// </summary>
        /// <param name="code">Example: "CUSTOMER" or "SUPPLIER"</param>
        /// <returns>The default  GLAccount for the central account type</returns>
        public GLAccount GetGLAccountByCentralAccountTypeCode(string code)
        {
            if (string.IsNullOrEmpty(code))
                return null;

            CentralAccountType cat = _folder.GetByCode<CentralAccountType>(code);
            if (cat == null)
                throw new ArgumentException(string.Format("CentralAccountType.Code '{0}' not found", code));

            var criteria = new GLAccount_CentralAccountTypeDAO().CreateCriteria()
                .Add(Condition.Eq(GLAccount_CentralAccountType.Names.CentralAccountType_Id, cat.Id))
                .Order(GLAccount_CentralAccountType.Names.IsCustomized)
                .SetFirstResult(0).SetMaxResults(1);

            var gla_cat = _folder.GetFilter<GLAccount_CentralAccountType>(criteria);

            if (gla_cat == null)
                throw new ArgumentException(string.Format("No account found for CentralAccountType.Code '{0}'", code));

            var glAccount = _folder.GetById<GLAccount>(gla_cat.GLAccount_Id);

            //var criteria = new GLAccountDAO().CreateCriteria()
            //    .CreateAlias("gla_cat", typeof(GLAccount_CentralAccountType), GLAccount_CentralAccountType.Names.GLAccount_Id, GLAccount.Names.Id)
            //    .CreateAlias("cat", typeof(CentralAccountType), CentralAccountType.Names.Id, "gla_cat."+ GLAccount_CentralAccountType.Names.CentralAccountType_Id)
            //    .Add(Condition.Eq("cat."+CentralAccountType.Names.Code, code))
            //    .Order("gla_cat."+GLAccount_CentralAccountType.Names.IsCustomized)
            //    .SetFirstResult(0).SetMaxResults(1);

            //GLAccount glAccount = _folder.GetFilter<GLAccount>(criteria);

            return glAccount;
        }

        /// <summary>
        /// Return the GLAccount.Id corresponding to the CentralAccountType.Id
        /// For example, for Vat.Code = "6", "12" or "21", Vat.CentralAccountType.Code = "V03" which is linked to the GLAccount.Code = "451000"
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public Guid GetGLAccountIdByCentralAccountTypeId(Guid centralAccountTypeId)
        {
            if (centralAccountTypeId == Guid.Empty)
                return Guid.Empty;

            var criteria = new GLAccount_CentralAccountTypeDAO().CreateCriteria()
                .Add(Condition.Eq(GLAccount_CentralAccountType.Names.CentralAccountType_Id, centralAccountTypeId))
                .SetFirstResult(0).SetMaxResults(1);

            GLAccount_CentralAccountType glAccCentType = _folder.GetFilter<GLAccount_CentralAccountType>(criteria);
            if (glAccCentType == null)
                return Guid.Empty;
            return glAccCentType.GLAccount_Id;
        }

        /// <summary>
        /// Return the list of transactions (GLTrans) of a invoice
        /// </summary>
        /// <param name="journal"></param>
        /// <param name="number"></param>
        /// <returns>GLTrans ordered by LineOrder</returns>
        public IList<GLTrans> GetGLTranss(Journal journal, long number)
        {
            var criteria = new GLTransDAO().CreateCriteria()
                .CreateAlias("header", typeof(GLTransHeader), GLTransHeader.Names.Id, GLTrans.Names.GLTransHeader_Id)
                .Add(Condition.Eq("header." + GLTransHeader.Names.Journal_Id, journal.Id))
                .Add(Condition.Eq("header." + GLTransHeader.Names.DocNumber, number))
                .Order(GLTrans.Names.LineOrder)
                .SetFirstResult(0);

            IList<GLTrans> glTranss = _folder.GetFilterAll<GLTrans>(criteria);
            return glTranss;
        }

        /// <summary>
        /// Return the GLTrans on the customer or the supplier for the invoice specified by the journal-number
        /// </summary>
        /// <param name="journal"></param>
        /// <param name="number"></param>
        /// <returns>The GLTrans on the customer or the supplier for the invoice specified by the journal-number</returns>
        public GLTrans GetGLTrans0(Journal journal, long number)
        {
            var criteria = new GLTransDAO().CreateCriteria()
                .CreateAlias("header", typeof(GLTransHeader), GLTransHeader.Names.Id, GLTrans.Names.GLTransHeader_Id)
                .Add(Condition.Eq("header." + GLTransHeader.Names.Journal_Id, journal.Id))
                .Add(Condition.Eq("header." + GLTransHeader.Names.DocNumber, number))
                .Add(Condition.Eq(GLTrans.Names.LineOrder, 0))
                .Order(GLTrans.Names.LineOrder)
                .SetFirstResult(0)
                .SetMaxResults(1);

            GLTrans glTrans = _folder.GetFilter<GLTrans>(criteria);
            return glTrans;
        }

        #region Get Id and entities in the cache

        /// <summary>
        /// Return the entity corresponding to the code.
        /// </summary>
        /// <typeparam name="T">A class where the code is the unique identifier. Not Journal or BookYear or BookPeriod.</typeparam>
        /// <param name="code"></param>
        /// <returns>The entity corresponding to the code</returns>
        public T GetEntity<T>(string code) where T : BaseOM
        {
            if (string.IsNullOrEmpty(code))
                return default(T);
            if (typeof(T) == typeof(Journal))
                throw new ArgumentException("GetEntity<T>(code) cannot be called for Journal. Use GetJournal(code) instead.");
            if (typeof(T) == typeof(ALAccount))
                throw new ArgumentException("GetEntity<T>(code) cannot be called for ALAccount. Use GetALAccount(alDimension, code) instead.");

            string entityName = typeof(T).Name;
            int pos = entityName.LastIndexOf('.');
            if (pos >= 0)
                entityName = entityName.Substring(pos + 1);

            string key = entityName + ":" + code;
            if (_entities.ContainsKey(key))
                return (T)_entities[key];

            T entity = _folder.GetByCode<T>(code, true);

            if (entity == null)
                throw new ArgumentException(string.Format("{0} '{1}' does not exist", entityName, code));
            _entities.Add(key, entity);
            return entity;
        }

        /// <summary>
        /// Return the journal corresponding to the code and the bookyear set with SetBookPeriod().
        /// 
        /// </summary>
        /// <param name="code"></param>
        /// <returns></returns>
        public Journal GetJournal(string code)
        {
            if (_bookPeriod == null)
                throw new System.ArgumentException("SetBookPeriod() was not called before to call GetJournal");

            if (_journals.ContainsKey(code))
                return _journals[code];

            var journalCriteria = new JournalDAO().CreateCriteria()
                .Add(Condition.Eq(Journal.Names.BookYear_Id, _bookPeriod.BookYear_Id))
                .Add(Condition.Eq(Journal.Names.Code, code))
                .SetFirstResult(0).SetMaxResults(1);

            Journal journal = _folder.GetFilter<Journal>(journalCriteria);
            if (journal == null)
                throw new System.ArgumentException(string.Format("Journal {0} not found in {1}", code, _bookPeriod.BookYear.Name));
            if (journal.IsLocked)
                throw new System.ArgumentException(string.Format("The Journal {0} is locked in {1}", code, _bookPeriod.BookYear.Name));

            _journals.Add(code, journal);
            return journal;
        }

        public ALAccount GetALAccount(ALDimension alDimension, string code)
        {
            if (alDimension == null)
                throw new ArgumentException(string.Format("Call of GetALAccount(alDimension, '{0}') with alDimension == null", code));

            var criteria = new ALAccountDAO().CreateCriteria()
                .Add(Condition.Eq(ALAccount.Names.ALDimension_Id, alDimension.Id))
                .Add(Condition.Eq(ALAccount.Names.Code, code))
                .SetFirstResult(0).SetMaxResults(1);

            ALAccount alAccount = _folder.GetFilter<ALAccount>(criteria);
            if (alAccount == null)
                throw new ArgumentException(string.Format("The cost account {0} does not exist in the dimension {1}", code, alDimension.Code));
            if (alAccount.IsLocked)
                throw new ArgumentException(string.Format("The cost account {0} in the dimension {1} is locked", code, alDimension.Code));
            return alAccount;
        }

        /// <summary>
        /// Return the Id (Guid) of the entity based on the code. SetBookPeriod() has to be called before.
        /// Throw an error (System.ArgumentException) if the code is not found or is the entity is locked.
        /// </summary>
        /// <param name="entityType"></param>
        /// <param name="code"></param>
        /// <returns></returns>
        public Guid GetId(string entityType, string code, string codeALDimension = "")
        {
            if (_bookPeriod == null)
                throw new System.ArgumentException("SetBookPeriod() was not call or was called with invalid parameters before to call GetId");

            // Search in the cache
            string key = entityType.ToString() + ":" + code + entityType == EntityType.ALAccount ? "-" + codeALDimension : "";
            if (_ids.ContainsKey(key))
                return _ids[key];

            // Search in the DB
            ICriteria criteria = null;
            switch (entityType)
            {
                case EntityType.ALDimension:
                    criteria = new ALDimensionDAO().CreateCriteria();
                    break;
                case EntityType.ALAccount:
                    criteria = new ALAccountDAO().CreateCriteria();
                    break;
                case EntityType.Currency:
                    criteria = new CurrencyDAO().CreateCriteria();
                    break;
            }
            if (entityType.Equals(EntityType.ALAccount))
            {
                criteria.CreateAlias("dimension", typeof(ALDimension), ALDimension.Names.Id, ALAccount.Names.ALDimension_Id);
                criteria.Add(Condition.Eq("dimension.Code", codeALDimension));
            }
            criteria.Add(Condition.Eq("Code", code));
            criteria.SetProjection(Projections.Property("Id"));
            if (entityType != EntityType.Currency)
                criteria.SetProjection(Projections.Property("IsLocked"));

            criteria.SetMaxResults(1);
            Newtonsoft.Json.Linq.JArray id = _folder.GetFilterAll(criteria.JsonCriteriaSerialize(), entityType);

            if (id == null)
                throw new ArgumentException(string.Format("The {0} with code {1} does not exist", entityType, code));

            if (entityType != EntityType.Currency)
            {
                bool isLocked = bool.Parse(id[1].ToString());
                if (isLocked)
                    throw new ArgumentException(string.Format("The {0} with code {1} is locked", entityType, code));
            }

            Guid guid = Guid.Parse(id[0].ToString());

            // Save in the cache
            _ids.Add(key, guid);

            return Guid.Empty;
        }

        #endregion Get Id and entities in the cache

    }
}
