﻿using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace Thry
{
    public class DecalSceneTool
    {
        private MaterialProperty _propPosition;
        private MaterialProperty _propRotation;
        private MaterialProperty _propScale;
        private MaterialProperty _propOffset;
        private Material _material;
        private Renderer _renderer;
        private int _uvIndex;
        private Mesh _mesh;
        private Vector2[][] _uvTriangles;
        private Vector3[][] _worldTriangles;
        private bool _isActive;
        private Mode _mode = Mode.None;
        private HandleMode _handleMode = HandleMode.Position;
        private Tool _previousTool;

        public enum Mode
        {
            None, Raycast, Handles
        }

        public enum HandleMode
        {
            Position, Rotation, Scale, Offset
        }

        private DecalSceneTool() {}

        public static DecalSceneTool Create(Renderer renderer, Material m, int uvIndex, MaterialProperty propPosition, MaterialProperty propRotation, MaterialProperty propScale, MaterialProperty propOffset)
        {
            var tool = new DecalSceneTool();
            tool._material = m;
            tool._uvIndex = uvIndex;
            tool._propPosition = propPosition;
            tool._propRotation = propRotation;
            tool._propScale = propScale;
            tool._propOffset = propOffset;
            tool._renderer = renderer;
            tool.Init();
            return tool;
        }

        public void SetMaterialProperties(MaterialProperty propPosition, MaterialProperty propRotation, MaterialProperty propScale, MaterialProperty propOffset)
        {
            _propPosition = propPosition;
            _propRotation = propRotation;
            _propScale = propScale;
            _propOffset = propOffset;
        }

        public void StartRaycastMode()
        {
            _mode = Mode.Raycast;
            this.Activate();
        }

        public void StartHandleMode()
        {
            _mode = Mode.Handles;
            this.Activate();
        }

        public void Activate()
        {
            if(_isActive) return;
            _previousTool = Tools.current;
            Tools.current = Tool.None;
            SceneView.duringSceneGui += OnSceneGUI;
            Selection.selectionChanged += OnSelectionChange;
            _isActive = true;
        }

        public void Deactivate() 
        {
            if(!_isActive) return;
            SceneView.duringSceneGui -= OnSceneGUI;
            Selection.selectionChanged -= OnSelectionChange;
            Tools.current = _previousTool;
            _isActive = false;
            _mode = Mode.None;
            
        }

        public Mode GetMode()
        {
            return _mode;
        }

        void OnSelectionChange() 
        {
            this.Deactivate();
        }

        void Init()
        {
            // EditorUtility.DisplayProgressBar("Decal Tool", "Loading Mesh...", 0.0f);
            GetMesh();
            _uvTriangles = new Vector2[_mesh.triangles.Length / 3][];
            _worldTriangles = new Vector3[_mesh.triangles.Length / 3][];
            int[] triangles = _mesh.triangles;
            Vector2[] uvs;
            if(_uvIndex == 1) uvs = _mesh.uv2;
            else if(_uvIndex == 2) uvs = _mesh.uv3;
            else if(_uvIndex == 3) uvs = _mesh.uv4;
            else uvs = _mesh.uv;
            Vector3[] vertices = _mesh.vertices;
            Transform root = _renderer.transform;
            Vector3 inverseScale = new Vector3(1.0f / root.lossyScale.x, 1.0f / root.lossyScale.y, 1.0f / root.lossyScale.z);
            bool isSMR = _renderer is SkinnedMeshRenderer;
            for(int i = 0; i < triangles.Length; i += 3)
            {
                // if(i%100 == 0) EditorUtility.DisplayProgressBar("Decal Tool", "Loading Mesh...", (float)i / triangles.Length);
                _uvTriangles[i / 3] = new Vector2[3];
                _worldTriangles[i / 3] = new Vector3[3];
                for(int j = 0; j < 3; j++)
                {
                    _uvTriangles[i / 3][j] = uvs[triangles[i + j]];
                    if(isSMR)
                        _worldTriangles[i / 3][j] = root.TransformPoint(Vector3.Scale(vertices[triangles[i + j]], inverseScale));
                    else
                        _worldTriangles[i / 3][j] = root.TransformPoint(vertices[triangles[i + j]]);
                    // _worldTriangles[i / 3][j] = vertices[triangles[i + j]];
                }
            }
            // EditorUtility.ClearProgressBar();
        }

        private void OnSceneGUI(SceneView sceneView) 
        {
            switch(_mode)
            {
                case Mode.Raycast:
                    RaycastMode(sceneView);
                    break;
                case Mode.Handles:
                    HandlesMode(sceneView);
                    break;
            }
        }

        void RaycastMode(SceneView sceneView)
        {
            if(Tools.current != Tool.View)
            {
                Tools.current = Tool.View;
            }

            Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);

            Vector2 uv = _propPosition.vectorValue;
            if(RaycastToClosestUV(ray, ref uv))
            {
                _propPosition.vectorValue = uv;
            }

            if(Event.current.type == EventType.MouseDown && Event.current.button == 0)
            {
                Event.current.Use();
                Deactivate();
            }
        }

        void HandlesMode(SceneView sceneView)
        {
            switch(_handleMode)
            {
                case HandleMode.Position:
                    PositionMode(sceneView);
                    break;
                case HandleMode.Rotation:
                    RotationMode(sceneView);
                    break;
                case HandleMode.Scale:
                    ScaleMode(sceneView);
                    break;
                case HandleMode.Offset:
                    OffsetMode(sceneView);
                    break;
            }

            if(Tools.current != Tool.None)
            {
                switch(Tools.current)
                {
                    case Tool.Move:
                        _handleMode = HandleMode.Position;
                        break;
                    case Tool.Rotate:
                        _handleMode = HandleMode.Rotation;
                        break;
                    case Tool.Scale:
                        _handleMode = HandleMode.Scale;
                        break;
                    case Tool.Rect:
                        _handleMode = HandleMode.Offset;
                        break;
                }
                Tools.current = Tool.None;
            }
        }

        void PositionMode(SceneView sceneView)
        {
            GetPivot();
            Vector3 gizmoNormal = _pivotNormal;
            if(Vector3.Dot(sceneView.camera.transform.forward, _pivotNormal) < 0)
            {
                gizmoNormal = -_pivotNormal;
            }
            Quaternion rotation = Quaternion.LookRotation(gizmoNormal, _pivotUp);

            if(Tools.pivotRotation == PivotRotation.Local)
            {
                rotation *= Quaternion.Euler(0, 0, -_propRotation.floatValue);
            }
            
            Vector3 moved = Handles.PositionHandle(_pivotPoint, rotation);
            if(moved != _pivotPoint)
            {
                Vector2 uv = Vector2.zero;
                Ray ray = new Ray(moved - _pivotNormal * 0.1f, _pivotNormal);
                if(RaycastToClosestUV(ray, ref uv))
                {
                    _propPosition.vectorValue = uv;
                }
            }
        }

        void RotationMode(SceneView sceneView)
        {
            GetPivot();
            Vector3 gizmoNormal = _pivotNormal;
            if(Vector3.Dot(sceneView.camera.transform.forward, _pivotNormal) < 0)
            {
                gizmoNormal = -_pivotNormal;
            }
            Quaternion rotation = Quaternion.LookRotation(gizmoNormal, _pivotUp);
            rotation *= Quaternion.Euler(0, 0, -_propRotation.floatValue);

            Quaternion moved = Handles.RotationHandle(rotation, _pivotPoint);
            if(moved != rotation)
            {
                Quaternion delta = Quaternion.Inverse(rotation) * moved;
                float deltaAngle = delta.eulerAngles.z;
                DecalTool.SetClampedRotation(_propRotation, _propRotation.floatValue - deltaAngle);
            }
        }

        Vector3 _initalScale;
        void ScaleMode(SceneView sceneView)
        {
            GetPivot();
            Vector3 gizmoNormal = _pivotNormal;
            if(Vector3.Dot(sceneView.camera.transform.forward, _pivotNormal) < 0)
            {
                gizmoNormal = -_pivotNormal;
            }
            Quaternion rotation = Quaternion.LookRotation(gizmoNormal, _pivotUp);
            rotation *= Quaternion.Euler(0, 0, -_propRotation.floatValue);

            if(Event.current.type == EventType.MouseDown && Event.current.button == 0)
            {
                _initalScale = _propScale.vectorValue;
            }

            Vector3 moved = Handles.ScaleHandle(Vector3.one, _pivotPoint, rotation, HandleUtility.GetHandleSize(_pivotPoint));
            if(moved != Vector3.one)
            {
                Vector4 scale = _initalScale;
                scale.x *= moved.x;
                scale.y *= moved.y;
                _propScale.vectorValue = scale;
            }
        }

        Vector4 _initalOffset;
        void OffsetMode(SceneView sceneView)
        {
            GetPivot();
            Vector3 normal = _pivotNormal;
            if(Vector3.Dot(sceneView.camera.transform.forward, _pivotNormal) < 0)
            {
                _pivotNormal = -_pivotNormal;
            }
            Quaternion rotation = Quaternion.LookRotation(_pivotNormal, _pivotUp);
            rotation *= Quaternion.Euler(0, 0, -_propRotation.floatValue);

            if(Event.current.type == EventType.MouseDown && Event.current.button == 0)
            {
                _initalOffset = _propOffset.vectorValue;
            }

            float size = HandleUtility.GetHandleSize(_pivotPoint);
            float left = Handles.ScaleValueHandle(1, _pivotPoint + rotation * Vector3.left * size, rotation * Quaternion.Euler(0, -90, 0), size * 5, Handles.ArrowHandleCap, 0);
            float right = Handles.ScaleValueHandle(1, _pivotPoint + rotation * Vector3.right * size, rotation * Quaternion.Euler(0, 90, 0), size * 5, Handles.ArrowHandleCap, 0);
            float down = Handles.ScaleValueHandle(1, _pivotPoint + rotation * Vector3.down * size, rotation * Quaternion.Euler(90, 0, 0), size * 5, Handles.ArrowHandleCap, 0);
            float up = Handles.ScaleValueHandle(1, _pivotPoint + rotation * Vector3.up * size, rotation * Quaternion.Euler(-90, 0, 0), size * 5, Handles.ArrowHandleCap, 0);
            if(left != 1 || right != 1 || down != 1 || up != 1)
            {
                Vector4 offset = _initalOffset;
                offset.x -= (left - 1) * _propScale.vectorValue.x * 0.25f;
                offset.y += (right - 1) * _propScale.vectorValue.x * 0.25f;
                offset.z -= (down - 1) * _propScale.vectorValue.y * 0.25f;
                offset.w += (up - 1) * _propScale.vectorValue.y * 0.25f;
                _propOffset.vectorValue = offset;
            }
        }

        Vector3 _pivotPoint;
        Vector3 _pivotNormal;
        Vector3 _pivotUp;
        void GetPivot()
        {
            _pivotPoint = Vector3.zero;
            _pivotNormal = Vector3.zero;

            Vector2 uv = _propPosition.vectorValue;
            Vector2 uvUp = uv + Vector2.up * 0.0001f;
            // uv position to world position using renderer mesh
            for(int i=0; i<_worldTriangles.Length;i++)
            {
                Vector2[] uvTriangle = _uvTriangles[i];
                float a = TriangleArea(uvTriangle[0], uvTriangle[1], uvTriangle[2]);
                if(a == 0) continue;
                // check if uv is inside uvTriangle
                float a1 = TriangleArea(uvTriangle[1], uvTriangle[2], uv) / a;
                if(a1 < 0) continue;
                float a2 = TriangleArea(uvTriangle[2], uvTriangle[0], uv) / a;
                if(a2 < 0) continue;
                float a3 = TriangleArea(uvTriangle[0], uvTriangle[1], uv) / a;
                if(a3 < 0) continue;
                // get a1, a2, a3 of uv up
                float a1Up = TriangleArea(uvTriangle[1], uvTriangle[2], uvUp) / a;
                float a2Up = TriangleArea(uvTriangle[2], uvTriangle[0], uvUp) / a;
                float a3Up = TriangleArea(uvTriangle[0], uvTriangle[1], uvUp) / a;
                // point inside the triangle - find mesh position by interpolation
                Vector3[] triangle = _worldTriangles[i];
                _pivotPoint = triangle[0] * a1 + triangle[1] * a2 + triangle[2] * a3;
                _pivotNormal = Vector3.Cross(triangle[1] - triangle[0], triangle[2] - triangle[0]).normalized;
                _pivotUp = (triangle[0] * a1Up + triangle[1] * a2Up + triangle[2] * a3Up - _pivotPoint).normalized;
                return;
            }
        }

        bool RaycastToClosestUV(Ray ray, ref Vector2 uv)
        {
            Vector4 scaleOffset = _propOffset.vectorValue;
            scaleOffset = new Vector4(-scaleOffset.x, scaleOffset.y, -scaleOffset.z, scaleOffset.w);
            Vector2 centerOffset = new Vector2((scaleOffset.x + scaleOffset.y)/2, (scaleOffset.z + scaleOffset.w)/2);

            float minDistance = float.MaxValue;
            for(int i=0; i<_worldTriangles.Length;i++)
            {
                Vector3[] triangle = _worldTriangles[i];
                // raycast to triangle
                Plane plane = new Plane(triangle[0], triangle[1], triangle[2]);
                float distance;
                if(plane.Raycast(ray, out distance))
                {
                    Vector3 hitPoint = ray.GetPoint(distance);
                    // check if hitPoint is inside triangle
                    float a = TriangleArea(triangle[0], triangle[1], triangle[2]);
                    if(a == 0) continue;
                    float a1 = TriangleArea(triangle[1], triangle[2], hitPoint) / a;
                    if(a1 < 0) continue;
                    float a2 = TriangleArea(triangle[2], triangle[0], hitPoint) / a;
                    if(a2 < 0) continue;
                    float a3 = TriangleArea(triangle[0], triangle[1], hitPoint) / a;
                    if(a3 < 0) continue;
                    if(distance < minDistance)
                    {
                        minDistance = distance;
                        // point inside the triangle - find uv by interpolation
                        Vector2[] uvTriangle = _uvTriangles[i];
                        uv = uvTriangle[0] * a1 + uvTriangle[1] * a2 + uvTriangle[2] * a3;
                        uv = uv - centerOffset;
                    }
                }
            }
            return minDistance != float.MaxValue;
        }

        float TriangleArea(Vector2 a, Vector2 b, Vector2 c)
        {
            var v1 = a - c;
            var v2 = b - c;
            return (v1.x * v2.y - v1.y * v2.x) / 2;
        }

        void GetMesh()
        {
            if(_renderer is MeshRenderer)
            {
                _mesh = _renderer.GetComponent<MeshFilter>().sharedMesh;
            }
            else if(_renderer is SkinnedMeshRenderer)
            {
                _mesh = new Mesh();
                (_renderer as SkinnedMeshRenderer).BakeMesh(_mesh);
            }
        }
    }
}