using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;

/// <summary>
/// Class containing information about deforming mesh
/// and controlling deformations - start/stop, intensity, area of effect etc...
/// </summary>
[RequireComponent(typeof(UndoStack))]
public class DeformationManager : MonoBehaviour {

    [SerializeField]
    private Color skinColor;
    [SerializeField]
    private Color deformationToolColor;
    [SerializeField]
    private Color deformationToolColorDeforming;

    private UndoStack undoStack;
    public UndoStack UndoStack { get { return undoStack; } }

    [SerializeField]
    private GameObject objectToDeform;
    public GameObject ObjectToDeform { get { return objectToDeform; } }

    [SerializeField]
    private GameObject nonDeformingObject;

    /// <summary>
    /// Original position of deforming object
    /// </summary>
    private Vector3 originalPosition;

    /// <summary>
    /// Texture of currently deforming object
    /// </summary>
    public Texture2D Texture {

        get
        {
            return (Texture2D)skinMaterial.GetTexture("_MainTex");
        }
    }

    private float _importScale = 1;
    public float ImportScale
    {
        get
        {
            return _importScale;
        }
        set
        {
            _importScale = value;
        }
    }

    public string OriginalName { get; private set; } = "Unnamed";



    private Material skinMaterial;

    [SerializeField]
    private Transform deformationPointTransform;

    /// <summary>
    /// Local position of deforming tool from last frame
    /// </summary>
    public Vector3 LocalLastPosition { get; private set; }

    /// <summary>
    /// Local position of deforming tool from current frame
    /// </summary>
    public Vector3 LocalCurrentPosition { get; private set; }

    /// <summary>
    /// Global position of deforming tool from last frame
    /// </summary>
    public Vector3 GlobalLastPosition { get; private set; }

    /// <summary>
    /// Global position of deforming tool from current frame
    /// </summary>
    public Vector3 GlobalCurrentPosition { get; private set; }

    public Dictionary<int, HashSet<int>> NeighbourVertices { get; private set; } = new Dictionary<int, HashSet<int>>();


    /// <summary>
    /// Vertices of currently deforming model
    /// </summary>
    public Vector3[] CurrentVertices { get; set; }

    /// <summary>
    /// Vertex colors of currently deforming model - used to mask vertices
    /// </summary>
    public Color[] CurrentVertexColors { get; set; }



    /// <summary>
    /// array of masked vertices :  1 - cannot be deformed, 0 - can be deformed
    /// </summary>
    public int[] MaskedVertices { get; set; }

    /// <summary>
    /// Mesh component of currently deforming model
    /// </summary>
    public Mesh DeformingMesh { get; private set; }

    /// <summary>
    /// An array of initialized deformations
    /// </summary>
    public BaseDeformation[] Deformations { get; private set; }


    public Action<BaseDeformation> OnDeformationChange;

    public BaseDeformation _currentDeformation;

    public BaseDeformation CurrentDeformation
    {
        get
        {
            return _currentDeformation;
        }
        protected set
        {
            _currentDeformation = value;
            if (OnDeformationChange != null)
            {
                OnDeformationChange.Invoke(value);
            }
        }
    }

    public float CurrentIntensity
    {
        get
        {
            return CurrentDeformation.Intensity;
        }
        set
        {
            CurrentDeformation.Intensity = value;
            InvokeOnIntensityChange(CurrentDeformation.Intensity);
        }
    }

    public Action<float> OnIntensityChange;

    private void InvokeOnIntensityChange(float newIntensity)
    {
        if (OnIntensityChange != null)
            OnIntensityChange.Invoke(newIntensity);
    }

    private float _areaOfEffect;
    public float AreaOfEffect
    {
        get
        {
            return _areaOfEffect;
        }

        set
        {

            if (value < MinAreaOfEffect)
            {
                _areaOfEffect = MinAreaOfEffect;
            } else if (value > MaxAreaOfEffect)
            {
                _areaOfEffect = MaxAreaOfEffect;
            }else
            {
                _areaOfEffect = value;
            }

            

            deformationPointTransform.localScale = new Vector3(_areaOfEffect, _areaOfEffect, _areaOfEffect) * 2;

            InvokeOnAreaOfEffectChange(_areaOfEffect);
        }
    }

    public Action<float> OnAreaOfEffectChange;
    private void InvokeOnAreaOfEffectChange(float newAreaOfEffect)
    {
        if (OnAreaOfEffectChange != null)
            OnAreaOfEffectChange.Invoke(newAreaOfEffect);
    }

    /// <summary>
    /// Area of effect scaled with deforming object's scale
    /// </summary>
    public float ScaledAreaOfEffect
    {
        get
        {
            return AreaOfEffect / ObjectToDeform.transform.localScale.x;
        }

    }

    public float MinAreaOfEffect { get; private set; } = 0.005f;
    public float MaxAreaOfEffect { get; private set; } = 0.35f;

    
    private Coroutine _coroutineRecalculatingNormals;



    private void Awake()
    {
        undoStack = GetComponent<UndoStack>();
        Deformations = GetComponentsInChildren<BaseDeformation>();

    }

    private void Start()
    {
        originalPosition = ObjectToDeform.transform.position;
        ChangeDeformationToolColor(deformationToolColor);
        SetupObject();
        CurrentDeformation = Deformations[0];
        AreaOfEffect = 0;
    }

 
    private void Update()
    {

        LocalLastPosition = LocalCurrentPosition;
        LocalCurrentPosition = ObjectToDeform.transform.InverseTransformPoint(deformationPointTransform.position);

        GlobalLastPosition = GlobalCurrentPosition;
        GlobalCurrentPosition = deformationPointTransform.position;


    }


    /// <summary>
    /// Set a new non deforming - reference mesh
    /// </summary>
    /// <param name="mesh">Mesh filter containing mesh of reference model</param>
    public void SetNonDeformingMesh(MeshFilter mesh)
    {
        MeshFilter nonDeformingObject = this.nonDeformingObject.GetComponent<MeshFilter>();
        if (nonDeformingObject != null)
        {
            nonDeformingObject.mesh = mesh.mesh;
        }
        this.nonDeformingObject.SetActive(true);
    }

    /// <summary>
    /// Setup new mesh for deforming
    /// </summary>
    /// <param name="name">Name of the model - needed for export</param>
    /// <param name="mesh">Mesh filter containing the mesh for deformation</param>
    /// <param name="scale">Scale in which the model was imported</param>
    /// <param name="texture">Texture of the model</param>
    public void SetNewMesh(String name, MeshFilter mesh, float scale = 1, Texture texture = null)
    {
        OriginalName = name;
        ImportScale = scale;
        ResetTransform();
        ObjectToDeform.GetComponent<MeshFilter>().mesh = mesh.mesh;

        skinMaterial.mainTexture = texture;

        skinMaterial.color = texture == null ? skinColor : Color.white;

        SetupObject();
    }


    /// <summary>
    /// Prepare object for deformation (compute neighbour vertices ...), initialize deformation objects (compute shaders...)
    /// </summary>
    private void SetupObject()
    {
        undoStack.InitializeUndoStack(this);

        skinMaterial = ObjectToDeform.GetComponent<MeshRenderer>().material;
        
        DeformingMesh = ObjectToDeform.GetComponent<MeshFilter>().mesh;

        SetUpNeighbourVertices();


        // copy mesh vertices into new array 
        CurrentVertices = new Vector3[DeformingMesh.vertices.Length];
        Array.Copy(DeformingMesh.vertices, CurrentVertices, CurrentVertices.Length);

        ClearMasking();



        foreach (var deformation in Deformations)
        {
            deformation.InitializeDeformation(this);
        }

    }

    /// <summary>
    /// Calculate neighbour vertices
    /// </summary>
    private void SetUpNeighbourVertices()
    {
        NeighbourVertices.Clear();
        int[] triangles = DeformingMesh.triangles;


        for (int i = 0; i < triangles.Length; i += 3)
        {
            if (!NeighbourVertices.ContainsKey(triangles[i]))
            {
                NeighbourVertices[triangles[i]] = new HashSet<int>();
            }
            NeighbourVertices[triangles[i]].Add(triangles[i + 1]);
            NeighbourVertices[triangles[i]].Add(triangles[i + 2]);

            if (!NeighbourVertices.ContainsKey(triangles[i + 1]))
            {
                NeighbourVertices[triangles[i + 1]] = new HashSet<int>();
            }
            NeighbourVertices[triangles[i + 1]].Add(triangles[i]);
            NeighbourVertices[triangles[i + 1]].Add(triangles[i + 2]);

            if (!NeighbourVertices.ContainsKey(triangles[i + 2]))
            {
                NeighbourVertices[triangles[i + 2]] = new HashSet<int>();
            }
            NeighbourVertices[triangles[i + 2]].Add(triangles[i]);
            NeighbourVertices[triangles[i + 2]].Add(triangles[i + 1]);

        }
    }

    /// <summary>
    /// Change to next deformation in array
    /// </summary>
    public void RotateDeformation()
    {
        int currentIndex = Array.IndexOf(Deformations, CurrentDeformation);
        int newIndex = (currentIndex + 1) % Deformations.Length;
        CurrentDeformation = Deformations[newIndex];
    }

    /// <summary>
    /// Initiate the deformation
    /// </summary>
    public void StartDeforming()
    {
        ChangeDeformationToolColor(deformationToolColorDeforming);

        CurrentDeformation.AddUndoStep(undoStack);
        CurrentDeformation.StartDeforming();
        Debug.Log("started deformation of " + CurrentDeformation.DeformationName);
        _coroutineRecalculatingNormals = StartCoroutine(PeriodicallyRecalculate(0.5f));
    }

    /// <summary>
    /// End the deformation
    /// </summary>
    public void StopDeforming()
    {
        ChangeDeformationToolColor(deformationToolColor);
        if (_coroutineRecalculatingNormals != null)
            StopCoroutine(_coroutineRecalculatingNormals);
        CurrentDeformation.StopDeforming();
    }

    /// <summary>
    /// periodically recalculate normals and bounds of the deforming object
    /// </summary>
    /// <param name="secondsBetweenRecalculating">Seconds between recalculating normals and bounds</param>
    /// <returns></returns>
    public IEnumerator PeriodicallyRecalculate(float secondsBetweenRecalculating)
    {
        while (true)
        {
            yield return new WaitForSecondsRealtime(secondsBetweenRecalculating);
            DeformingMesh.RecalculateNormals();
            DeformingMesh.RecalculateBounds();
        }
        
    }

    /// <summary>
    /// Set deformation by type of deformation
    /// </summary>
    /// <param name="deformationName"></param>
    public void SetDeformationByName(Type deformationName)
    {

        for (int i = 0; i < Deformations.Length; i++)
        {
            if (Deformations[i].GetType() == deformationName)
            {
                CurrentDeformation = Deformations[i];
                return;
            }
        }
        Debug.Log("Invalid deformation name : " + deformationName);
    }

    /// <summary>
    /// Change transparency of currently deforming object
    /// </summary>
    /// <param name="newTransparency"></param>
    public void ChangeSkinTransparency(float newTransparency)
    {
        Debug.Log("changing transparency");
        if (skinMaterial.HasProperty("_Transparency")){
            Debug.Log("succesfully");
            skinMaterial.SetFloat("_Transparency", Mathf.Clamp(newTransparency, 0, 1));
        }
            
    }

    /// <summary>
    /// Hide non deforming (reference) object
    /// </summary>
    public void HideSkull()
    {
        nonDeformingObject.SetActive(false);
    }

    /// <summary>
    /// Reset position, rotation and scale of currently deforming object
    /// </summary>
    public void ResetTransform()
    {
        ObjectToDeform.transform.localScale = Vector3.one;
        ObjectToDeform.transform.localRotation = Quaternion.Euler(Vector3.zero);
        ObjectToDeform.transform.position = originalPosition;
    }

    /// <summary>
    /// Reset masing of currently deforming object
    /// </summary>
    public void ClearMasking()
    {
        // create an array of masking ints - 0 - vertex can be deformed, 1 - vertex cannot be deformed
        MaskedVertices = new int[DeformingMesh.vertices.Length];
        Array.Clear(MaskedVertices, 0, MaskedVertices.Length);

        // create an array of vertex colors used to mask vertices to deformation
        CurrentVertexColors = new Color[DeformingMesh.vertices.Length];
        for (int i = 0; i < CurrentVertexColors.Length; i++)
        {
            CurrentVertexColors[i] = Color.white;
        }
        DeformingMesh.colors = CurrentVertexColors;
    }


    private void ChangeDeformationToolColor(Color color)
    {
        var renderer = deformationPointTransform.GetComponent<Renderer>();
        renderer.material.color = color;
    }
}