/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * Licensed under the Oculus SDK License Agreement (the "License");
 * you may not use the Oculus SDK except in compliance with the License,
 * which is provided at the time of installation or download, or which
 * otherwise accompanies this software in either electronic or hard copy form.
 *
 * You may obtain a copy of the License at
 *
 * https://developer.oculus.com/licenses/oculussdk/
 *
 * Unless required by applicable law or agreed to in writing, the Oculus SDK
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using Oculus.Interaction.Surfaces;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace Oculus.Interaction.UnityCanvas
{
    public class CanvasCylinder : CanvasMesh, ICurvedPlane, ICylinderClipper
    {
        [Serializable]
        public struct MeshGenerationSettings
        {
            [Delayed]
            public float VerticesPerDegree;

            [Delayed]
            public int MaxHorizontalResolution;

            [Delayed]
            public int MaxVerticalResolution;
        }

        public const int MIN_RESOLUTION = 2;

        [SerializeField]
        [Tooltip("The cylinder used to dictate the position and radius of the mesh.")]
        private Cylinder _cylinder;

        [SerializeField]
        [Tooltip("Determines how the mesh is projected on the cylinder wall. " +
            "Vertical results in a left-to-right curvature, Horizontal results in a top-to-bottom curvature.")]
        private CylinderOrientation _orientation = CylinderOrientation.Vertical;

        [SerializeField]
        private MeshGenerationSettings _meshGeneration = new MeshGenerationSettings()
        {
            VerticesPerDegree = 1.4f,
            MaxHorizontalResolution = 128,
            MaxVerticalResolution = 32
        };

        public float Radius => _cylinder.Radius;
        public Cylinder Cylinder => _cylinder;
        public float ArcDegrees { get; private set; }
        public float Rotation { get; private set; }
        public float Bottom { get; private set; }
        public float Top { get; private set; }

        private float CylinderRelativeScale => _cylinder.transform.lossyScale.x / transform.lossyScale.x;

        public bool GetCylinderSegment(out CylinderSegment segment)
        {
            segment = new CylinderSegment(Rotation, ArcDegrees, Bottom, Top);
            return _started && isActiveAndEnabled;
        }

        protected override void Start()
        {
            this.BeginStart(ref _started, () => base.Start());
            this.AssertField(_cylinder, nameof(_cylinder));
            this.EndStart(ref _started);
        }

#if UNITY_EDITOR
        protected virtual void OnValidate()
        {
            _meshGeneration.MaxHorizontalResolution = Mathf.Max(MIN_RESOLUTION,
                _meshGeneration.MaxHorizontalResolution);
            _meshGeneration.MaxVerticalResolution = Mathf.Max(MIN_RESOLUTION,
                _meshGeneration.MaxVerticalResolution);
            _meshGeneration.VerticesPerDegree = Mathf.Max(0, _meshGeneration.VerticesPerDegree);

            if (Application.isPlaying && _started)
            {
                EditorApplication.delayCall += () =>
                {
                    UpdateImposter();
                };
            }
        }
#endif

        protected override void UpdateImposter()
        {
            base.UpdateImposter();
            UpdateMeshPosition();
            UpdateCurvedPlane();
        }

        protected override Vector3 MeshInverseTransform(Vector3 localPosition)
        {
            float angle = Mathf.Atan2(localPosition.x, localPosition.z + Radius);
            float x = angle * Radius;
            float y = localPosition.y;
            return new Vector3(x, y);
        }

        protected override void GenerateMesh(out List<Vector3> verts,
                                             out List<int> tris,
                                             out List<Vector2> uvs)
        {
            verts = new List<Vector3>();
            tris = new List<int>();
            uvs = new List<Vector2>();

            Vector2 worldSize = GetWorldSize();
            float scaledRadius = Radius * CylinderRelativeScale;

            float xPos = worldSize.x * 0.5f;
            float xNeg = -xPos;
            float yPos = worldSize.y * 0.5f;
            float yNeg = -yPos;

            Vector2Int GetClampedResolution(float arcMax, float axisMax)
            {
                int horizontalResolution = Mathf.Max(2,
                    Mathf.RoundToInt(_meshGeneration.VerticesPerDegree *
                    Mathf.Rad2Deg * arcMax / scaledRadius));
                int verticalResolution =
                    Mathf.Max(2, Mathf.RoundToInt(horizontalResolution * axisMax / arcMax));

                horizontalResolution = Mathf.Clamp(horizontalResolution, 2,
                    _meshGeneration.MaxHorizontalResolution);
                verticalResolution = Mathf.Clamp(verticalResolution, 2,
                    _meshGeneration.MaxVerticalResolution);

                return new Vector2Int(horizontalResolution, verticalResolution);
            }

            Vector3 GetCurvedPoint(float u, float v)
            {
                float x = Mathf.Lerp(xNeg, xPos, u);
                float y = Mathf.Lerp(yNeg, yPos, v);

                float angle;
                Vector3 point;

                switch (_orientation)
                {
                    default:
                    case CylinderOrientation.Vertical:
                        angle = x / scaledRadius;
                        point.x = Mathf.Sin(angle) * scaledRadius;
                        point.y = y;
                        point.z = Mathf.Cos(angle) * scaledRadius - scaledRadius;
                        break;
                    case CylinderOrientation.Horizontal:
                        angle = y / scaledRadius;
                        point.x = x;
                        point.y = Mathf.Sin(angle) * scaledRadius;
                        point.z = Mathf.Cos(angle) * scaledRadius - scaledRadius;
                        break;
                }
                return point;
            }

            Vector2Int resolution;
            switch (_orientation)
            {
                default:
                case CylinderOrientation.Vertical:
                    resolution = GetClampedResolution(xPos, yPos);
                    break;
                case CylinderOrientation.Horizontal:
                    resolution = GetClampedResolution(yPos, xPos);
                    break;
            }

            for (int y = 0; y < resolution.y; y++)
            {
                for (int x = 0; x < resolution.x; x++)
                {
                    float u = x / (resolution.x - 1.0f);
                    float v = y / (resolution.y - 1.0f);

                    verts.Add(GetCurvedPoint(u, v));
                    uvs.Add(new Vector2(u, v));
                }
            }

            for (int y = 0; y < resolution.y - 1; y++)
            {
                for (int x = 0; x < resolution.x - 1; x++)
                {
                    int v00 = x + y * resolution.x;
                    int v10 = v00 + 1;
                    int v01 = v00 + resolution.x;
                    int v11 = v00 + 1 + resolution.x;

                    tris.Add(v00);
                    tris.Add(v11);
                    tris.Add(v10);

                    tris.Add(v00);
                    tris.Add(v01);
                    tris.Add(v11);
                }
            }
        }

        private void UpdateMeshPosition()
        {
            Vector3 posInCylinder = _cylinder.transform.InverseTransformPoint(transform.position);

            Vector3 localYOffset = new Vector3(0, posInCylinder.y, 0);
            Vector3 localCancelY = posInCylinder - localYOffset;

            // If canvas position is on cylinder center axis, project forward.
            // Otherwise, project canvas onto cylinder wall from center axis.
            Vector3 projection = Mathf.Approximately(localCancelY.sqrMagnitude, 0f) ?
                                 Vector3.forward : localCancelY.normalized;

            Vector3 localUp;
            switch (_orientation)
            {
                default:
                case CylinderOrientation.Vertical:
                    localUp = Vector3.up;
                    break;
                case CylinderOrientation.Horizontal:
                    localUp = Vector3.right;
                    break;
            }

            transform.position = _cylinder.transform.TransformPoint((projection * _cylinder.Radius) + localYOffset);
            transform.rotation = _cylinder.transform.rotation * Quaternion.LookRotation(projection, localUp);

            if (_meshCollider != null &&
                _meshCollider.transform != transform &&
                !transform.IsChildOf(_meshCollider.transform))
            {
                _meshCollider.transform.position = transform.position;
                _meshCollider.transform.rotation = transform.rotation;
                _meshCollider.transform.localScale *= transform.lossyScale.x / _meshCollider.transform.lossyScale.x;
            }
        }

        private Vector2 GetWorldSize()
        {
            Vector2Int resolution = _canvasRenderTexture.GetBaseResolutionToUse();
            float width = _canvasRenderTexture.PixelsToUnits(Mathf.RoundToInt(resolution.x));
            float height = _canvasRenderTexture.PixelsToUnits(Mathf.RoundToInt(resolution.y));
            return new Vector2(width, height) / transform.lossyScale;
        }

        private void UpdateCurvedPlane()
        {
            // Get world size in cylinder space
            Vector2 cylinderSize = GetWorldSize() / CylinderRelativeScale;

            float arcSize, axisSize;
            switch (_orientation)
            {
                default:
                case CylinderOrientation.Vertical:
                    arcSize = cylinderSize.x;
                    axisSize = cylinderSize.y;
                    break;
                case CylinderOrientation.Horizontal:
                    arcSize = cylinderSize.y;
                    axisSize = cylinderSize.x;
                    break;
            }

            Vector3 posInCylinder = Cylinder.transform.InverseTransformPoint(transform.position);
            Rotation = Mathf.Atan2(posInCylinder.x, posInCylinder.z) * Mathf.Rad2Deg;
            ArcDegrees = (arcSize * 0.5f / Radius) * 2f * Mathf.Rad2Deg;
            Top = posInCylinder.y + (axisSize * 0.5f);
            Bottom = posInCylinder.y - (axisSize * 0.5f);
        }

        #region Inject

        public void InjectAllCanvasCylinder(CanvasRenderTexture canvasRenderTexture,
                                            MeshFilter meshFilter,
                                            Cylinder cylinder,
                                            CylinderOrientation orientation)
        {
            InjectAllCanvasMesh(canvasRenderTexture, meshFilter);
            InjectCylinder(cylinder);
            InjectOrientation(orientation);
        }

        public void InjectCylinder(Cylinder cylinder)
        {
            _cylinder = cylinder;
        }

        public void InjectOrientation(CylinderOrientation orientation)
        {
            _orientation = orientation;
        }

        #endregion
    }
}
