using System;
using System.Collections;
using System.Collections.Generic;
using Kreta.Framework.Util;

namespace Kreta.Framework.Entities
{
    /// <summary>
    /// Az entitások absztrakt ősosztálya.
    /// </summary>
    public abstract class Entity
    {
        // <<Layer Supertype>>

        //private static Hashtable m_EntityAttributeCache = Hashtable.Synchronized(new Hashtable());
        protected int m_ID;
        protected EntityState m_State;
        protected int m_Serial;
        protected DateTime? m_EntityLastChanged = null;
        protected DateTime? m_EntityCreated = null;
        protected bool m_Torolt = false;
        protected int? m_EntityModifier = null;
        protected int m_EntityCreator = -1;
        private Hashtable m_OriginalValues = new Hashtable();
        private Hashtable m_CurrentValues = new Hashtable();

        /// <summary>
        /// Az osztály alapértelmezett konstruktora.
        /// </summary>
        protected Entity()
        {
        }

        #region Belső dolgok

        /// <summary>
        /// Ellenőrzi, hogy az entitás módosítható állapotban van.
        /// </summary>
        /// <exception cref="EntityStateException">Ha nem módosítható az entitás</exception>
        protected void CheckModifyable()
        {
            if (!CanModifyFields)
            {
                throw new EntityStateException(m_State);
            }
        }

        /// <summary>
        /// Alapállapotba állítja az objektumot
        /// </summary>
        protected virtual void Reset()
        {
            m_State = EntityState.Uninitialized;
            m_ID = -1;
            m_Serial = -1;
            m_EntityCreated = null;
            m_EntityCreator = -1;
            m_EntityLastChanged = null;
            m_Torolt = false;
            m_EntityModifier = null;
            m_CurrentValues.Clear();
            m_OriginalValues.Clear();
        }

        /// <summary>
        /// Visszaadja az entitás adatbázisműveleteit végző <see cref="IEntityDataAccessor"/> objektumát.
        /// </summary>
        /// <returns>Az entitás adatbázisműveleteit végző <see cref="IEntityDataAccessor"/> objektum</returns>
        protected abstract IEntityDataAccessor GetDataAccessor();

        #endregion

        #region Nyilvános felület

        /// <summary>
        /// Visszaadja az entitás nevét.
        /// </summary>
        /// <remarks>
        /// Az entitás nevét a rajta lévő <see cref="EntityAttribute"/> attribútum definiálja.
        /// A névnek rendszer szinten egyedinek kell lennie.
        /// </remarks>
        public string GetEntityName()
        {
            return EntityAttributeCache.GetEntityNameFromCache(GetType());
        }

        /// <summary>
        /// Az entitás azonosítója
        /// </summary>
        /// <value>Az entitást azonosító nemnegatív egész szám</value>
        public int ID
        {
            get
            {
                return m_ID;
            }
            set
            {
                m_ID = value;
            }
        }

        /// <summary>
        /// Az entitás utolsó módosításának ideje
        /// </summary>
        public virtual DateTime? EntityLastChanged
        {
            get
            {
                return m_EntityLastChanged;
            }
            internal protected set
            {
                m_EntityLastChanged = value;
            }
        }

        /// <summary>
        /// Az entitás létrehozásának ideje
        /// </summary>
        public virtual DateTime? EntityCreated
        {
            get
            {
                return m_EntityCreated;
            }
            internal protected set
            {
                m_EntityCreated = value;
            }
        }

        /// <summary>
        /// Az entitás létrehozójának adatai
        /// </summary>
        public virtual int EntityCreator
        {
            get
            {
                return m_EntityCreator;
            }
            internal protected set
            {
                m_EntityCreator = value;
            }
        }

        /// <summary>
        /// Az entitás logikailag törölve van.
        /// </summary>
        public virtual bool Torolt
        {
            get
            {
                return m_Torolt;
            }
            set
            {
                m_Torolt = value;
            }
        }

        /// <summary>
        /// Az entitás módosítójának adatai
        /// </summary>
        public virtual int? EntityModifier
        {
            get
            {
                return m_EntityModifier;
            }
            internal protected set
            {
                m_EntityModifier = value;
            }
        }

        /// <summary>
        /// Az entitás verziója
        /// </summary>
        /// <value>Az entitás verziója</value>
        /// <remarks>Az entitás verziója kezdetben nulla, és minden mentésnél eggyel no.</remarks>
        public int Serial
        {
            get
            {
                return m_Serial;
            }
            set
            {
                m_Serial = value;
            }
        }

        /// <summary>
        /// Az entitás jelenlegi állapota.
        /// </summary>
        public EntityState State
        {
            get
            {
                return m_State;
            }
            set
            {
                SetState(value);
            }
        }

        /// <summary>
        /// Új állapotba hozza az entitást.
        /// </summary>
        /// <param name="state">Új állapot</param>
        public void SetState(EntityState state)
        {
            m_State = state;
        }

        /// <summary>
        /// Betöltött állapotba helyezi az entitást.
        /// </summary>
        public void SetLoaded()
        {
            m_State = EntityState.Initialized;
            StoreOriginalValues();
        }

        /// <summary>
        /// Ellenőrzi, hogy az entitás attribútumai érvényes értékeket tartalmaznak-e.
        /// </summary>
        /// <exception cref="InvalidEntityAttributeException">
        /// Ha valamelyik attribútum értéke érvénytelen.
        /// </exception>
        protected virtual void Validate(bool skipValidateAttributes = false)
        {
            // Az ősosztályban nem kell csinálnunk semmit.
        }

        #endregion

        #region Állapottal kapcsolatos dolgok

        /// <summary>
        /// Visszaadja, hogy az entitás be van-e már töltve.
        /// </summary>
        public bool IsInitialized
        {
            get
            {
                return (m_State == EntityState.Initialized || m_State == EntityState.Modified);
            }
        }

        /// <summary>
        /// Visszaadja, hogy az entitás módosítva lett-e.
        /// </summary>
        public bool IsModified
        {
            get
            {
                return (m_State == EntityState.Modified);
            }
        }

        /// <summary>
        /// Visszaadja, hogy az entitás törölve lett-e.
        /// </summary>
        public bool IsDeleted
        {
            get
            {
                return (m_State == EntityState.Removed);
            }
        }

        /// <summary>
        /// Ellenőrzi, hogy az entitás állapota megfelelő-e a betöltés művelethez.
        /// </summary>
        /// <value>True, ha az entitás betölthető; egyébként false</value>
        protected bool CanLoad
        {
            get
            {
                return (m_State == EntityState.Uninitialized);
            }
        }

        /// <summary>
        /// Ellenőrzi, hogy az entitás állapota megfelelő-e a beszúrás művelethez.
        /// </summary>
        /// <value>True, ha az entitás beszúrható az adatbázisba; egyébként false</value>
        protected bool CanInsert
        {
            get
            {
                return (m_State == EntityState.New || m_State == EntityState.Uninitialized);
            }
        }

        /// <summary>
        /// Ellenőrzi, hogy az entitás állapota megfelelő-e a módosítás művelethez.
        /// </summary>
        /// <value>True, ha az entitás módosítható az adatbázisban; egyébként false</value>
        protected bool CanUpdate
        {
            get
            {
                return (m_State == EntityState.Modified || m_State == EntityState.Initialized);
            }
        }

        /// <summary>
        /// Ellenőrzi, hogy az entitás állapota megfelelő-e a törléshez.
        /// </summary>
        /// <value>True, ha az entitás törölhető az adatbázisban; egyébként false</value>
        protected bool CanRemove
        {
            get
            {
                return (m_State != EntityState.Uninitialized);
            }
        }

        /// <summary>
        /// Ellenőrzi, hogy az entitás állapota megfelelő-e az attribútumainak módosításához.
        /// </summary>
        /// <value>True, ha az entitás attribútumai módosíthatóak; egyébként false</value>
        protected bool CanModifyFields
        {
            get
            {
                return (m_State != EntityState.Removed);
            }
        }

        #endregion

        #region CRUD logika

        /// <summary>
        /// Azonosító alapján feltölti az entitást az adatbázisból
        /// </summary>
        /// <param name="id">Az entitás azonosítója</param>
        /// <returns>True, ha sikeres; egyébként False</returns>
        public bool LoadEntityByID(int id)
        {
            if (m_State != EntityState.Uninitialized)
            {
                throw new EntityStateException(m_State);
            }
            m_ID = id;
            if (!GetDataAccessor().LoadEntity(this, id))
            {
                m_ID = -1;
                return false;
            }
            return true;
        }

        /// <summary>
        /// Azonosító alapján feltölti az entitást az adatbázisból, minden oszloppal
        /// </summary>
        /// <param name="id">Az entitás azonosítója</param>
        /// <exception cref="EntityNotFoundException">Ha a megadott azonosítójú entitás nem található.</exception>
        public void LoadByID(int id)
        {
            if (m_State != EntityState.Uninitialized)
            {
                throw new EntityStateException(m_State);
            }
            m_ID = id;
            if (!GetDataAccessor().LoadEntity(this, id))
            {
                m_ID = -1;
                throw new EntityNotFoundException(GetEntityName(), id);
            }
        }

        /// <summary>
        /// Azonosító alapján feltölti az entitást az adatbázisból, tiltó oszlopszűréssel
        /// </summary>
        /// <param name="id">Az entitás azonosítója</param>
        /// <param name="filteredColumns">A betöltésre nem kerülő oszlopok</param>
        public void LoadByID(int id, IEnumerable<string> filteredColumns)
        {
            this.LoadByID(id, ColumnFilterMode.DEFAULT_ALLOWED, filteredColumns);
        }

        /// <summary>
        /// Azonosító alapján feltölti az entitást az adatbázisból, oszlopszűréssel
        /// </summary>
        /// <param name="id">Az entitás azonosítója</param>
        /// <param name="columnFilterMode">A szűrés módja, megengedő vagy tiltó</param>
        /// <param name="columns">A szűrendő oszlopok felsorolása</param>
        public void LoadByID(int id, ColumnFilterMode columnFilterMode, IEnumerable<string> columns)
        {
            if (m_State != EntityState.Uninitialized)
            {
                throw new EntityStateException(m_State);
            }
            m_ID = id;
            if (!GetDataAccessor().FilteredLoadEntity(this, id, columnFilterMode, columns))
            {
                m_ID = -1;
                throw new EntityNotFoundException(GetEntityName(), id);
            }
        }

        /// <summary>
        /// Azonosító alapján feltölti az entitást adatbázisból.
        /// </summary>
        /// <param name="id">Az entitás azonosítója</param>
        /// <param name="serial">Az entitás verziószáma</param>
        /// <exception cref="EntityExpiredException">Ha az entitás verziószáma nem egyezik a megadottal.</exception>
        /// <exception cref="EntityNotFoundException">Ha a megadott azonosítójú entitás nem található.</exception>
        public void LoadByID(int id, int serial)
        {
            LoadByID(id);

            if (m_Serial != serial)
            {
                throw new EntityExpiredException(GetEntityName(), ID, m_Serial, serial);
            }
        }

        /// <summary>
        /// Azonosító alapján feltölti az entitást adatbázisból, oszlopszűréssel
        /// </summary>
        /// <param name="id"></param>
        /// <param name="serial"></param>
        /// <param name="columnFilterMode"></param>
        /// <param name="columns"></param>
        public void LoadByID(int id, int serial, ColumnFilterMode columnFilterMode, IEnumerable<string> columns)
        {
            LoadByID(id, columnFilterMode, columns);

            if (m_Serial != serial)
            {
                throw new EntityExpiredException(GetEntityName(), ID, m_Serial, serial);
            }
        }

        /// <summary>
        /// Adatbázisba menti a még nem létező entitást.
        /// </summary>
        public void Insert()
        {
            Insert(true);
        }

        /// <summary>
        /// Adatbázisba menti a még nem létező entitást.
        /// </summary>
        /// <param name="runHandler">Futtassa-e a hozzá tartozó <see cref="EntityHandler"/> objektumot, vagy sem</param>
        public void Insert(bool runHandler)
        {
            try
            {
                // <<Template Method>>
                IEntityHandler handler = null;
                if (runHandler)
                {
                    handler = EntityHandler.Create(GetType());
                    handler.BeforeInsert(this);
                }

                DoInsert();

                if (runHandler)
                {
                    handler.AfterInsert(this);
                }
            }
            catch
            {
                EntityState es = this.State;
                this.State = EntityState.New;  // biztos ami biztos
                this.StoreOriginalValues(); // hogy legyen mit logolni
                //EntityHistoryLogger.LogErrorHistory(this);
                this.State = es;
                throw;
            }
        }

        /// <summary>
        /// Adatbázisba menti a már létező entitás tulajdonságait.
        /// </summary>
        public void Update()
        {
            Update(true);
        }

        /// <summary>
        /// Adatbázisba menti a már létező entitás tulajdonságait.
        /// </summary>
        /// <param name="runHandler">Futtassa-e a hozzá tartozó <see cref="EntityHandler"/> objektumot, vagy sem</param>
        public void Update(bool runHandler)
        {
            try
            {
                // <<Template Method>>
                IEntityHandler handler = null;
                if (runHandler)
                {
                    handler = EntityHandler.Create(GetType());
                    handler.BeforeUpdate(this);
                }

                DoUpdate(true, false);

                if (runHandler)
                {
                    handler.AfterUpdate(this);
                }
            }
            catch
            {
                EntityState es = this.State;
                this.State = EntityState.Modified;  // különben nem logol
                //EntityHistoryLogger.LogErrorHistory(this);
                this.State = es;
                throw;
            }
        }

        /// <summary>
        /// Adatbázisba menti az entitás asszociációit.
        /// </summary>
        /// <remarks>Valójában csak azokat, amelyeknél a kapcsolómező ennek az entitásnak a táblájában van!</remarks>
        public void UpdateAssociations()
        {
            UpdateAssociations(true);
        }

        /// <summary>
        /// Adatbázisba menti az entitás asszociációit.
        /// </summary>
        /// <param name="runHandler">Futtassa-e a hozzá tartozó <see cref="EntityHandler"/> objektumot, vagy sem</param>
        /// <remarks>Valójában csak azokat, amelyeknél a kapcsolómező ennek az entitásnak a táblájában van!</remarks>
        public void UpdateAssociations(bool runHandler)
        {
            try
            {
                // <<Template Method>>
                IEntityHandler handler = null;
                if (runHandler)
                {
                    handler = EntityHandler.Create(GetType());
                    handler.BeforeUpdate(this);
                }

                DoUpdate(false, true);

                if (runHandler)
                {
                    handler.AfterUpdate(this);
                }
            }
            catch
            {
                EntityState es = this.State;
                this.State = EntityState.Modified;  // különben nem logol
                //EntityHistoryLogger.LogErrorHistory(this);
                this.State = es;
                throw;
            }
        }

        /// <summary>
        /// Adatbázisba menti a már létező entitás tulajdonságait és asszociációit.
        /// </summary>
        public void FullUpdate()
        {
            FullUpdate(true);
        }

        /// <summary>
        /// Adatbázisba menti a már létező entitás tulajdonságait és asszociációit.
        /// </summary>
        /// <param name="runHandler">Futtassa-e a hozzá tartozó <see cref="EntityHandler"/> objektumot, vagy sem</param>
        public void FullUpdate(bool runHandler)
        {
            try
            {
                // <<Template Method>>
                IEntityHandler handler = null;
                if (runHandler)
                {
                    handler = EntityHandler.Create(GetType());
                    handler.BeforeUpdate(this);
                }

                DoUpdate(true, true);

                if (runHandler)
                {
                    handler.AfterUpdate(this);
                }
            }
            catch
            {
                EntityState es = this.State;
                this.State = EntityState.Modified;  // különben nem logol
                //EntityHistoryLogger.LogErrorHistory(this);
                this.State = es;
                throw;
            }
        }

        /// <summary>
        /// Törli az entitást az adatbázisból.
        /// </summary>
        /// <remarks>Sikeres törlés esetén az entitás 'EntityState.REMOVED' állapotba kerül.</remarks>
        public void Delete(bool logikai = true)
        {
            Delete(true, logikai);
        }

        /// <summary>
        /// Leellenőrzi az aktív kapcsolatait törléshez.
        /// </summary>
        void CheckActiveConnections()
        {
            var entityNames = new List<string>() { GetEntityName() };

            var entityType = this.GetType();
            if (entityType.BaseType != typeof(Entity))
            {
                entityNames.Add(EntityAttributeCache.GetEntityNameFromCache(entityType.BaseType));
            }

            var EntitasKapcsolatok = EntityUtils.GetEntitiesConnections(new List<int>() { this.ID }, entityNames);

            // ha van az entitásnak kapcsolata, akkor elszáll
            if (EntitasKapcsolatok.Count > 0 && EntitasKapcsolatok[ID].Count > 0)
            {
                throw new EntityDeleteFailedException(StringResourcesUtil.GetString(3478), EntitasKapcsolatok[ID]);
            }
        }

        /// <summary>
        /// Törli az entitást az adatbázisból.
        /// </summary>
        /// <param name="runHandler">Futtassa-e a hozzá tartozó <see cref="EntityHandler"/> objektumot, vagy sem</param>
        public void Delete(bool runHandler, bool logikai = true)
        {
            try
            {
                // <<Template Method>>
                IEntityHandler handler = null;
                if (runHandler)
                {
                    handler = EntityHandler.Create(GetType());
                    handler.BeforeDelete(this);
                }

                // ellenőrzi az aktív kapcsolatait
                CheckActiveConnections();
                DoDelete(false, runHandler, logikai);

                if (runHandler)
                {
                    handler.AfterDelete(this);
                }
            }
            catch /*(Exception ex)*/
            {
                EntityState es = this.State;
                this.State = EntityState.Removed;  // biztos ami biztos
                //EntityHistoryLogger.LogErrorHistory(this);
                this.State = es;
                throw;
            }
        }

        /// <summary>
        /// Törli az entitást az adatbázisból a kapcsolódásaival együtt.
        /// </summary>
        /// <remarks>Sikeres törlés esetén az entitás 'EntityState.REMOVED' állapotba kerül.</remarks>
        public void CascadeDelete(bool logikai = true)
        {
            CascadeDelete(false, logikai);
        }

        /// <summary>
        /// Törli az entitást az adatbázisból a kapcsolódásaival együtt.
        /// </summary>
        /// <param name="runHandler">Futtassa-e a hozzá tartozó <see cref="EntityHandler"/> objektumot, vagy sem</param>
        public void CascadeDelete(bool runHandler, bool logikai = true)
        {
            try
            {
                // <<Template Method>>
                IEntityHandler handler = null;
                if (runHandler)
                {
                    handler = EntityHandler.Create(GetType());
                    handler.BeforeDelete(this);
                }

                DoDelete(true, runHandler);

                if (runHandler)
                {
                    handler.AfterDelete(this);
                }
            }
            catch
            {
                EntityState es = this.State;
                this.State = EntityState.Removed;  // biztos ami biztos
                //EntityHistoryLogger.LogErrorHistory(this);
                this.State = es;
                throw;
            }
        }

        /// <summary>
        /// Elvégzi az entitás törlése előtti szétkapcsolásokat és kaszkád törléseket.
        /// </summary>
        protected virtual void DeAssociateBeforeDelete(bool runHandler = false)
        {
            // Az ősosztályban nem kell csinálnunk semmit.
        }

        /// <summary>
        /// Elvégzi az entitás törlését.
        /// </summary>
        /// <param name="performCascadeDelete">Kaszkádosított törlés legyen-e, vagy sem</param>
        protected virtual void DoDelete(bool performCascadeDelete, bool runHandler = false, bool logikai = true)
        {
            if (!CanRemove)
            {
                throw new EntityStateException(m_State);
            }

            if (performCascadeDelete)
            {
                DeAssociateBeforeDelete(runHandler);
            }

            GetDataAccessor().DeleteEntity(this, logikai);
            SetState(EntityState.Removed);
            //EntityHistoryLogger.LogHistory(this);
        }

        /// <summary>
        /// elvegzi az entitás update-jet
        /// </summary>
        protected virtual void DoUpdate(bool withattributes, bool withassociations)
        {
            if (!CanUpdate)
            {
                throw new EntityStateException(m_State);
            }

            Validate(!withattributes);

            if (withattributes)
            {
                if (!GetDataAccessor().UpdateEntity(this))
                {
                    throw new EntityExpiredException(GetEntityName(), ID, m_Serial);
                }
            }

            if (withassociations)
            {
                if (!GetDataAccessor().UpdateAssociations(this))
                {
                    throw new EntityExpiredException(GetEntityName(), ID, m_Serial);
                }
            }
            //EntityHistoryLogger.LogHistory(this);
            SetState(EntityState.Initialized);
        }

        /// <summary>
        /// elvegzi az entitas insertjet
        /// </summary>
        protected virtual void DoInsert()
        {
            if (!CanInsert)
            {
                throw new EntityStateException(m_State);
            }

            Validate();

            GetDataAccessor().InsertEntity(this);

            StoreOriginalValues();

            //EntityHistoryLogger.LogHistory(this);
            SetState(EntityState.Initialized);
        }

        #endregion

        #region Oszlopszintű naplózás támogatása

        /// <summary>
        /// Eltárolja az entitás összes attribútumának adatbázisban tárolt értékét.
        /// </summary>
        protected virtual void StoreOriginalValues()
        {
            m_OriginalValues.Clear();
        }

        /// <summary>
        /// A függvény eltárolja egy attribútum értékének megváltozását.
        /// </summary>
        /// <param name="attributeName">A módosult attribútum neve</param>
        /// <param name="value">A módosult attribútum új értéke</param>
        /// <remarks>Ezt a metódust meg kell hívni minden olyan esetben, amikor olyan attribútum módosult,
        /// amelyet oszlop szinten naplózni kell.</remarks>
        protected void FieldModified(string attributeName, object value)
        {
            if (string.IsNullOrWhiteSpace(attributeName))
            {
                throw new ArgumentNullException(nameof(attributeName));
            }

            if (m_State == EntityState.Initialized)
            {
                SetState(EntityState.Modified);
            }
            else if (m_State == EntityState.Uninitialized)
            {
                SetState(EntityState.New);
            }

            m_CurrentValues[attributeName] = (value ?? DBNull.Value);
        }

        /// <summary>
        /// Az entitás eredeti naplózandó attribútumai.
        /// </summary>
        protected internal Hashtable OriginalValues
        {
            get
            {
                return m_OriginalValues;
            }
        }

        /// <summary>
        /// Az entitás módosult naplózandó attribútumai a jelenlegi értékükkel.
        /// </summary>
        protected internal Hashtable CurrentValues
        {
            get
            {
                return m_CurrentValues;
            }
        }

        /// <summary>
        /// Visszaadja egy attribútum eredeti értékét.
        /// </summary>
        /// <param name="attributeName">Az attribútum neve</param>
        /// <returns>Az attribútum eredeti értéke</returns>
        /// <exception cref="ArgumentException">Ha az entitás nem rendelkezik a magadott attribútummal, vagy az nem naplózandó.</exception>
        public object GetOriginalValue(string attributeName)
        {
            if (!m_OriginalValues.ContainsKey(attributeName))
            {
                throw new ArgumentException("The entity does not contain the original value for the specified attribute.", nameof(attributeName));
            }

            object result = m_OriginalValues[attributeName];

            if (result == DBNull.Value)
            {
                return null;
            }

            return result;
        }

        /// <summary>
        /// Megvizsgálja, hogy egy attribútum eredeti értéke null-e, vagy sem.
        /// </summary>
        /// <param name="attributeName">Az attribútum neve</param>
        /// <returns>True, ha az attribútum eredeti értéke null; egyébként false</returns>
        /// <exception cref="ArgumentException">Ha az entitás nem rendelkezik a magadott attribútummal, vagy az nem naplózandó.</exception>
        public bool IsOriginalValueNull(string attributeName)
        {
            return (GetOriginalValue(attributeName) == null);
        }

        /// <summary>
        /// Megvizsgálja, hogy egy attribútum értéke változott-e, vagy sem.
        /// </summary>
        /// <param name="attributeName">Az attribútum neve</param>
        /// <returns>True, ha változott; egyébként false</returns>
        public bool HasChanged(string attributeName)
        {
            return m_CurrentValues.ContainsKey(attributeName);
        }

        public IEnumerable<string> ChangedAttributes()
        {
            string[] result = new string[m_CurrentValues.Count];
            m_CurrentValues.Keys.CopyTo(result, 0);
            return result;
        }

        #endregion
    }
}