// Copyright (c) 2011-2017 Thinksquirrel Inc.

using UnityEngine;
using System.Collections;
using UnityEngine.Rendering;

namespace Thinksquirrel.Thinkscroller
{
    /// <summary>
    /// Scroll layer component.
    /// </summary>
    /// <remarks>
    /// Scroll layers represent each parallax layer, and can be both in the foreground and background.
    /// Scroll layers are sorted by their weight - this can be done automatically, or manually overriden.
    /// Layers can also be stretched across the screen or pixel-perfect.
    /// Note that scroll layers are independent of Unity's layer system.
    /// </remarks>
    [AddComponentMenu("Thinkscroller/Scroll Layer")]
    [ExecuteInEditMode]
    public sealed class ScrollLayer : ThinkscrollerMonoBehaviourBase, System.IComparable<ScrollLayer>
    {
        [SerializeField] [HideInInspector] private MeshFilter meshFilter;
        [SerializeField] [HideInInspector] private MeshRenderer meshRenderer;
        [SerializeField] [HideInInspector] private bool autoBillboard = true;
        [SerializeField] [HideInInspector] private bool objectLayer;
        [SerializeField] [HideInInspector] private bool _objectLayerPixelSpace = true;
        [SerializeField] [HideInInspector] private bool auto = true;
        [SerializeField] [HideInInspector] private float weight;
        [SerializeField] [HideInInspector] private bool stretch;
        [SerializeField] [HideInInspector] private bool pixelPerfect = true;
        [SerializeField] [HideInInspector] private bool _tileX = true;
        [SerializeField] [HideInInspector] private bool _tileY = true;
        [SerializeField] [HideInInspector] private Vector2 _offset = Vector2.zero;
        [SerializeField] [HideInInspector] private Vector2 _pixelOffset = Vector2.zero;
        [SerializeField] [HideInInspector] private ScrollLayerAlignment mAlignment = ScrollLayerAlignment.Center;
        [SerializeField] [HideInInspector] private Vector2 _padding = Vector2.zero;
        [SerializeField] [HideInInspector] private float xSize = 10;
        [SerializeField] [HideInInspector] private float ySize = 10;
        [SerializeField] [HideInInspector] private float scrollSpeed = 1;
        [SerializeField] [HideInInspector] private Vector2 scrollMod = Vector2.one;
        [SerializeField] [HideInInspector] private Texture2D texture;
        [SerializeField] [HideInInspector] private string[] textureNames;
        [SerializeField] [HideInInspector] private bool _refreshVertsOnAwake = true;
        [SerializeField] [HideInInspector] private bool _refreshMaterialOnAwake = true;
        [SerializeField] [HideInInspector] private bool _refreshTextureOnAwake = true;
        [SerializeField] [HideInInspector] private bool _calculateNormals;
        [SerializeField] [HideInInspector] private bool _calculateTangents;
        [SerializeField] [HideInInspector] private Vector3 oldPosition = Vector3.zero;
        [HideInInspector] public bool textureNamesFoldout_EDITOR = true;
        [HideInInspector] public int foldoutSize_EDITOR = 1;
        [HideInInspector] public bool advancedFoldout_EDITOR;

        /// <summary>
        /// If true, refresh materials on Awake().
        /// </summary>
        /// <remarks>
        /// Only applicable for auto billboards.
        /// </remarks>
        public bool refreshMaterialOnAwake
        {
            get { return _refreshMaterialOnAwake; }
            set { _refreshMaterialOnAwake = value; }
        }

        /// <summary>
        /// If true, refresh the texture on Awake().
        /// </summary>
        /// <remarks>
        /// Only applicable for auto billboards.
        /// </remarks>
        public bool refreshTextureOnAwake
        {
            get { return _refreshTextureOnAwake; }
            set { _refreshTextureOnAwake = value; }
        }

        /// <summary>
        /// If true, refresh vertices on Awake().
        /// </summary>
        /// <remarks>
        /// Only applicable for auto billboards.
        /// </remarks>
        public bool refreshVertsOnAwake
        {
            get { return _refreshVertsOnAwake; }
            set { _refreshVertsOnAwake = value; }
        }

        /// <summary>
        /// If true, calculate normals on the billboard mesh.
        /// </summary>
        /// <remarks>
        /// Only applicable for auto billboards.
        /// </remarks>
        public bool calculateNormals
        {
            get { return _calculateNormals; }
            set
            {
                if (value == false)
                {
                    _calculateTangents = false;
                }
                _calculateNormals = value;
            }
        }

        /// <summary>
        /// If true, calculate tangents on the billboard mesh.
        /// </summary>
        /// <remarks>
        /// Only applicable for auto billboards.
        /// </remarks>
        public bool calculateTangents
        {
            get { return _calculateTangents; }
            set
            {
                if (value == true)
                {
                    _calculateNormals = true;
                }
                _calculateTangents = value;
            }
        }
        /// <summary>
        /// Sets the weight of the scroll layer. This will force a speed change if the speed was previously overwritten, even if the weight is the same as the current weight.
        /// <remarks>
        /// Lighter weights scroll faster than heavier weights.
        /// </remarks>
        /// </summary>
        public void SetWeight(float weight, bool force = false)
        {
            if (force || Mathf.Abs(this.weight - weight) > Vector3.kEpsilon)
            {
                this.weight = weight;
                scrollSpeed = 1f / (weight + .00001f);
            }
        }

        /// <summary>
        /// Gets the weight of the scroll layer.
        /// </summary>
        /// <remarks>
        /// Lighter weights scroll faster than heavier weights.
        /// </remarks>
        public float GetWeight()
        {
            return weight;
        }

        /// <summary>
        /// Gets the scrolling speed of the scroll layer.
        /// </summary>
        public float GetScrollSpeed()
        {
            return scrollSpeed;
        }

        /// <summary>
        /// Sets the scrolling speed of the scroll layer directly, overriding the weight.
        /// </summary>
        public void SetScrollSpeed(float scrollSpeed)
        {
            this.scrollSpeed = scrollSpeed;
        }

        /// <summary>
        /// Gets the scrolling speed modifier of the scroll layer.
        /// </summary>
        public Vector2 GetScrollMod()
        {
            return scrollMod;
        }

        /// <summary>
        /// Sets the scrolling speed modifier of the scroll layer.
        /// </summary>
        public void SetScrollMod(Vector2 scrollMod)
        {
            this.scrollMod = scrollMod;
        }

        /// <summary>
        /// Determines whether the scroll layer is automatically configured.
        /// </summary>
        public bool isAutoConfigured
        {
            get { return auto; }
            set
            {
                if (value && !auto)
                {
                    auto = true;
                    if (Parallax.instance)
                    {
                        Parallax.instance.RefreshLayers();
                    }
                }
                else
                {
                    auto = value;
                }
            }
        }

        /// <summary>
        /// Determines whether the scroll layer has an automatic billboard.
        /// </summary>
        public bool isAutoBillboard
        {
            get { return autoBillboard; }
            set
            {
                if (value && !autoBillboard)
                {
                    objectLayer = false;
                    autoBillboard = true;
                    RefreshBillboard();
                }
                else if (!value && autoBillboard)
                {
                    if (meshRenderer)
                    {
                        DestroyImmediate(meshRenderer);
                    }
                    if (meshFilter)
                    {
                        DestroyImmediate(meshFilter);
                    }
                    autoBillboard = false;
                }
            }
        }

        /// <summary>
        /// Determines whether the scroll layer is an object layer.
        /// </summary>
        /// <remarks>
        /// Object layers scroll the transform and ignore any textures on the object. It can also scroll groups of objects. This offers compatibility with sprite/2D management systems.
        /// </remarks>
        public bool isObjectLayer
        {
            get { return objectLayer; }
            set
            {
                if (value && !objectLayer)
                {
                    isAutoBillboard = false;
                }
                objectLayer = value;
            }
        }

        /// <summary>
        /// If true, object layers scroll in pixel space (similar to auto-billboards), as opposed to world space.
        /// </summary>
        /// <remarks>
        /// Note that object layers do not always align perfectly with auto-billboards over long distances, due to floating point calculation.
        /// </remarks>
        public bool objectLayerPixelSpace
        {
            get { return _objectLayerPixelSpace; }
            set { _objectLayerPixelSpace = value; }
        }

        /// <summary>
        /// Determines whether the auto billboard is pixel-perfect.
        /// </summary>
        public bool isPixelPerfect
        {
            get { return pixelPerfect; }
            set
            {
                if (value && !pixelPerfect)
                {
                    pixelPerfect = true;
                    SetBillboardStretch(false);
                    RefreshBillboard(true, false, false);
                }
                else if (!value && pixelPerfect)
                {
                    pixelPerfect = false;
                    RefreshBillboard(true, false, false);
                }
            }
        }

        /// <summary>
        /// Determines whether to tile on the X axis.
        /// </summary>
        public bool tileX
        {
            get { return _tileX; }
            set
            {
                if (value != _tileX)
                {
                    _tileX = value;
                    RefreshBillboard(true, false, false);
                }
            }
        }

        /// <summary>
        /// Determines whether to tile on the Y axis
        /// </summary>
        public bool tileY
        {
            get { return _tileY; }
            set
            {
                if (value != _tileY)
                {
                    _tileY = value;
                    RefreshBillboard(true, false, false);
                }
            }
        }

        /// <summary>
        /// The offset (in pixels) of the parallax billboard.
        /// </summary>
        public Vector2 offset
        {
            get { return _offset; }
            set
            {
                if (value != _offset)
                {
                    _offset = value;
                    RefreshBillboard(true, false, false);
                }
            }
        }

        /// <summary>
        /// The offset (in pixels) of the texture size. Used for fixing border issues.
        /// </summary>
        public Vector2 pixelOffset
        {
            get { return _pixelOffset; }
            set
            {
                if (value != _pixelOffset)
                {
                    _pixelOffset = value;
                    RefreshBillboard(true, false, false);
                }
            }
        }

        /// <summary>
        /// Gets or sets the scroll layer's alignment.
        /// </summary>
        public ScrollLayerAlignment alignment
        {
            get { return mAlignment; }
            set
            {
                if (value != mAlignment)
                {
                    mAlignment = value;
                    RefreshBillboard(true, false, false);
                }
            }
        }

        /// <summary>
        /// UV padding. Used for fixing border issues.
        /// </summary>
        public Vector2 padding
        {
            get { return _padding; }
            set
            {
                if (value != _padding)
                {
                    _padding = value;
                    RefreshBillboard(true, false, false);
                }
            }
        }

        /// <summary>
        /// Gets the auto billboard scale, in world units.
        /// </summary>
        public Vector2 GetBillboardScale()
        {
            return new Vector2(xSize, ySize);
        }

        /// <summary>
        /// Sets the auto billboard scale, in world units.
        /// </summary>
        public void SetBillboardScale(Vector2 scale)
        {
            SetBillboardScale(scale.x, scale.y);
        }

        /// <summary>
        /// Sets the auto billboard scale, in world units.
        /// </summary>
        public void SetBillboardScale(float xSize, float ySize)
        {
            var changed = false;

            if (Mathf.Abs(this.xSize - xSize) > Vector3.kEpsilon)
            {
                this.xSize = xSize;
                changed = true;
            }
            if (Mathf.Abs(this.ySize - ySize) > Vector3.kEpsilon)
            {
                this.ySize = ySize;
                changed = true;
            }
            if (changed) RefreshBillboard(true, false, false);
        }

        /// <summary>
        /// If true, the billboard will stretch to fit the camera viewport.
        /// </summary>
        /// <remarks>
        /// Only works while in play mode.
        /// </remarks>
        public bool GetBillboardStretch()
        {
            return stretch;
        }

        /// <summary>
        /// Sets whether the billboard will stretch.
        /// </summary>
        public void SetBillboardStretch(bool stretch)
        {
            this.stretch = stretch;
        }

        /// <summary>
        /// Gets the mesh renderer associated with the scroll layer.
        /// </summary>
        public MeshRenderer GetRenderer()
        {
            return meshRenderer;
        }

        /// <summary>
        /// Sets the mesh renderer associated with the scroll layer.
        /// </summary>
        public void SetRenderer(MeshRenderer renderer)
        {
            meshRenderer = renderer;
        }

        /// <summary>
        /// Gets the material associated with the scroll layer.
        /// </summary>
        public Material GetMaterial()
        {
            if (autoBillboard)
            {
                if (!meshRenderer)
                {
                    meshRenderer = gameObject.AddComponent<MeshRenderer>();
                    ResetMaterial_INTERNAL();
                }
                else if (meshRenderer.sharedMaterial == null)
                {
                    ResetMaterial_INTERNAL();
                }

                return meshRenderer.sharedMaterial;
            }
            else if (meshRenderer)
            {
                if (meshRenderer.sharedMaterial != null)
                {
                    return meshRenderer.sharedMaterial;
                }
            }
            return null;
        }

        /// <summary>
        /// Sets the material associated with the scroll layer.
        /// </summary>
        public void SetMaterial(Material material)
        {
            if (meshRenderer)
            {
                meshRenderer.sharedMaterial = material;
            }
        }

        public Texture2D GetTexture()
        {
            return texture;
        }

        public void SetTexture(Texture2D texture)
        {
            if (this.texture != texture)
            {
                this.texture = texture;
            }
        }

        /// <summary>
        /// Gets the texture names associated with a scroll layer.
        /// </summary>
        /// <remarks>
        /// Texture names are used to determine which textures on a material to scroll.
        /// </remarks>
        public string[] GetTextureNames()
        {
            return textureNames;
        }

        /// <summary>
        /// Gets the texture name associated with a scroll layer at the specified index.
        /// </summary>
        /// <remarks>
        /// Texture names are used to determine which textures on a material to scroll.
        /// </remarks>
        public string GetTextureName(int index)
        {
            if (textureNames.Length > index)
            {
                return textureNames[index];
            }
            return "";
        }

        /// <summary>
        /// Sets the texture names associated with a scroll layer.
        /// </summary>
        /// <remarks>
        /// Texture names are used to determine which textures on a material to scroll.
        /// </remarks>
        public void SetTextureNames(params string[] textureNames)
        {
            this.textureNames = textureNames;
        }

        /// <summary>
        /// Sets the texture name associated with a scroll layer at the specified index.
        /// </summary>
        /// <remarks>
        /// Texture names are used to determine which textures on a material to scroll.
        /// </remarks>
        public void SetTextureName(int index, string textureName)
        {
            if (textureNames.Length > index)
            {
                textureNames[index] = textureName;
            }
        }

        /// <summary>
        /// Compares a scroll layer to another by weight.
        /// </summary>
        public int CompareTo(ScrollLayer other)
        {
            // If other is not a valid object reference, this instance is greater.
            if (other == null) return 1;

            // Compare the underlying weights.
            return GetWeight().CompareTo(other.GetWeight());
        }

        internal static int Comparison(ScrollLayer layer, ScrollLayer other)
        {
            // If other is not a valid object reference, this instance is greater.
            if (other == null) return 1;

            // Compare the underlying weights.
            var weight = layer.GetWeight();
            var otherWeight = other.GetWeight();
            return weight < otherWeight ? -1 : (weight > otherWeight ? 1 : 0);
        }
        /* Behaviours */

        private void RefreshVertices_INTERNAL()
        {
            if (!Parallax.instance) return;

            if (!Parallax.instance.GetParallaxCamera()) return;

            var parallaxCam = Parallax.instance.GetParallaxCamera();
            var distance = Vector3.Distance(parallaxCam.transform.position, transform.position);
            var topLeft = Vector3.zero;
            var topRight = Vector3.zero;
            var bottomLeft = Vector3.zero;
            var bottomRight = Vector3.zero;
            var ppxy0 = Vector3.zero;
            var ppxy1 = Vector3.zero;

            var m = parallaxCam.cameraToWorldMatrix;
            var mm = parallaxCam.projectionMatrix.inverse;
            var x = parallaxCam.orthographicSize * Parallax.instance.GetCameraAspectRatio();
            float width = 0;
            float height = 0;

            var pixel_w = Parallax.GetPixelWidth(parallaxCam, transform.position);
            var pixel_h = Parallax.GetPixelHeight(parallaxCam, transform.position);
            var offset = this.offset;
            offset.Scale(new Vector2(pixel_w, pixel_h));

            switch (alignment)
            {
                case ScrollLayerAlignment.TopLeft:
                    offset -= new Vector2(-Parallax.instance.GetCameraWidth() * .5f * pixel_w,
                                          Parallax.instance.GetCameraHeight() * .5f * pixel_h);
                    break;

                case ScrollLayerAlignment.Top:
                    offset -= new Vector2(0, Parallax.instance.GetCameraHeight() * .5f * pixel_h);
                    break;

                case ScrollLayerAlignment.TopRight:
                    offset -= new Vector2(Parallax.instance.GetCameraWidth() * .5f * pixel_w,
                                          Parallax.instance.GetCameraHeight() * .5f * pixel_h);
                    break;

                case ScrollLayerAlignment.Left:
                    offset -= new Vector2(-Parallax.instance.GetCameraWidth() * .5f * pixel_w, 0);
                    break;

                case ScrollLayerAlignment.Center:
                    offset -= Vector2.zero;
                    break;

                case ScrollLayerAlignment.Right:
                    offset -= new Vector2(Parallax.instance.GetCameraWidth() * .5f * pixel_w, 0);
                    break;

                case ScrollLayerAlignment.BottomLeft:
                    offset -= new Vector2(-Parallax.instance.GetCameraWidth() * .5f * pixel_w,
                                          -Parallax.instance.GetCameraHeight() * .5f * pixel_h);
                    break;

                case ScrollLayerAlignment.Bottom:
                    offset -= new Vector2(0, -Parallax.instance.GetCameraHeight() * .5f * pixel_h);
                    break;

                case ScrollLayerAlignment.BottomRight:
                    offset -= new Vector2(Parallax.instance.GetCameraWidth() * .5f * pixel_w,
                                          -Parallax.instance.GetCameraHeightRaw() * .5f * pixel_h);
                    break;
            }

            if (texture)
            {
                width = texture.width + pixelOffset.x;
                height = texture.height + pixelOffset.y;

                if (!stretch && pixelPerfect && tileX && !tileY)
                {
                    ppxy0 = parallaxCam.transform.position - mm.MultiplyPoint3x4(new Vector3(0, 0, -distance));
                    ppxy1 = parallaxCam.transform.position -
                            mm.MultiplyPoint3x4(new Vector3(width / Parallax.instance.GetCameraWidth(),
                                                            height / Parallax.instance.GetCameraHeight(),
                                                            -distance));

                    topLeft = parallaxCam.transform.position -
                              m.MultiplyPoint3x4(
                                  new Vector3(-x + offset.x, -(ppxy1.y - ppxy0.y) + offset.y, -distance));
                    topRight = parallaxCam.transform.position -
                               m.MultiplyPoint3x4(
                                   new Vector3(x + offset.x, -(ppxy1.y - ppxy0.y) + offset.y, -distance));
                    bottomLeft = parallaxCam.transform.position -
                                 m.MultiplyPoint3x4(
                                     new Vector3(-x + offset.x, (ppxy1.y - ppxy0.y) + offset.y, -distance));
                    bottomRight = parallaxCam.transform.position -
                                  m.MultiplyPoint3x4(
                                      new Vector3(x + offset.x, (ppxy1.y - ppxy0.y) + offset.y, -distance));
                }
                else if (!stretch && pixelPerfect && tileY && !tileX)
                {
                    ppxy0 = parallaxCam.transform.position - mm.MultiplyPoint3x4(new Vector3(0, 0, -distance));
                    ppxy1 = parallaxCam.transform.position -
                            mm.MultiplyPoint3x4(new Vector3(width / Parallax.instance.GetCameraWidth(),
                                                            height / Parallax.instance.GetCameraHeight(),
                                                            -distance));

                    var xS = -(ppxy1.y - ppxy0.y) * (width / height);

                    topLeft = parallaxCam.transform.position -
                              m.MultiplyPoint3x4(
                                  new Vector3(-xS + offset.x, parallaxCam.orthographicSize + offset.y, -distance));
                    topRight = parallaxCam.transform.position -
                               m.MultiplyPoint3x4(
                                   new Vector3(xS + offset.x, parallaxCam.orthographicSize + offset.y, -distance));
                    bottomLeft = parallaxCam.transform.position -
                                 m.MultiplyPoint3x4(
                                     new Vector3(-xS + offset.x, -parallaxCam.orthographicSize + offset.y, -distance));
                    bottomRight = parallaxCam.transform.position -
                                  m.MultiplyPoint3x4(
                                      new Vector3(xS + offset.x, -parallaxCam.orthographicSize + offset.y, -distance));
                }
                else if (!stretch && pixelPerfect && !tileY && !tileX)
                {
                    ppxy0 = parallaxCam.transform.position - mm.MultiplyPoint3x4(new Vector3(0, 0, -distance));
                    ppxy1 = parallaxCam.transform.position -
                            mm.MultiplyPoint3x4(new Vector3(width / Parallax.instance.GetCameraWidth(),
                                                            height / Parallax.instance.GetCameraHeight(),
                                                            -distance));

                    var xS = -(ppxy1.y - ppxy0.y) * (width / height);

                    topLeft = parallaxCam.transform.position -
                              m.MultiplyPoint3x4(
                                  new Vector3(-xS + offset.x, -(ppxy1.y - ppxy0.y) + offset.y, -distance));
                    topRight = parallaxCam.transform.position -
                               m.MultiplyPoint3x4(
                                   new Vector3(xS + offset.x, -(ppxy1.y - ppxy0.y) + offset.y, -distance));
                    bottomLeft = parallaxCam.transform.position -
                                 m.MultiplyPoint3x4(
                                     new Vector3(-xS + offset.x, (ppxy1.y - ppxy0.y) + offset.y, -distance));
                    bottomRight = parallaxCam.transform.position -
                                  m.MultiplyPoint3x4(
                                      new Vector3(xS + offset.x, (ppxy1.y - ppxy0.y) + offset.y, -distance));
                }
                else
                {
                    topLeft = parallaxCam.transform.position -
                              m.MultiplyPoint3x4(
                                  new Vector3(-x + offset.x, parallaxCam.orthographicSize + offset.y, -distance));
                    topRight = parallaxCam.transform.position -
                               m.MultiplyPoint3x4(
                                   new Vector3(x + offset.x, parallaxCam.orthographicSize + offset.y, -distance));
                    bottomLeft = parallaxCam.transform.position -
                                 m.MultiplyPoint3x4(
                                     new Vector3(-x + offset.x, -parallaxCam.orthographicSize + offset.y, -distance));
                    bottomRight = parallaxCam.transform.position -
                                  m.MultiplyPoint3x4(
                                      new Vector3(x + offset.x, -parallaxCam.orthographicSize + offset.y, -distance));
                }
            }

            var mesh = new Mesh
            {
                vertices = new[]
                {
                    new Vector3(topLeft.x, topLeft.y, 0),
                    new Vector3(topRight.x, topRight.y, 0),
                    new Vector3(bottomLeft.x, bottomLeft.y, 0),
                    new Vector3(bottomRight.x, bottomRight.y, 0)
                },
                triangles = new[] {0, 1, 2, 1, 3, 2}
            };

            if (calculateNormals)
            {
                mesh.RecalculateNormals();
            }

            if (!stretch)
            {
                var xx = -(x / xSize);
                var yy = -(parallaxCam.orthographicSize / ySize);

                if (pixelPerfect && texture)
                {
                    if (tileX && tileY)
                    {
                        ppxy0 = parallaxCam.transform.position - mm.MultiplyPoint3x4(new Vector3(0, 0, -distance));
                        ppxy1 = parallaxCam.transform.position -
                                mm.MultiplyPoint3x4(
                                    new Vector3(width / Parallax.instance.GetCameraWidth(),
                                                height / Parallax.instance.GetCameraHeight(),
                                                -distance));
                    }

                    ySize = -(ppxy1.y - ppxy0.y);

                    xSize = ySize * width / height;

                    if (!tileX && !tileY)
                    {
                        xx = -1 - padding.x;
                        yy = -1 - padding.y;
                    }
                    else
                    {
                        if (tileX)
                        {
                            xx = -(x / xSize) - padding.x;
                        }
                        else
                        {
                            xx = -1 - padding.x;
                        }

                        if (tileY)
                        {
                            yy = -(parallaxCam.orthographicSize / ySize) - padding.y;
                        }
                        else
                        {
                            yy = -1 - padding.y;
                        }
                    }
                }

                mesh.uv = new[]
                {
                    new Vector2(padding.x, yy),
                    new Vector2(xx, yy),
                    new Vector2(padding.x, padding.y),
                    new Vector2(xx, padding.y),
                };
            }
            else
            {
                mesh.uv = new[]
                {
                    new Vector2(padding.x, -padding.y),
                    new Vector2(-1 - padding.x, -padding.y),
                    new Vector2(padding.x, 1 - padding.y),
                    new Vector2(-1 - padding.x, 1 - padding.y)
                };
            }

            if (calculateNormals && calculateTangents)
            {
                RecalculateTangents(mesh);
            }

#if !UNITY_5_5_OR_NEWER
            mesh.Optimize();
#endif
            if (meshFilter.sharedMesh)
            {
                DestroyImmediate(meshFilter.sharedMesh);
            }
            meshFilter.sharedMesh = mesh;
        }

        private void ResetMaterial_INTERNAL()
        {
            var shader = Shader.Find("Thinkscroller/Parallax Default");
            if (meshRenderer.sharedMaterial)
            {
                meshRenderer.sharedMaterial.shader = shader;
            }
            else
            {
                meshRenderer.sharedMaterial = new Material(shader) {name = "Thinkscroller/Parallax Default"};
            }
            textureNames = new[] {"_MainTex"};
        }

        private void RefreshTexture_INTERNAL()
        {
            if (texture)
            {
                meshRenderer.sharedMaterial.mainTexture = texture;
            }
        }

        /// <summary>
        /// Refreshes the auto billboard mesh.
        /// </summary>
        public void RefreshBillboard()
        {
            RefreshBillboard(true, true, true);
        }

        /// <summary>
        /// Refreshes the auto billboard mesh.
        /// </summary>
        /// <param name='refreshVertices'>
        /// If true, refresh vertices.
        /// </param>
        /// <param name='resetMaterial'>
        /// If true, reset material.
        /// </param>
        /// <param name='refreshTexture'>
        /// If true, refresh texture.
        /// </param>
        public void RefreshBillboard(bool refreshVertices, bool resetMaterial, bool refreshTexture)
        {
            if (!enabled) return;

            if (!meshFilter && gameObject.GetComponent<MeshFilter>())
            {
                meshFilter = gameObject.GetComponent<MeshFilter>();
            }
            else if (!meshFilter)
            {
                meshFilter = gameObject.AddComponent<MeshFilter>();
            }

            meshFilter.hideFlags |= HideFlags.HideInInspector;

            if (gameObject.GetComponent<MeshRenderer>())
            {
                meshRenderer = gameObject.GetComponent<MeshRenderer>();
                meshRenderer.shadowCastingMode = ShadowCastingMode.Off;
                meshRenderer.receiveShadows = false;
            }
            else if (gameObject.GetComponent<MeshRenderer>() == null)
            {
                meshRenderer = gameObject.AddComponent<MeshRenderer>();
                meshRenderer.shadowCastingMode = ShadowCastingMode.Off;
                meshRenderer.receiveShadows = false;
            }

            meshRenderer.hideFlags |= HideFlags.HideInInspector;
            meshRenderer.enabled = true;

            if (refreshVertices) RefreshVertices_INTERNAL();
            if (resetMaterial) ResetMaterial_INTERNAL();
            if (refreshTexture) RefreshTexture_INTERNAL();
        }

        /// <summary>
        /// Reset the scroll layer.
        /// </summary>
        public void Reset()
        {
            if (!Parallax.instance)
            {
                return;
            }
            if (autoBillboard)
            {
                RefreshBillboard();
            }
            Parallax.instance.RefreshLayers();
        }

        /// <summary>
        /// Reset the scroll layer's position (Object layers only).
        /// </summary>
        public void ResetPosition()
        {
            if (isObjectLayer)
            {
                transform.position = oldPosition;
            }
        }

        /// <summary>
        /// Reset the scroll layer's position (Object layers only).
        /// </summary>
        public void ResetPosition(Vector3 resetPosition)
        {
            if (isObjectLayer)
            {
                transform.position = resetPosition;
                oldPosition = transform.position;
            }
        }

        private IEnumerator ResetDelay_INTERNAL()
        {
            yield return new WaitForEndOfFrame();

            if (Parallax.instance)
            {
                DoReset();
                Parallax.instance.RefreshLayers();
            }
        }

        internal void DoReset()
        {
            if (autoBillboard && (_refreshMaterialOnAwake || _refreshTextureOnAwake || _refreshVertsOnAwake))
            {
                RefreshBillboard(_refreshVertsOnAwake, _refreshMaterialOnAwake, _refreshTextureOnAwake);
            }
        }

        /* MonoBehaviours */

        private void Awake()
        {
            oldPosition = transform.position;
            StartCoroutine(ResetDelay_INTERNAL());
        }

        private void OnDisable()
        {
            if (meshRenderer) meshRenderer.enabled = false;

            if (Parallax.instance)
            {
                Parallax.instance.RefreshLayers();
            }
        }

        private void OnEnable()
        {
            if (meshRenderer) meshRenderer.enabled = true;

            if (Parallax.instance)
            {
                Parallax.instance.RefreshLayers();
            }
        }

        /// <summary>
        /// Recalculates the tangents of a mesh.
        /// </summary>
        private static void RecalculateTangents(Mesh mesh)
        {
            var triangleCount = mesh.triangles.Length / 3;
            var vertexCount = mesh.vertices.Length;

            var tan1 = new Vector3[vertexCount];
            var tan2 = new Vector3[vertexCount];

            var tangents = new Vector4[vertexCount];

            for (long a = 0; a < triangleCount; a += 3)
            {
                long i1 = mesh.triangles[a + 0];
                long i2 = mesh.triangles[a + 1];
                long i3 = mesh.triangles[a + 2];

                var v1 = mesh.vertices[i1];
                var v2 = mesh.vertices[i2];
                var v3 = mesh.vertices[i3];

                var w1 = mesh.uv[i1];
                var w2 = mesh.uv[i2];
                var w3 = mesh.uv[i3];

                var x1 = v2.x - v1.x;
                var x2 = v3.x - v1.x;
                var y1 = v2.y - v1.y;
                var y2 = v3.y - v1.y;
                var z1 = v2.z - v1.z;
                var z2 = v3.z - v1.z;

                var s1 = w2.x - w1.x;
                var s2 = w3.x - w1.x;
                var t1 = w2.y - w1.y;
                var t2 = w3.y - w1.y;

                var r = 1.0f / (s1 * t2 - s2 * t1);

                var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
                var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);

                tan1[i1] += sdir;
                tan1[i2] += sdir;
                tan1[i3] += sdir;

                tan2[i1] += tdir;
                tan2[i2] += tdir;
                tan2[i3] += tdir;
            }

            for (long a = 0; a < vertexCount; ++a)
            {
                var n = mesh.normals[a];
                var t = tan1[a];

                var tmp = (t - n * Vector3.Dot(n, t)).normalized;
                tangents[a] = new Vector4(tmp.x, tmp.y, tmp.z);

                tangents[a].w = (Vector3.Dot(Vector3.Cross(n, t), tan2[a]) < 0.0f) ? -1.0f : 1.0f;
            }

            mesh.tangents = tangents;
        }
    }
}
