/*
 * Copyright (C) 2012-2017 CypherCore <http://github.com/CypherCore>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using Framework.Constants;
using Framework.GameMath;
using Game.AI;
using Game.Maps;
using Game.Movement;
using Game.Network.Packets;
using Game.Spells;
using System;
using System.Collections.Generic;

namespace Game.Entities
{
    public class AreaTrigger : WorldObject
    {
        public AreaTrigger() : base(false)
        {
            _previousCheckOrientation = float.PositiveInfinity;
            _reachedDestination = true;

            objectTypeMask |= TypeMask.AreaTrigger;
            objectTypeId = TypeId.AreaTrigger;

            m_updateFlag = UpdateFlag.StationaryPosition | UpdateFlag.Areatrigger;

            valuesCount = (int)AreaTriggerFields.End;

            _spline = new Spline();
        }

        public override void AddToWorld()
        {
            // Register the AreaTrigger for guid lookup and for caster
            if (!IsInWorld)
            {
                GetMap().GetObjectsStore().Add(GetGUID(), this);
                base.AddToWorld();
            }
        }

        public override void RemoveFromWorld()
        {
            // Remove the AreaTrigger from the accessor and from all lists of objects in world
            if (IsInWorld)
            {
                _isRemoved = true;

                Unit caster = GetCaster();
                if (caster)
                    caster._UnregisterAreaTrigger(this);

                // Handle removal of all units, calling OnUnitExit & deleting auras if needed
                HandleUnitEnterExit(new List<Unit>());

                _ai.OnRemove();

                base.RemoveFromWorld();
                GetMap().GetObjectsStore().Remove(GetGUID());
            }
        }

        public bool CreateAreaTrigger(uint spellMiscId, Unit caster, Unit target, SpellInfo spell, Position pos, int duration, uint spellXSpellVisualId, ObjectGuid castId = default(ObjectGuid), AuraEffect aurEff = null)
        {
            _targetGuid = target ? target.GetGUID() : ObjectGuid.Empty;
            _aurEff = aurEff;

            SetMap(caster.GetMap());
            Relocate(pos);
            if (!IsPositionValid())
            {
                Log.outError(LogFilter.AreaTrigger, "AreaTrigger (spell {0}) not created. Invalid coordinates (X: {0} Y: {1})", spell.Id, GetPositionX(), GetPositionY());
                return false;
            }

            _areaTriggerMiscTemplate = Global.AreaTriggerDataStorage.GetAreaTriggerMiscTemplate(spellMiscId);
            if (_areaTriggerMiscTemplate == null)
            {
                Log.outError(LogFilter.AreaTrigger, "AreaTrigger (spellMiscId {0}) not created. Invalid areatrigger miscid ({1})", spellMiscId, spellMiscId);
                return false;
            }

            _Create(ObjectGuid.Create(HighGuid.AreaTrigger, GetMapId(), GetTemplate().Id, caster.GetMap().GenerateLowGuid(HighGuid.AreaTrigger)));
            SetPhaseMask(caster.GetPhaseMask(), false);

            SetEntry(GetTemplate().Id);
            SetDuration(duration);

            SetObjectScale(1.0f);

            SetGuidValue(AreaTriggerFields.Caster, caster.GetGUID());
            SetGuidValue(AreaTriggerFields.CreatingEffectGuid, castId);

            SetUInt32Value(AreaTriggerFields.SpellId, spell.Id);
            SetUInt32Value(AreaTriggerFields.SpellForVisuals, spell.Id);
            SetUInt32Value(AreaTriggerFields.SpellXSpellVisualId, spellXSpellVisualId);
            SetUInt32Value(AreaTriggerFields.TimeToTargetScale, GetMiscTemplate().TimeToTargetScale != 0 ? GetMiscTemplate().TimeToTargetScale : GetUInt32Value(AreaTriggerFields.Duration));
            SetFloatValue(AreaTriggerFields.BoundsRadius2d, GetTemplate().MaxSearchRadius);
            SetUInt32Value(AreaTriggerFields.DecalPropertiesId, GetMiscTemplate().DecalPropertiesId);

            for (byte scaleCurveIndex = 0; scaleCurveIndex < SharedConst.MaxAreatriggerScale; ++scaleCurveIndex)
                if (GetMiscTemplate().ScaleInfo.ExtraScale[scaleCurveIndex].AsInt32 != 0)
                    SetUInt32Value(AreaTriggerFields.ExtraScaleCurve + scaleCurveIndex, (uint)GetMiscTemplate().ScaleInfo.ExtraScale[scaleCurveIndex].AsInt32);

            CopyPhaseFrom(caster);

            if (target && GetTemplate().HasFlag(AreaTriggerFlags.HasAttached))
            {
                m_movementInfo.transport.guid = target.GetGUID();
            }

            UpdateShape();

            if (GetMiscTemplate().HasSplines())
            {
                uint timeToTarget = GetMiscTemplate().TimeToTarget != 0 ? GetMiscTemplate().TimeToTarget : GetUInt32Value(AreaTriggerFields.Duration);
                InitSplineOffsets(GetMiscTemplate().SplinePoints, timeToTarget);
            }

            // movement on transport of areatriggers on unit is handled by themself
            Transport transport = m_movementInfo.transport.guid.IsEmpty() ? caster.GetTransport() : null;
            if (transport)
            {
                float x, y, z, o;
                pos.GetPosition(out x, out y, out z, out o);
                transport.CalculatePassengerOffset(ref x, ref y, ref z, ref o);
                m_movementInfo.transport.pos.Relocate(x, y, z, o);

                // This object must be added to transport before adding to map for the client to properly display it
                transport.AddPassenger(this);
            }

            AI_Initialize();

            if (!GetMap().AddToMap(this))
            {         // Returning false will cause the object to be deleted - remove from transport
                if (transport)
                    transport.RemovePassenger(this);
                return false;
            }

            caster._RegisterAreaTrigger(this);

            _ai.OnCreate();

            return true;
        }

        public override void Update(uint diff)
        {
            base.Update(diff);
            _timeSinceCreated += diff;

            if (GetTemplate().HasFlag(AreaTriggerFlags.HasAttached))
            {
                Unit target = GetTarget();
                if (target)
                    GetMap().AreaTriggerRelocation(this, target.GetPositionX(), target.GetPositionY(), target.GetPositionZ(), target.GetOrientation());
            }
            else
                UpdateSplinePosition(diff);

            if (GetDuration() != -1)
            {
                if (GetDuration() > diff)
                    _UpdateDuration((int)(_duration - diff));
                else
                {
                    Remove(); // expired
                    return;
                }
            }

            _ai.OnUpdate(diff);

            UpdateTargetList();
        }

        public void Remove()
        {
            if (IsInWorld)           
                AddObjectToRemoveList();
        }

        public void SetDuration(int newDuration)
        {
            _duration = newDuration;
            _totalDuration = newDuration;

            // negative duration (permanent areatrigger) sent as 0
            SetUInt32Value(AreaTriggerFields.Duration, (uint)Math.Max(newDuration, 0));
        }

        void _UpdateDuration(int newDuration)
        {
            _duration = newDuration;

            // should be sent in object create packets only
            UpdateData[(int)AreaTriggerFields.Duration] = _duration;
        }

        float GetProgress()
        {
            return GetTimeSinceCreated() < GetTimeToTargetScale() ? GetTimeSinceCreated() / GetTimeToTargetScale() : 1.0f;
        }

        void UpdateTargetList()
        {
            List<Unit> targetList = new List<Unit>();

            switch (GetTemplate().TriggerType)
            {
                case AreaTriggerTypes.Sphere:
                    SearchUnitInSphere(targetList);
                    break;
                case AreaTriggerTypes.Box:
                    SearchUnitInBox(targetList);
                    break;
                case AreaTriggerTypes.Polygon:
                    SearchUnitInPolygon(targetList);
                    break;
                case AreaTriggerTypes.Cylinder:
                    SearchUnitInCylinder(targetList);
                    break;
                default:
                    break;
            }

            HandleUnitEnterExit(targetList);
        }

        void SearchUnitInSphere(List<Unit> targetList)
        {
            float radius = GetTemplate().SphereDatas.Radius;
            if (GetTemplate().HasFlag(AreaTriggerFlags.HasDynamicShape))
            {
                if (GetMiscTemplate().MorphCurveId != 0)
                {
                    radius = MathFunctions.lerp(GetTemplate().SphereDatas.Radius, GetTemplate().SphereDatas.RadiusTarget, Global.DB2Mgr.GetCurveValueAt(GetMiscTemplate().MorphCurveId, GetProgress()));
                }
            }

            var check = new AnyUnitInObjectRangeCheck(this, radius);
            var searcher = new UnitListSearcher(this, targetList, check);
            Cell.VisitAllObjects(this, searcher, GetTemplate().MaxSearchRadius);
        }

        void SearchUnitInBox(List<Unit> targetList)
        {
            float extentsX, extentsY, extentsZ;

            unsafe
            {
                fixed (float* ptr = GetTemplate().BoxDatas.Extents)
                {
                    extentsX = ptr[0];
                    extentsY = ptr[1];
                    extentsZ = ptr[2];
                }
            }

            var check = new AnyUnitInObjectRangeCheck(this, GetTemplate().MaxSearchRadius, false);
            var searcher = new UnitListSearcher(this, targetList, check);
            Cell.VisitAllObjects(this, searcher, GetTemplate().MaxSearchRadius);

            float halfExtentsX = extentsX / 2.0f;
            float halfExtentsY = extentsY / 2.0f;
            float halfExtentsZ = extentsZ / 2.0f;

            float minX = GetPositionX() - halfExtentsX;
            float maxX = GetPositionX() + halfExtentsX;

            float minY = GetPositionY() - halfExtentsY;
            float maxY = GetPositionY() + halfExtentsY;

            float minZ = GetPositionZ() - halfExtentsZ;
            float maxZ = GetPositionZ() + halfExtentsZ;

            AxisAlignedBox box = new AxisAlignedBox(new Vector3(minX, minY, minZ), new Vector3(maxX, maxY, maxZ));

            targetList.RemoveAll(unit =>
            {
                return !box.contains(new Vector3(unit.GetPositionX(), unit.GetPositionY(), unit.GetPositionZ()));
            });
        }

        void SearchUnitInPolygon(List<Unit> targetList)
        {
            var check = new AnyUnitInObjectRangeCheck(this, GetTemplate().MaxSearchRadius, false);
            var searcher = new UnitListSearcher(this, targetList, check);
            Cell.VisitAllObjects(this, searcher, GetTemplate().MaxSearchRadius);

            float height = GetTemplate().PolygonDatas.Height;
            float minZ = GetPositionZ() - height;
            float maxZ = GetPositionZ() + height;

            targetList.RemoveAll(unit =>
            {
                return !CheckIsInPolygon2D(unit)
                    || unit.GetPositionZ() < minZ
                    || unit.GetPositionZ() > maxZ;
            });
        }

        void SearchUnitInCylinder(List<Unit> targetList)
        {
            var check = new AnyUnitInObjectRangeCheck(this, GetTemplate().MaxSearchRadius, false);
            var searcher = new UnitListSearcher(this, targetList, check);
            Cell.VisitAllObjects(this, searcher, GetTemplate().MaxSearchRadius);

            float height = GetTemplate().CylinderDatas.Height;
            float minZ = GetPositionZ() - height;
            float maxZ = GetPositionZ() + height;

            targetList.RemoveAll(unit =>
            {
                return unit.GetPositionZ() < minZ
                    || unit.GetPositionZ() > maxZ;
            });
        }

        void HandleUnitEnterExit(List<Unit> newTargetList)
        {
            List<ObjectGuid> exitUnits = _insideUnits;
            _insideUnits.Clear();

            List<Unit> enteringUnits = new List<Unit>();

            foreach (Unit unit in newTargetList)
            {
                if (!exitUnits.Remove(unit.GetGUID())) // erase(key_type) returns number of elements erased
                    enteringUnits.Add(unit);

                _insideUnits.Add(unit.GetGUID());
            }

            // Handle after _insideUnits have been reinserted so we can use GetInsideUnits() in hooks
            foreach (Unit unit in enteringUnits)
            {
                Player player = unit.ToPlayer();
                if (player)
                    if (player.isDebugAreaTriggers)
                        player.SendSysMessage(CypherStrings.DebugAreatriggerEntered, GetTemplate().Id);

                DoActions(unit);

                _ai.OnUnitEnter(unit);
            }

            foreach (ObjectGuid exitUnitGuid in exitUnits)
            {
                Unit leavingUnit = Global.ObjAccessor.GetUnit(this, exitUnitGuid);
                if (leavingUnit)
                {
                    Player player = leavingUnit.ToPlayer();
                    if (player)
                        if (player.isDebugAreaTriggers)
                            player.SendSysMessage(CypherStrings.DebugAreatriggerLeft, GetTemplate().Id);

                    UndoActions(leavingUnit);

                    _ai.OnUnitExit(leavingUnit);
                }
            }
        }

        public AreaTriggerTemplate GetTemplate()
        {
            return _areaTriggerMiscTemplate.Template;
        }

        public uint GetScriptId()
        {
            return GetTemplate().ScriptId;
        }

        public Unit GetCaster()
        {
            return Global.ObjAccessor.GetUnit(this, GetCasterGuid());
        }

        Unit GetTarget()
        {
            return Global.ObjAccessor.GetUnit(this, _targetGuid);
        }

        void UpdatePolygonOrientation()
        {
            float newOrientation = GetOrientation();

            // No need to recalculate, orientation didn't change
            if (MathFunctions.fuzzyEq(_previousCheckOrientation, newOrientation))
                return;

            _polygonVertices = GetTemplate().PolygonVertices;

            float angleSin = (float)Math.Sin(newOrientation);
            float angleCos = (float)Math.Cos(newOrientation);

            // This is needed to rotate the vertices, following orientation
            for (var i = 0; i < _polygonVertices.Count; ++i)
            {
                Vector2 vertice = _polygonVertices[i];

                vertice.X = vertice.X * angleCos - vertice.Y * angleSin;
                vertice.Y = vertice.Y * angleCos + vertice.X * angleSin;
            }

            _previousCheckOrientation = newOrientation;
        }

        bool CheckIsInPolygon2D(Position pos)
        {
            float testX = pos.GetPositionX();
            float testY = pos.GetPositionY();

            //this method uses the ray tracing algorithm to determine if the point is in the polygon
            bool locatedInPolygon = false;

            for (int vertex = 0; vertex < _polygonVertices.Count; ++vertex)
            {
                int nextVertex;

                //repeat loop for all sets of points
                if (vertex == (_polygonVertices.Count - 1))
                {
                    //if i is the last vertex, let j be the first vertex
                    nextVertex = 0;
                }
                else
                {
                    //for all-else, let j=(i+1)th vertex
                    nextVertex = vertex + 1;
                }

                float vertX_i = GetPositionX() + _polygonVertices[vertex].X;
                float vertY_i = GetPositionY() + _polygonVertices[vertex].Y;
                float vertX_j = GetPositionX() + _polygonVertices[nextVertex].X;
                float vertY_j = GetPositionY() + _polygonVertices[nextVertex].Y;

                // following statement checks if testPoint.Y is below Y-coord of i-th vertex
                bool belowLowY = vertY_i > testY;
                // following statement checks if testPoint.Y is below Y-coord of i+1-th vertex
                bool belowHighY = vertY_j > testY;

                /* following statement is true if testPoint.Y satisfies either (only one is possible)
                -.(i).Y < testPoint.Y < (i+1).Y        OR
                -.(i).Y > testPoint.Y > (i+1).Y

                (Note)
                Both of the conditions indicate that a point is located within the edges of the Y-th coordinate
                of the (i)-th and the (i+1)- th vertices of the polygon. If neither of the above
                conditions is satisfied, then it is assured that a semi-infinite horizontal line draw
                to the right from the testpoint will NOT cross the line that connects vertices i and i+1
                of the polygon
                */
                bool withinYsEdges = belowLowY != belowHighY;

                if (withinYsEdges)
                {
                    // this is the slope of the line that connects vertices i and i+1 of the polygon
                    float slopeOfLine = (vertX_j - vertX_i) / (vertY_j - vertY_i);

                    // this looks up the x-coord of a point lying on the above line, given its y-coord
                    float pointOnLine = (slopeOfLine * (testY - vertY_i)) + vertX_i;

                    //checks to see if x-coord of testPoint is smaller than the point on the line with the same y-coord
                    bool isLeftToLine = testX < pointOnLine;

                    if (isLeftToLine)
                    {
                        //this statement changes true to false (and vice-versa)
                        locatedInPolygon = !locatedInPolygon;
                    }//end if (isLeftToLine)
                }//end if (withinYsEdges
            }

            return locatedInPolygon;
        }

        public void UpdateShape()
        {
            if (GetTemplate().IsPolygon())
                UpdatePolygonOrientation();
        }

        bool UnitFitToActionRequirement(Unit unit, Unit caster, AreaTriggerActionUserTypes targetType)
        {
            switch (targetType)
            {
                case AreaTriggerActionUserTypes.Friend:
                        return caster.IsFriendlyTo(unit);
                case AreaTriggerActionUserTypes.Enemy:
                        return !caster.IsFriendlyTo(unit);
                case AreaTriggerActionUserTypes.Raid:
                        return caster.IsInRaidWith(unit);
                case AreaTriggerActionUserTypes.Party:
                        return caster.IsInPartyWith(unit);
                case AreaTriggerActionUserTypes.Caster:
                        return unit.GetGUID() == caster.GetGUID();
                case AreaTriggerActionUserTypes.Any:
                default:
                    break;
            }

            return true;
        }

        void DoActions(Unit unit)
        {
            Unit caster = GetCaster();
            if (caster)
            {
                foreach (AreaTriggerAction action in GetTemplate().Actions)
        {
                    if (UnitFitToActionRequirement(unit, caster, action.TargetType))
                    {
                        switch (action.ActionType)
                        {
                            case AreaTriggerActionTypes.Cast:
                                caster.CastSpell(unit, action.Param, true);
                                break;
                            case AreaTriggerActionTypes.AddAura:
                                caster.AddAura(action.Param, unit);
                                break;
                            default:
                                break;
                        }
                    }
                }
            }
        }

        void UndoActions(Unit unit)
        {
            foreach (AreaTriggerAction action in GetTemplate().Actions)
            {
                if (action.ActionType == AreaTriggerActionTypes.Cast || action.ActionType == AreaTriggerActionTypes.AddAura)
                    unit.RemoveAurasDueToSpell(action.Param, GetCasterGuid());
            }
        }

        void InitSplineOffsets(List<Vector3> offsets, uint timeToTarget)
        {
            float angleSin = (float)Math.Sin(GetOrientation());
            float angleCos = (float)Math.Cos(GetOrientation());

            // This is needed to rotate the spline, following caster orientation
            List<Vector3> rotatedPoints = new List<Vector3>();
            for (var i = 0; i < offsets.Count; ++i)
            {
                Vector3 offset = offsets[i];
                float tempX = offset.X;
                float tempY = offset.Y;
                float tempZ = GetPositionZ();

                offset.X = (tempX * angleCos - tempY * angleSin) + GetPositionX();
                offset.Y = (tempX * angleSin + tempY * angleCos) + GetPositionY();
                UpdateAllowedPositionZ(offset.X, offset.Y, ref tempZ);
                offset.Z += tempZ;

                float x = GetPositionX() + (offset.X * angleCos - offset.Y * angleSin);
                float y = GetPositionY() + (offset.Y * angleCos + offset.X * angleSin);
                float z = GetPositionZ();

                UpdateAllowedPositionZ(x, y, ref z);
                z += offset.Z;

                rotatedPoints.Add(new Vector3(x, y, z));
            }

            InitSplines(rotatedPoints, timeToTarget);
        }

        void InitSplines(List<Vector3> splinePoints, uint timeToTarget)
        {
            if (splinePoints.Count < 2)
                return;

            _movementTime = 0;

            _spline.Init_Spline(splinePoints.ToArray(), splinePoints.Count, Spline.EvaluationMode.Linear);
            _spline.initLengths();

            // should be sent in object create packets only
            UpdateData[(int)AreaTriggerFields.TimeToTarget] = timeToTarget;

            if (IsInWorld)
            {
                if (_reachedDestination)
                {
                    AreaTriggerReShape reshapeDest = new AreaTriggerReShape();
                    reshapeDest.TriggerGUID = GetGUID();
                    SendMessageToSet(reshapeDest, true);
                }

                AreaTriggerReShape reshape = new AreaTriggerReShape();
                reshape.TriggerGUID = GetGUID();
                reshape.AreaTriggerSpline.HasValue = true;
                reshape.AreaTriggerSpline.Value.ElapsedTimeForMovement = GetElapsedTimeForMovement();
                reshape.AreaTriggerSpline.Value.TimeToTarget = timeToTarget;
                reshape.AreaTriggerSpline.Value.Points = splinePoints;
                SendMessageToSet(reshape, true);
            }

            _reachedDestination = false;
        }

        void UpdateSplinePosition(uint diff)
        {
            if (_reachedDestination)
                return;

            if (!HasSplines())
                return;

            _movementTime += diff;

            if (_movementTime >= GetTimeToTarget())
            {
                _reachedDestination = true;
                _lastSplineIndex = _spline.last();

                Vector3 lastSplinePosition = _spline.getPoint(_lastSplineIndex);
                GetMap().AreaTriggerRelocation(this, lastSplinePosition.X, lastSplinePosition.Y, lastSplinePosition.Z, GetOrientation());

                DebugVisualizePosition();

                _ai.OnSplineIndexReached(_lastSplineIndex);
                _ai.OnDestinationReached();
                return;
            }

            float currentTimePercent = _movementTime / GetTimeToTarget();

            if (currentTimePercent <= 0.0f)
                return;

            if (GetMiscTemplate().MoveCurveId != 0)
            {
                float progress = Global.DB2Mgr.GetCurveValueAt(GetMiscTemplate().MoveCurveId, currentTimePercent);
                if (progress < 0.0f || progress > 1.0f)
                {
                    Log.outError(LogFilter.AreaTrigger, "AreaTrigger (Id: {0}, SpellMiscId: {1}) has wrong progress ({2}) caused by curve calculation (MoveCurveId: {3})",
                        GetTemplate().Id, GetMiscTemplate().MiscId, progress, GetMiscTemplate().MorphCurveId);
                }
                else
                    currentTimePercent = progress;
            }

            int lastPositionIndex = 0;
            float percentFromLastPoint = 0;
            _spline.computeIndex(currentTimePercent, ref lastPositionIndex, ref percentFromLastPoint);

            Vector3 currentPosition;
            _spline.Evaluate_Percent(lastPositionIndex, percentFromLastPoint, out currentPosition);

            float orientation = GetOrientation();
            if (GetTemplate().HasFlag(AreaTriggerFlags.HasFaceMovementDir))
            {
                Vector3 nextPoint = _spline.getPoint(lastPositionIndex + 1);
                orientation = GetAngle(nextPoint.X, nextPoint.Y);
            }

            GetMap().AreaTriggerRelocation(this, currentPosition.X, currentPosition.Y, currentPosition.Z, orientation);

            DebugVisualizePosition();

            if (_lastSplineIndex != lastPositionIndex)
            {
                _lastSplineIndex = lastPositionIndex;
                _ai.OnSplineIndexReached(_lastSplineIndex);
            }
        }

        void AI_Initialize()
        {
            AI_Destroy();
            AreaTriggerAI ai = Global.ScriptMgr.GetAreaTriggerAI(this);
            if (ai == null)
                ai = new NullAreaTriggerAI(this);

            _ai = ai;
            _ai.OnInitialize();
        }

        void AI_Destroy()
        {
            _ai = null;
        }

        AreaTriggerAI GetAI() { return _ai; }

        [System.Diagnostics.Conditional("DEBUG")]
        void DebugVisualizePosition()
        {
            Unit caster = GetCaster();
            if (caster)
            {
                Player player = caster.ToPlayer();
                if (player)
                    if (player.isDebugAreaTriggers)
                        player.SummonCreature(1, this, TempSummonType.TimedDespawn, GetTimeToTarget());
            }
        }

        public bool IsRemoved() { return _isRemoved; }
        public uint GetSpellId() { return GetUInt32Value(AreaTriggerFields.SpellId); }
        public AuraEffect GetAuraEffect() { return _aurEff; }
        public uint GetTimeSinceCreated() { return _timeSinceCreated; }
        public uint GetTimeToTarget() { return GetUInt32Value(AreaTriggerFields.TimeToTarget); }
        public uint GetTimeToTargetScale() { return GetUInt32Value(AreaTriggerFields.TimeToTargetScale); }
        public int GetDuration() { return _duration; }
        public int GetTotalDuration() { return _totalDuration; }

        public void Delay(int delaytime) { SetDuration(GetDuration() - delaytime); }

        public List<ObjectGuid> GetInsideUnits() { return _insideUnits; }

        public AreaTriggerMiscTemplate GetMiscTemplate() { return _areaTriggerMiscTemplate; }

        public ObjectGuid GetCasterGuid() { return GetGuidValue(AreaTriggerFields.Caster); }

        public Vector3 GetRollPitchYaw() { return _rollPitchYaw; }
        public Vector3 GetTargetRollPitchYaw() { return _targetRollPitchYaw; }

        public bool HasSplines() { return !_spline.empty(); }
        public Spline GetSpline() { return _spline; }
        public uint GetElapsedTimeForMovement() { return GetTimeSinceCreated(); } /// @todo: research the right value, in sniffs both timers are nearly identical

        ObjectGuid _targetGuid;

        AuraEffect _aurEff;

        int _duration;
        int _totalDuration;
        uint _timeSinceCreated;
        float _previousCheckOrientation;
        bool _isRemoved;

        Vector3 _rollPitchYaw;
        Vector3 _targetRollPitchYaw;
        List<Vector2> _polygonVertices;
        Spline _spline;

        bool _reachedDestination;
        int _lastSplineIndex;
        uint _movementTime;

        AreaTriggerMiscTemplate _areaTriggerMiscTemplate;
        List<ObjectGuid> _insideUnits = new List<ObjectGuid>();

        AreaTriggerAI _ai;
    }
}
