using UnityEngine;
using System.Collections;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Dreamteck.Splines
{
    [AddComponentMenu("Dreamteck/Splines/Users/Object Controller")]
    public class ObjectController : SplineUser
    {
        [System.Serializable]
        internal class ObjectControl
        {
            public bool isNull
            {
                get
                {
                    return gameObject == null;
                }
            }
            public Transform transform
            {
                get {
                    if (gameObject == null) return null;
                    return gameObject.transform;  
                }
            }
            public GameObject gameObject;
            public Vector3 position = Vector3.zero;
            public Quaternion rotation = Quaternion.identity;
            public Vector3 scale = Vector3.one;
            public bool active = true;

            public Vector3 baseScale = Vector3.one;

            public ObjectControl(GameObject input)
            {
                gameObject = input;
                baseScale = gameObject.transform.localScale;
            }

            public void Destroy()
            {
                if (gameObject == null) return;
                GameObject.Destroy(gameObject);
            }

            public void DestroyImmediate()
            {
                if (gameObject == null) return;
                GameObject.DestroyImmediate(gameObject);
            }

            public void Apply()
            {
                if (gameObject == null) return;
                transform.position = position;
                transform.rotation = rotation;
                transform.localScale = scale;
                gameObject.SetActive(active);
            }

        }

        public enum ObjectMethod { Instantiate, GetChildren }
        public enum Positioning { Stretch, Clip }
        public enum Iteration { Ordered, Random }

        [SerializeField]
        [HideInInspector]
        public GameObject[] objects = new GameObject[0];

        public ObjectMethod objectMethod
        {
            get { return _objectMethod; }
            set
            {
                if (value != _objectMethod)
                {
                    if (value == ObjectMethod.GetChildren)
                    {
                        _objectMethod = value;
                        Spawn();
                    }
                    else _objectMethod = value;
                }
            }
        }

        public int spawnCount
        {
            get { return _spawnCount; }
            set
            {
                if (value != _spawnCount)
                {
                    if (value < 0) value = 0;
                    if (_objectMethod == ObjectMethod.Instantiate)
                    {
                        if (value < _spawnCount)
                        {
                            _spawnCount = value;
                            Remove();
                        }
                        else
                        {
                            _spawnCount = value;
                            Spawn();
                        }
                    }
                    else _spawnCount = value;
                }
            }
        }

        public Positioning objectPositioning
        {
            get { return _objectPositioning; }
            set
            {
                if (value != _objectPositioning)
                {
                    _objectPositioning = value;
                    Rebuild();
                }
            }
        }

        public Iteration iteration
        {
            get { return _iteration; }
            set
            {
                if (value != _iteration)
                {
                    _iteration = value;
                    Rebuild();
                }
            }
        }

#if UNITY_EDITOR
        public bool retainPrefabInstancesInEditor
        {
            get { return _retainPrefabInstancesInEditor; }
            set
            {
                if (value != _retainPrefabInstancesInEditor)
                {
                    _retainPrefabInstancesInEditor = value;
                    Clear();
                    Spawn();
                    Rebuild();
                }
            }
        }
#endif

        public int randomSeed
        {
            get { return _randomSeed; }
            set
            {
                if (value != _randomSeed)
                {
                    _randomSeed = value;
                    Rebuild();
                }
            }
        }

        public Vector3 minOffset
        {
            get { return _minOffset; }
            set
            {
                if (value != _minOffset)
                {
                    _minOffset = value;
                    Rebuild();
                }
            }
        }

        public Vector3 maxOffset
        {
            get { return _maxOffset; }
            set
            {
                if (value != _maxOffset)
                {
                    _maxOffset = value;
                    Rebuild();
                }
            }
        }

        public bool offsetUseWorldCoords
        {
            get { return _offsetUseWorldCoords; }
            set
            {
                if (value != _offsetUseWorldCoords)
                {
                    _offsetUseWorldCoords = value;
                    Rebuild();
                }
            }
        }

        public Vector3 minRotation
        {
            get { return _minRotation; }
            set
            {
                if (value != _minRotation)
                {
                    _minRotation = value;
                    Rebuild();
                }
            }
        }

        public Vector3 maxRotation
        {
            get { return _maxRotation; }
            set
            {
                if (value != _maxRotation)
                {
                    _maxRotation = value;
                    Rebuild();
                }
            }
        }

        public Vector3 rotationOffset
        {
            get { return (_maxRotation+_minRotation)/2f; }
            set
            {
                if (value != _minRotation || value != _maxRotation)
                {
                    _minRotation = _maxRotation = value;
                    Rebuild();
                }
            }
        }

        public Vector3 minScaleMultiplier
        {
            get { return _minScaleMultiplier; }
            set
            {
                if (value != _minScaleMultiplier)
                {
                    _minScaleMultiplier = value;
                    Rebuild();
                }
            }
        }

        public Vector3 maxScaleMultiplier
        {
            get { return _maxScaleMultiplier; }
            set
            {
                if (value != _maxScaleMultiplier)
                {
                    _maxScaleMultiplier = value;
                    Rebuild();
                }
            }
        }

        public bool uniformScaleLerp
        {
            get { return _uniformScaleLerp; }
            set
            {
                if(value != _uniformScaleLerp)
                {
                    _uniformScaleLerp = value;
                    Rebuild();
                }
            }
        }

        public bool shellOffset
        {
            get { return _shellOffset; }
            set
            {
                if (value != _shellOffset)
                {
                    _shellOffset = value;
                    Rebuild();
                }
            }
        }

        public bool applyRotation
        {
            get { return _applyRotation; }
            set
            {
                if (value != _applyRotation)
                {
                    _applyRotation = value;
                    Rebuild();
                }
            }
        }

        public bool rotateByOffset
        {
            get { return _rotateByOffset; }
            set
            {
                if (value != _rotateByOffset)
                {
                    _rotateByOffset = value;
                    Rebuild();
                }
            }
        }

        public bool applyScale
        {
            get { return _applyScale; }
            set
            {
                if (value != _applyScale)
                {
                    _applyScale = value;
                    Rebuild();
                }
            }
        }

        public float evaluateOffset
        {
            get { return _evaluateOffset; }
            set
            {
                if (value != _evaluateOffset)
                {
                    _evaluateOffset = value;
                    Rebuild();
                }
            }
        }

        public float minObjectDistance
        {
            get { return _minObjectDistance; }
            set
            {
                if (value != _minObjectDistance)
                {
                    _minObjectDistance = value;
                    Rebuild();
                }
            }
        }

        public float maxObjectDistance
        {
            get { return _maxObjectDistance; }
            set
            {
                if (value != _maxObjectDistance)
                {
                    _maxObjectDistance = value;
                    Rebuild();
                }
            }
        }

        public ObjectControllerCustomRuleBase customOffsetRule
        {
            get { return _customOffsetRule; }
            set
            {
                if (value != _customOffsetRule)
                {
                    _customOffsetRule = value;
                    Rebuild();
                }
            }
        }

        public ObjectControllerCustomRuleBase customRotationRule
        {
            get { return _customRotationRule; }
            set
            {
                if (value != _customRotationRule)
                {
                    _customRotationRule = value;
                    Rebuild();
                }
            }
        }

        public ObjectControllerCustomRuleBase customScaleRule
        {
            get { return _customScaleRule; }
            set
            {
                if (value != _customScaleRule)
                {
                    _customScaleRule = value;
                    Rebuild();
                }
            }
        }

        [SerializeField]
        [HideInInspector]
        private float _evaluateOffset = 0f;
        [SerializeField]
        [HideInInspector]
        private int _spawnCount = 0;
#if UNITY_EDITOR
        [SerializeField]
        [HideInInspector]
        private bool _retainPrefabInstancesInEditor = true;
#endif
        [SerializeField]
        [HideInInspector]
        private Positioning _objectPositioning = Positioning.Stretch;
        [SerializeField]
        [HideInInspector]
        private Iteration _iteration = Iteration.Ordered;
        [SerializeField]
        [HideInInspector]
        private int _randomSeed = 1;
        [SerializeField]
        [HideInInspector]
        private Vector3 _minOffset = Vector3.zero;
        [SerializeField]
        [HideInInspector]
        private Vector3 _maxOffset = Vector3.zero;
        [SerializeField]
        [HideInInspector]
        private bool _offsetUseWorldCoords = false;
        [SerializeField]
        [HideInInspector]
        private Vector3 _minRotation = Vector3.zero;
        [SerializeField]
        [HideInInspector]
        private Vector3 _maxRotation = Vector3.zero;
        [SerializeField]
        [HideInInspector]
        private bool _uniformScaleLerp = true;
        [SerializeField]
        [HideInInspector]
        private Vector3 _minScaleMultiplier = Vector3.one;
        [SerializeField]
        [HideInInspector]
        private Vector3 _maxScaleMultiplier = Vector3.one;
        [SerializeField]
        [HideInInspector]
        private bool _shellOffset = false;
        [SerializeField]
        [HideInInspector]
        private bool _applyRotation = true;
        [SerializeField]
        [HideInInspector]
        private bool _rotateByOffset = false;
        [SerializeField]
        [HideInInspector]
        private bool _applyScale = false;
        [SerializeField]
        [HideInInspector]
        private ObjectMethod _objectMethod = ObjectMethod.Instantiate;
        [HideInInspector]
        public bool delayedSpawn = false;
        [HideInInspector]
        public float spawnDelay = 0.1f;
        [SerializeField]
        [HideInInspector]
        private int lastChildCount = 0;
        [SerializeField]
        [HideInInspector]
        private ObjectControl[] spawned = new ObjectControl[0];
        [SerializeField]
        [HideInInspector]
        private bool _useCustomObjectDistance = false;
        [SerializeField]
        [HideInInspector]
        private float _minObjectDistance = 0f;
        [SerializeField]
        [HideInInspector]
        private float _maxObjectDistance = 0f;

        [SerializeField]
        [HideInInspector]
        private ObjectControllerCustomRuleBase _customOffsetRule;

        [SerializeField]
        [HideInInspector]
        private ObjectControllerCustomRuleBase _customRotationRule;

        [SerializeField]
        [HideInInspector]
        private ObjectControllerCustomRuleBase _customScaleRule;

        System.Random offsetRandomizer, shellRandomizer, rotationRandomizer, scaleRandomizer, distanceRandomizer;

        public void Clear()
        {
            for (int i = 0; i < spawned.Length; i++)
            {
                if (spawned[i] == null || spawned[i].transform == null) continue;
                spawned[i].transform.localScale = spawned[i].baseScale;
                if (_objectMethod == ObjectMethod.GetChildren) spawned[i].gameObject.SetActive(false);
                else
                {
#if UNITY_EDITOR
                    if (!Application.isPlaying) spawned[i].DestroyImmediate();
                    else spawned[i].Destroy();
#else
                    spawned[i].Destroy();
#endif

                }
            }
            spawned = new ObjectControl[0];
        }

        private void OnValidate()
        {
            if (_spawnCount < 0) _spawnCount = 0;
        }

        private void Remove()
        {
            if (_spawnCount >= spawned.Length) return;
            for (int i = spawned.Length - 1; i >= _spawnCount; i--)
            {
                if (i >= spawned.Length) break;
                if (spawned[i] == null) continue;
                spawned[i].transform.localScale = spawned[i].baseScale;
                if (_objectMethod == ObjectMethod.GetChildren) spawned[i].gameObject.SetActive(false);
                else
                {
                    if (Application.isEditor) spawned[i].DestroyImmediate();
                    else spawned[i].Destroy();

                }
            }
            ObjectControl[] newSpawned = new ObjectControl[_spawnCount];
            for (int i = 0; i < newSpawned.Length; i++)
            {
                newSpawned[i] = spawned[i];
            }
            spawned = newSpawned;
            Rebuild();
        }

        public void GetAll()
        {
            ObjectControl[] newSpawned = new ObjectControl[transform.childCount];
            int index = 0;
            foreach (Transform child in transform)
            {
                if (newSpawned[index] == null)
                {
                    newSpawned[index++] = new ObjectControl(child.gameObject);
                    continue;
                }
                bool found = false;
                for (int i = 0; i < spawned.Length; i++)
                {
                    if (spawned[i].gameObject == child.gameObject)
                    {
                        newSpawned[index++] = spawned[i];
                        found = true;
                        break;
                    }
                }
                if (!found) newSpawned[index++] = new ObjectControl(child.gameObject);
            }
            spawned = newSpawned;
        }

        public void Spawn()
        {
            if (_objectMethod == ObjectMethod.Instantiate)
            {
                if (delayedSpawn && Application.isPlaying)
                {
                    StopCoroutine("InstantiateAllWithDelay");
                    StartCoroutine(InstantiateAllWithDelay());
                }
                else InstantiateAll();
            }
            else GetAll();
            Rebuild();
        }

        protected override void LateRun()
        {
            base.LateRun();
            if (_objectMethod == ObjectMethod.GetChildren && lastChildCount != transform.childCount)
            {
                Spawn();
                lastChildCount = transform.childCount;
            }
        }


        IEnumerator InstantiateAllWithDelay()
        {
            if (spline == null) yield break;
            if (objects.Length == 0) yield break;
            for (int i = spawned.Length; i <= spawnCount; i++)
            {
                InstantiateSingle();
                yield return new WaitForSeconds(spawnDelay);
            }
        }

        private void InstantiateAll()
        {
            if (spline == null) return;
            if (objects.Length == 0) return;
            for (int i = spawned.Length; i < spawnCount; i++) InstantiateSingle();
        }

        private void InstantiateSingle()
        {
            if (objects.Length == 0) return;
            int index = 0;
            if (_iteration == Iteration.Ordered)
            {
                index = spawned.Length - Mathf.FloorToInt(spawned.Length / objects.Length) * objects.Length;
            }
            else index = Random.Range(0, objects.Length);
            if (objects[index] == null) return;

            ObjectControl[] newSpawned = new ObjectControl[spawned.Length + 1];
            spawned.CopyTo(newSpawned, 0);
#if UNITY_EDITOR
            if (!Application.isPlaying && retainPrefabInstancesInEditor)
            {
                GameObject go = (GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(objects[index]);
                go.transform.position = transform.position;
                go.transform.rotation = transform.rotation;
                newSpawned[newSpawned.Length - 1] = new ObjectControl(go);
            } else
            {
                newSpawned[newSpawned.Length - 1] = new ObjectControl((GameObject)Instantiate(objects[index], transform.position, transform.rotation));
            }
#else
            newSpawned[newSpawned.Length - 1] = new ObjectControl((GameObject)Instantiate(objects[index], transform.position, transform.rotation));
#endif
            newSpawned[newSpawned.Length - 1].transform.parent = transform;
            spawned = newSpawned;
        }

        protected override void Build()
        {
            base.Build();
            offsetRandomizer = new System.Random(_randomSeed);
            if(_shellOffset) shellRandomizer = new System.Random(_randomSeed + 1);
            rotationRandomizer = new System.Random(_randomSeed + 2);
            scaleRandomizer = new System.Random(_randomSeed + 3);
            distanceRandomizer = new System.Random(_randomSeed + 4);

            bool hasCustomOffset = _customOffsetRule != null;
            bool hasCustomRotation = _customRotationRule != null;
            bool hasCustomScale = _customScaleRule != null;

            bool randomScaleMultiplier = _minScaleMultiplier != _maxScaleMultiplier;
            double distancePercentAccum = 0.0;
            for (int i = 0; i < spawned.Length; i++)
            {
                if (spawned[i] == null)
                {
                    Clear();
                    Spawn();
                    break;
                }
                float percent = 0f;
                if (spawned.Length > 1)
                {
                    if(!_useCustomObjectDistance)
                    {
                        if (spline.isClosed)
                        {
                            percent = (float)i / spawned.Length;
                        }
                        else
                        {
                            percent = (float)i / (spawned.Length - 1);
                        }
                    } else
                    {
                        percent = (float)distancePercentAccum;
                    }
                }

                percent += _evaluateOffset;
                if (percent > 1f)
                {
                    percent -= 1f;
                }
                else if (percent < 0f)
                {
                    percent += 1f;
                }
                
                if (objectPositioning == Positioning.Clip)
                {
                    spline.Evaluate(percent, ref evalResult);
                }
                else
                {
                    Evaluate(percent, ref evalResult);
                }

                ModifySample(ref evalResult);
                spawned[i].position = evalResult.position;

                if (_applyScale)
                {
                    if (hasCustomScale)
                    {
                        _customScaleRule.SetContext(this, evalResult, i, spawned.Length);
                        spawned[i].scale = _customOffsetRule.GetScale();
                    } 
                    else
                    {
                        Vector3 scale = spawned[i].baseScale * evalResult.size;
                        Vector3 multiplier = _minScaleMultiplier;

                        if (randomScaleMultiplier)
                        {

                            if (_uniformScaleLerp)
                            {
                                multiplier = Vector3.Lerp(new Vector3(_minScaleMultiplier.x, _minScaleMultiplier.y, _minScaleMultiplier.z), new Vector3(_maxScaleMultiplier.x, _maxScaleMultiplier.y, _maxScaleMultiplier.z), (float)scaleRandomizer.NextDouble());
                            }
                            else
                            {
                                multiplier.x = Mathf.Lerp(_minScaleMultiplier.x, _maxScaleMultiplier.x, (float)scaleRandomizer.NextDouble());
                                multiplier.y = Mathf.Lerp(_minScaleMultiplier.y, _maxScaleMultiplier.y, (float)scaleRandomizer.NextDouble());
                                multiplier.z = Mathf.Lerp(_minScaleMultiplier.z, _maxScaleMultiplier.z, (float)scaleRandomizer.NextDouble());
                            }
                        }
                        scale.x *= multiplier.x;
                        scale.y *= multiplier.y;
                        scale.z *= multiplier.z;
                        spawned[i].scale = scale;
                    }
                }
                else
                {
                    spawned[i].scale = spawned[i].baseScale;
                }

                Vector3 right = Vector3.Cross(evalResult.forward, evalResult.up).normalized;

                Vector3 posOffset = _minOffset;
                if (hasCustomOffset)
                {
                    _customOffsetRule.SetContext(this, evalResult, i, spawned.Length);
                    posOffset = _customOffsetRule.GetOffset();
                } 
                else if (_minOffset != _maxOffset)
                {
                    if(_shellOffset)
                    {
                        float x = _maxOffset.x - _minOffset.x;
                        float y = _maxOffset.y - _minOffset.y;
                        float angleInRadians = (float)shellRandomizer.NextDouble() * 360f * Mathf.Deg2Rad;
                        posOffset = new Vector2(0.5f * Mathf.Cos(angleInRadians), 0.5f * Mathf.Sin(angleInRadians));
                        posOffset.x *= x;
                        posOffset.y *= y;
                    } else
                    {
                        float rnd = (float)offsetRandomizer.NextDouble();
                        posOffset.x = Mathf.Lerp(_minOffset.x, _maxOffset.x, rnd);
                        rnd = (float)offsetRandomizer.NextDouble();
                        posOffset.y = Mathf.Lerp(_minOffset.y, _maxOffset.y, rnd);
                        rnd = (float)offsetRandomizer.NextDouble();
                        posOffset.z = Mathf.Lerp(_minOffset.z, _maxOffset.z, rnd);
                    }
                }

                if (_offsetUseWorldCoords)
                {
                    spawned[i].position += posOffset;
                }
                else
                {
                    spawned[i].position += right * posOffset.x * evalResult.size + evalResult.up * posOffset.y * evalResult.size;
                }

                if (_applyRotation)
                {
                    if (hasCustomRotation)
                    {
                        _customRotationRule.SetContext(this, evalResult, i, spawned.Length);
                        spawned[i].rotation = _customRotationRule.GetRotation();
                    }
                    else
                    {
                        Quaternion offsetRot = Quaternion.Euler(Mathf.Lerp(_minRotation.x, _maxRotation.x, (float)rotationRandomizer.NextDouble()), Mathf.Lerp(_minRotation.y, _maxRotation.y, (float)rotationRandomizer.NextDouble()), Mathf.Lerp(_minRotation.z, _maxRotation.z, (float)rotationRandomizer.NextDouble()));
                        if (_rotateByOffset) spawned[i].rotation = Quaternion.LookRotation(evalResult.forward, spawned[i].position - evalResult.position) * offsetRot;
                        else spawned[i].rotation = evalResult.rotation * offsetRot;
                    }
                }

                if (_objectPositioning == Positioning.Clip)
                {
                    if (percent < clipFrom || percent > clipTo) spawned[i].active = false;
                    else spawned[i].active = true;
                }
                if (_useCustomObjectDistance)
                {
                    if (objectPositioning == Positioning.Clip)
                    {
                        distancePercentAccum = spline.Travel(distancePercentAccum, Mathf.Lerp(_minObjectDistance, _maxObjectDistance, (float)distanceRandomizer.NextDouble()));
                    }
                    else
                    {
                        distancePercentAccum = Travel(distancePercentAccum, Mathf.Lerp(_minObjectDistance, _maxObjectDistance, (float)distanceRandomizer.NextDouble()));
                    }
                }
            }
        }

        protected override void PostBuild()
        {
            base.PostBuild();
            for (int i = 0; i < spawned.Length; i++)
            {
                spawned[i].Apply();
            }
        }
    }
}