// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using UnityEngine;

namespace HoloToolkit.Unity.SpatialMapping
{
    /// <summary>
    /// All possible plane types that a SurfacePlane can be.
    /// </summary>
    [Flags]
    public enum PlaneTypes
    {
        Wall = 0x1,
        Floor = 0x2,
        Ceiling = 0x4,
        Table = 0x8,
        Unknown = 0x10
    }

    /// <summary>
    /// The SurfacePlane class is used by SurfaceMeshesToPlanes to create different types of planes (walls, floors, tables, etc.) 
    /// based on the Spatial Mapping data returned by the SpatialMappingManager's source.
    /// This script should be a component on the SufacePlane prefab, which is used by SurfaceMeshesToPlanes.
    /// </summary>
    public class SurfacePlane : MonoBehaviour
    {
        [Tooltip("Thickness to make each plane.")]
        [Range(0.0f, 1.0f)]
        public float PlaneThickness = 0.01f;

        [Tooltip("Threshold for acceptable normals (the closer to 1, the stricter the standard). Used when determining plane type.")]
        [Range(0.0f, 1.0f)]
        public float UpNormalThreshold = 0.9f;

        [Tooltip("Buffer to use when determining if a horizontal plane near the floor should be considered part of the floor.")]
        [Range(0.0f, 1.0f)]
        public float FloorBuffer = 0.1f;

        [Tooltip("Buffer to use when determining if a horizontal plane near the ceiling should be considered part of the ceiling.")]
        [Range(0.0f, 1.0f)]
        public float CeilingBuffer = 0.1f;

        [Tooltip("Material to use when rendering Wall planes.")]
        public Material WallMaterial;

        [Tooltip("Material to use when rendering floor planes.")]
        public Material FloorMaterial;

        [Tooltip("Material to use when rendering ceiling planes.")]
        public Material CeilingMaterial;

        [Tooltip("Material to use when rendering table planes.")]
        public Material TableMaterial;

        [Tooltip("Material to use when rendering planes of the unknown type.")]
        public Material UnknownMaterial;

        [Tooltip("Type of plane that the object has been classified as.")]
        public PlaneTypes PlaneType = PlaneTypes.Unknown;

        /// <summary>
        /// The BoundedPlane associated with the SurfacePlane object.
        /// </summary>
        private BoundedPlane plane = new BoundedPlane();

        /// <summary>
        /// Gets or Sets the BoundedPlane, which determines the orientation/size/position of the gameObject.
        /// </summary>
        public BoundedPlane Plane
        {
            get
            {
                return plane;
            }
            set
            {
                plane = value;
                UpdateSurfacePlane();
            }
        }

        /// <summary>
        /// Gets the normal of the plane that was determined by the BoundedPlane object.
        /// </summary>
        public Vector3 SurfaceNormal { get; private set; }

        /// <summary>
        /// Gets or sets the visibility of the current gameObject.
        /// </summary>
        public bool IsVisible
        {
            get
            {
                return gameObject.GetComponent<Renderer>().enabled;
            }
            set
            {
                if (IsVisible != value)
                {
                    gameObject.GetComponent<Renderer>().enabled = value;
                }
            }
        }

        private void Awake()
        {
            plane = new BoundedPlane(transform);
        }

        private void Start()
        {
            UpdateSurfacePlane();
        }

        /// <summary>
        /// Updates the SurfacePlane object to have the same configuration of the BoundingPlane object.
        /// Determine what type of plane the SurfacePlane aligns to.
        /// Sets the material based on the plane type.
        /// </summary>
        private void UpdateSurfacePlane()
        {
            SetPlaneGeometry();
            SetPlaneType();
            SetPlaneMaterialByType();
        }

        /// <summary>
        /// Updates the plane geometry to match the bounded plane found by SurfaceMeshesToPlanes.
        /// </summary>
        private void SetPlaneGeometry()
        {
            // Set the SurfacePlane object to have the same extents as the BoundingPlane object.
            gameObject.transform.position = plane.Bounds.Center;
            gameObject.transform.rotation = plane.Bounds.Rotation;
            Vector3 extents = plane.Bounds.Extents * 2;
            gameObject.transform.localScale = new Vector3(extents.x, extents.y, PlaneThickness);
        }

        /// <summary>
        /// Classifies the surface as a floor, wall, ceiling, table, etc.
        /// </summary>
        private void SetPlaneType()
        {
            SurfaceNormal = plane.Plane.normal;
            float floorYPosition = SurfaceMeshesToPlanes.Instance.FloorYPosition;
            float ceilingYPosition = SurfaceMeshesToPlanes.Instance.CeilingYPosition;

            // Determine what type of plane this is.
            // Use the upNormalThreshold to help determine if we have a horizontal or vertical surface.
            if (SurfaceNormal.y >= UpNormalThreshold)
            {
                // If we have a horizontal surface with a normal pointing up, classify it as a floor.
                PlaneType = PlaneTypes.Floor;

                if (gameObject.transform.position.y > (floorYPosition + FloorBuffer))
                {
                    // If the plane is too high to be considered part of the floor, classify it as a table.
                    PlaneType = PlaneTypes.Table;
                }
            }
            else if (SurfaceNormal.y <= -(UpNormalThreshold))
            {
                // If we have a horizontal surface with a normal pointing down, classify it as a ceiling.
                PlaneType = PlaneTypes.Ceiling;

                if (gameObject.transform.position.y < (ceilingYPosition - CeilingBuffer))
                {
                    // If the plane is not high enough to be considered part of the ceiling, classify it as a table.
                    PlaneType = PlaneTypes.Table;
                }
            }
            else if (Mathf.Abs(SurfaceNormal.y) <= (1 - UpNormalThreshold))
            {
                // If the plane is vertical, then classify it as a wall.
                PlaneType = PlaneTypes.Wall;
            }
            else
            {
                // The plane has a strange angle, classify it as 'unknown'.
                PlaneType = PlaneTypes.Unknown;
            }
        }

        /// <summary>
        /// Sets the renderer material to match the object's plane type.
        /// </summary>
        private void SetPlaneMaterialByType()
        {
            Renderer renderer = gameObject.GetComponent<Renderer>();

            switch (PlaneType)
            {
                case PlaneTypes.Floor:
                    if (FloorMaterial != null)
                    {
                        renderer.material = FloorMaterial;
                    }
                    break;
                case PlaneTypes.Table:
                    if (TableMaterial != null)
                    {
                        renderer.material = TableMaterial;
                    }
                    break;
                case PlaneTypes.Ceiling:
                    if (CeilingMaterial != null)
                    {
                        renderer.material = CeilingMaterial;
                    }
                    break;
                case PlaneTypes.Wall:
                    if (WallMaterial != null)
                    {
                        renderer.material = WallMaterial;
                    }
                    break;
                default:
                    if (UnknownMaterial != null)
                    {
                        renderer.material = UnknownMaterial;
                    }
                    break;
            }
        }
    }
}