using System;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;

namespace ru.chikalin.textdecal
{
[ExecuteAlways]
[RequireComponent(typeof(TMP_Text))]
[DefaultExecutionOrder(11)]
[AddComponentMenu("Mesh/TextMeshPro - Decal")]
[HelpURL("https://assetstore.unity.com/")] // todo
#if UNITY_2021_2_OR_NEWER
[Icon("Editor/Resources/Icons/sticker.png")] // todo
#endif
public class TextMeshProDecal : MonoBehaviour
{
    private const int VolumetricVertexCount = 8;
    private const int VolumetricTrianglesCount = 36;

    private readonly int[] _volumetricTriangles =
    {
        0, 1, 2, //face front
        2, 3, 0,
        1, 5, 6, //face top
        6, 2, 1,
        3, 2, 6, //face right
        6, 7, 3,
        4, 5, 1, //face left
        1, 0, 4,
        4, 0, 3, //face back
        3, 7, 4,
        7, 6, 5, //face bottom
        5, 4, 7
    };

    [Tooltip("Volumetric mesh depth")] public float depth = 1;
    private Vector3 _depthVector;

    private TMP_Text _text;
    private Vector3[] _vertices;
    private int[] _triangles;
    private Color[] _colors;
    private Vector2[] _uv;
    private Vector2[] _uv2;
    private Vector2[] _uv4;
    private Vector2[] _uv5;
    private Vector2[] _uv6;
    private Vector2[] _uv7;
    private Vector2[] _uv8;

    private Mesh _volumetricMesh;

    private Mesh VolumetricMesh
    {
        get
        {
            if (_volumetricMesh != null) return _volumetricMesh;
            _volumetricMesh = new Mesh
            {
                hideFlags = HideFlags.HideAndDontSave
            };
#if DEVELOPMENT_BUILD || UNITY_EDITOR
            _volumetricMesh.name = "TextMeshPro Decal Mesh";
#endif
            _volumetricMesh.MarkDynamic();
            return _volumetricMesh;
        }
    }

    private MeshFilter _meshFilter;

    private MeshFilter MeshFilter
    {
        get
        {
            if (_meshFilter != null) return _meshFilter;
            _meshFilter = GetComponent<MeshFilter>();
            if (_meshFilter != null) return _meshFilter;
            _meshFilter = gameObject.AddComponent<MeshFilter>();
            _meshFilter.hideFlags = HideFlags.HideInInspector | HideFlags.HideAndDontSave;
            return _meshFilter;
        }
    }

    private void Awake()
    {
        _text = GetComponent<TMP_Text>();
    }

    private void OnEnable()
    {
        UpdateDepthVector();
        _text.OnPreRenderText += OnPreRenderText;
        TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChangedEvent);
        OnPreRenderText(_text.textInfo);
    }

    private void OnDisable()
    {
        TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChangedEvent);
        _text.OnPreRenderText -= OnPreRenderText;

        _text.ForceMeshUpdate();
        MeshFilter.sharedMesh = _text.mesh;
        if (_volumetricMesh != null)
        {
            DestroyImmediate(_volumetricMesh);
        }
    }

    private void OnTextChangedEvent(Object obj)
    {
        if (obj != _text || !string.IsNullOrEmpty(_text.text)) return;

        // force update if text is empty
        OnPreRenderText(_text.textInfo);
    }

    private void OnPreRenderText(TMP_TextInfo textInfo)
    {
        // todo remove
        if (_text.isVolumetricText)
        {
            var textMesh = _text.mesh;
            MeshFilter.sharedMesh = textMesh;
        }
        else
        {
            PrepareMeshData(textInfo);
            var characterIndex = 0;
            for (var i = 0; i < textInfo.characterCount; i++)
            {
                var characterInfo = textInfo.characterInfo[i];
                if (!characterInfo.isVisible) continue;
                var materialIndex = characterInfo.materialReferenceIndex;
                var meshInfo = textInfo.meshInfo[materialIndex];
                AddCharacter(characterInfo, meshInfo, characterIndex++);
            }

            UpdateMesh();
        }
    }

    /// <summary>
    /// Add volumetric mesh for a character
    /// </summary>
    /// <param name="charInfo">TextMesh character info</param>
    /// <param name="meshInfo">TextMesh mesh info</param>
    /// <param name="characterIndex">Character index</param>
    private void AddCharacter(TMP_CharacterInfo charInfo, TMP_MeshInfo meshInfo, int characterIndex)
    {
        var volumetricVertexIndex = characterIndex * VolumetricVertexCount;
        var vertexIndex = charInfo.vertexIndex;

        /*
         * copy TextMesh vertices
         */
        var bl = meshInfo.vertices[vertexIndex + 0];
        var tl = meshInfo.vertices[vertexIndex + 1];
        var tr = meshInfo.vertices[vertexIndex + 2];
        var br = meshInfo.vertices[vertexIndex + 3];
        _vertices[volumetricVertexIndex + 0] = bl;
        _vertices[volumetricVertexIndex + 1] = tl;
        _vertices[volumetricVertexIndex + 2] = tr;
        _vertices[volumetricVertexIndex + 3] = br;
        /*
         * Extrude face to make a cube
         */
        _vertices[volumetricVertexIndex + 4] = bl + _depthVector;
        _vertices[volumetricVertexIndex + 5] = tl + _depthVector;
        _vertices[volumetricVertexIndex + 6] = tr + _depthVector;
        _vertices[volumetricVertexIndex + 7] = br + _depthVector;

        /*
         * Fill volumetric triangles
         */
        for (var i = 0; i < _volumetricTriangles.Length; i++)
        {
            _triangles[characterIndex * VolumetricTrianglesCount + i] = volumetricVertexIndex + _volumetricTriangles[i];
        }

        /*
         * Copy TextMesh UVs
         */
        _uv[volumetricVertexIndex + 0] = charInfo.vertex_BL.uv;
        _uv[volumetricVertexIndex + 1] = charInfo.vertex_TL.uv;
        _uv[volumetricVertexIndex + 2] = charInfo.vertex_TR.uv;
        _uv[volumetricVertexIndex + 3] = charInfo.vertex_BR.uv;
        _uv[volumetricVertexIndex + 4] = charInfo.vertex_BL.uv;
        _uv[volumetricVertexIndex + 5] = charInfo.vertex_TL.uv;
        _uv[volumetricVertexIndex + 6] = charInfo.vertex_TR.uv;
        _uv[volumetricVertexIndex + 7] = charInfo.vertex_BR.uv;

        _uv2[volumetricVertexIndex + 0] = charInfo.vertex_BL.uv2;
        _uv2[volumetricVertexIndex + 1] = charInfo.vertex_TL.uv2;
        _uv2[volumetricVertexIndex + 2] = charInfo.vertex_TR.uv2;
        _uv2[volumetricVertexIndex + 3] = charInfo.vertex_BR.uv2;
        _uv2[volumetricVertexIndex + 4] = charInfo.vertex_BL.uv2;
        _uv2[volumetricVertexIndex + 5] = charInfo.vertex_TL.uv2;
        _uv2[volumetricVertexIndex + 6] = charInfo.vertex_TR.uv2;
        _uv2[volumetricVertexIndex + 7] = charInfo.vertex_BR.uv2;

        /*
         * Additional mesh data for decal shader
         */
        // character bottom left vertex position
        var uv4 = new Vector2(bl.x, bl.y);
        // character bottom left UV
        var uv5 = charInfo.vertex_BL.uv;

        // character 3D size
        var width = Vector3.Distance(br, bl);
        var height = Vector3.Distance(tr, br);
        var uv6 = new Vector2(width, height);

        // character UV size
        var uvWidth = Vector2.Distance(charInfo.vertex_BR.uv, charInfo.vertex_BL.uv);
        var uvHeight = Vector2.Distance(charInfo.vertex_TR.uv, charInfo.vertex_BR.uv);
        var uv7 = new Vector2(uvWidth, uvHeight);

        // character 3D Z-rotation
        var angle = Vector3.SignedAngle(Vector3.up, tl - bl, Vector3.forward);
        var uv8 = Mathf.Deg2Rad * angle;

        for (var i = 0; i < VolumetricVertexCount; i++)
        {
            _uv4[volumetricVertexIndex + i] = uv4;
            _uv5[volumetricVertexIndex + i] = uv5;
            _uv6[volumetricVertexIndex + i] = uv6;
            _uv7[volumetricVertexIndex + i] = uv7;
            _colors[volumetricVertexIndex + i] = charInfo.color;
            _uv8[volumetricVertexIndex + i].x = uv8;
        }
    }

    private void UpdateMesh()
    {
        var mesh = VolumetricMesh;
        mesh.Clear();
        mesh.vertices = _vertices;
        mesh.triangles = _triangles;
        mesh.uv = _uv;
        mesh.uv2 = _uv2;
        mesh.colors = _colors;
        mesh.uv4 = _uv4;
        mesh.uv5 = _uv5;
        mesh.uv6 = _uv6;
        mesh.uv7 = _uv7;
        mesh.uv8 = _uv8;
        if (MeshFilter.sharedMesh != mesh)
        {
            MeshFilter.sharedMesh = mesh;
        }

        mesh.RecalculateBounds();
        mesh.MarkModified();
    }

    private void PrepareMeshData(TMP_TextInfo textInfo)
    {
        var size = textInfo.characterInfo.Length;
        var vertexSize = size * VolumetricVertexCount;
        UpdateArray(ref _vertices, vertexSize);
        UpdateArray(ref _triangles, size * VolumetricTrianglesCount);
        UpdateArray(ref _colors, vertexSize);
        UpdateArray(ref _uv, vertexSize);
        UpdateArray(ref _uv2, vertexSize);
        UpdateArray(ref _uv4, vertexSize);
        UpdateArray(ref _uv5, vertexSize);
        UpdateArray(ref _uv6, vertexSize);
        UpdateArray(ref _uv7, vertexSize);
        UpdateArray(ref _uv8, vertexSize);
    }

    private static void UpdateArray<T>(ref T[] array, int size)
    {
        if (array == null)
        {
            array = new T[size];
            return;
        }

        if (array.Length < size)
        {
            Array.Resize(ref array, size);
        }

        Array.Clear(array, 0, array.Length);
    }


#if UNITY_EDITOR
    private void OnValidate()
    {
        if (_text != null)
        {
            _text.ForceMeshUpdate();
        }

        UpdateDepthVector();
    }
#endif

    private void UpdateDepthVector() => _depthVector = new Vector3(0, 0, depth);
}
}