using System;
using Framework.Geometry;
using UnityEditor;
using UnityEngine;
namespace Framework.Actors.Platformer {
  
  /// Allows the user to edit the abilities in the inspector window
  [CustomEditor(typeof(Abilities))]
  public class AbilitiesEditor : Editor {
    /// Height of the diagram to illustrate the jump trajectory
    private const float DiagramHeight = 150.0f;
    
    /// Name of the buttons for selecting to number of possible jumps
    private static readonly string[] JumpCountNames = {"None", "1", "2", "3", "4"};
    /// Maximum running speed the developer can configure
    private const float MaximumRunSpeed = 30.0f;
    /// Maximum jump height the developer can configure
    private const float MaximumJumpHeight = 10.0f;
    #region struct GuiClippingScope
    /// Scope that enter a GUI clipping area for its duration
    private struct GuiClippingScope : IDisposable {
      
      /// Initialized a new GUI clipping scope
      /// Clipping rectangle the scope will set
      public GuiClippingScope(Rect clippingRectangle) {
        GUI.BeginClip(clippingRectangle);        
      }
      /// Leaves the clipping rectangle again
      public void Dispose() {
        GUI.EndClip();
      }
    }
    
    #endregion // struct GuiClippingScope
    #region struct GlMatrixStackScope
    /// Pushes the current GL matrix onto the stack for its duration
    private struct GlMatrixStackScope : IDisposable {
      
      /// Pushes the current GL matrix onto the stack
      /// Not used
      public GlMatrixStackScope(int dummy) {
        GL.PushMatrix();
      }
      /// Pops the GL matrix from the stack again
      public void Dispose() {
        GL.PopMatrix();
      }
    }
    
    #endregion // struct GlMatrixStackScope
    
    #region struct GlCommandScope
    /// Begins a GL command and ends it after the scope expires
    private struct GlCommandScope : IDisposable {
      
      /// Begins a GL command for the specified primitive topology
      /// Primitive topology that will be drawn
      public GlCommandScope(int mode) {
        GL.Begin(mode);
      }
      /// Ends the GL command again
      public void Dispose() {
        GL.End();
      }
    }
    
    #endregion // struct GlCommandScope
    private bool areVelocityControlsExpanded;
    
    /// Called when the editor is added to the inspector panel
    protected void OnEnable() {
      Shader shader = Shader.Find("Hidden/Internal-Colored");
      this.material = new Material(shader);
    }
    
    /// Called when the editor is removed from the inspector panel
    protected void OnDisable() {
      DestroyImmediate(this.material);
    }
    
    /// Called when the inspector GUI is layouted or rendered
    public override void OnInspectorGUI() {
      Abilities abilities = target as Abilities;
      if(abilities == null) {
        Debug.LogError(
          "Abilities editor shown for a component that was not an instance of " +
          "the abilities class."
        );
        return;
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Jump Count");
        abilities.JumpCount = GUILayout.SelectionGrid(abilities.JumpCount, JumpCountNames, 5);
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Air Control");
        abilities.AirControlFactor = GUILayout.HorizontalSlider(
          abilities.AirControlFactor, 0.0f, 1.0f
        );
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Jump Height");
        abilities.JumpHeight = GUILayout.VerticalSlider(
          abilities.JumpHeight, MaximumJumpHeight, 0.0f,
          GUILayout.MaxHeight(DiagramHeight)
        );
        drawJumpCurves(abilities);
      }
      if(abilities.SquatMovement != SquatMovementMode.Prohibit) {
        using(new EditorGUILayout.HorizontalScope()) {
          EditorGUILayout.PrefixLabel("Sneak/Crawl Speed");
          abilities.SneakingSpeed = GUILayout.HorizontalSlider(
            abilities.SneakingSpeed, 0.0f, MaximumRunSpeed
          );
        }
      }
      
      EditorGUILayout.MinMaxSlider(
        "Walk+Run Speed",
        ref abilities.WalkingSpeed, ref abilities.RunningSpeed,
        0.0f, MaximumRunSpeed
      );
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Dash Speed");
        abilities.DashingSpeed = GUILayout.HorizontalSlider(
          abilities.DashingSpeed, 0.0f, MaximumRunSpeed
        );
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Speed for Floor Slide");
        abilities.SpeedForFloorSlide= GUILayout.HorizontalSlider(
          abilities.SpeedForFloorSlide, 0.0f, MaximumRunSpeed
        );
      }
      this.areVelocityControlsExpanded = EditorGUILayout.Foldout(
        this.areVelocityControlsExpanded, "Speed in Numbers", toggleOnLabelClick: true
      );
      if(this.areVelocityControlsExpanded) {
        using(new EditorGUILayout.HorizontalScope()) {
          EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
          EditorGUILayout.PrefixLabel("Walking Speed");
          string walkingSpeedString = GUILayout.TextField(
            abilities.WalkingSpeed.ToString("0.0"), 6
          );
          float.TryParse(walkingSpeedString, out abilities.WalkingSpeed);
        }
        using(new EditorGUILayout.HorizontalScope()) {
          EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
          EditorGUILayout.PrefixLabel("Running Speed");
          string runningSpeedString = GUILayout.TextField(
            abilities.RunningSpeed.ToString("0.0"), 6
          );
          float.TryParse(runningSpeedString, out abilities.RunningSpeed);
        }
        using(new EditorGUILayout.HorizontalScope()) {
          EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
          EditorGUILayout.PrefixLabel("Dash Speed");
          string dashSpeedString = GUILayout.TextField(
            abilities.DashingSpeed.ToString("0.0"), 6
          );
          float.TryParse(dashSpeedString, out abilities.DashingSpeed);
        }
        using(new EditorGUILayout.HorizontalScope()) {
          EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
          EditorGUILayout.PrefixLabel("Sneak Speed");
          string sneakingSpeedString = GUILayout.TextField(
            abilities.SneakingSpeed.ToString("0.0"), 6
          );
          float.TryParse(sneakingSpeedString, out abilities.SneakingSpeed);
        }
        using(new EditorGUILayout.HorizontalScope()) {
          EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
          EditorGUILayout.PrefixLabel("Speed for Floor Slide");
          string floorSlideSpeedString = GUILayout.TextField(
            abilities.SpeedForFloorSlide.ToString("0.0"), 6
          );
          float.TryParse(floorSlideSpeedString, out abilities.SpeedForFloorSlide);
        }
        using(new EditorGUILayout.HorizontalScope()) {
          EditorGUILayout.LabelField(" ", GUILayout.Width(10.0f));
          EditorGUILayout.PrefixLabel("Maximum Jump Height");
          string jumpHeightdString = GUILayout.TextField(
            abilities.JumpHeight.ToString("0.0"), 6
          );
          float.TryParse(jumpHeightdString, out abilities.JumpHeight);
        }
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Can Sprint");
        abilities.CanSprint = EditorGUILayout.Toggle(abilities.CanSprint);
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Can Dash");
        abilities.CanDash = EditorGUILayout.Toggle(abilities.CanDash);
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Can Dash In-Air");
        abilities.CanAirDash = EditorGUILayout.Toggle(abilities.CanAirDash);
      }
      
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Can Wall-Jump");
        abilities.CanWallJump = EditorGUILayout.Toggle(abilities.CanWallJump);
      }
      
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Slide down Walls");
        abilities.CanWallSlide = EditorGUILayout.Toggle(abilities.CanWallSlide);
      }
      
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Wall Hang Mode");
        abilities.WallHanging = (LedgeHangMode)EditorGUILayout.EnumPopup(abilities.WallHanging);
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Pole Hang Mode");
        abilities.PoleHanging = (LedgeHangMode)EditorGUILayout.EnumPopup(abilities.PoleHanging);
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Can Squat");
        abilities.CanSquat = EditorGUILayout.Toggle(abilities.CanSquat);
      }
      using(new EditorGUILayout.HorizontalScope()) {
        EditorGUILayout.PrefixLabel("Squat Movement Mode");
        abilities.SquatMovement = (SquatMovementMode)EditorGUILayout.EnumPopup(
          abilities.SquatMovement
        );
      }
      
      //base.OnInspectorGUI();
    }
    
    /// Draws the jump curves currently set up by the developer
    /// Abilities the editor is letting the developer edit
    private void drawJumpCurves(Abilities abilities) {
      Rect controlRectangle = GUILayoutUtility.GetRect(10, 1000, DiagramHeight, DiagramHeight);
      if(Event.current.type == EventType.Repaint) {
        using(new GuiClippingScope(controlRectangle)) {
          
          // Adjust control rectangle for inside clipping scope 
          controlRectangle = new Rect(0.0f, 0.0f, controlRectangle.width, controlRectangle.height);
          // World coordinate area that will be displayed within the illustration
          Rect viewRectangle;
          {
            float maximumJumpOffVelocity = Trajectory.GetJumpOffVelocity(MaximumJumpHeight);
            viewRectangle = new Rect(
              0.0f,
              MaximumJumpHeight,
              Trajectory.GetTimeInFlight(maximumJumpOffVelocity) * MaximumRunSpeed,
              -MaximumJumpHeight // flip in Y (control coordinates are Y-down)
            );
          }
          
          GL.PushMatrix();
          this.material.SetPass(0);
          // Fill the background
          glFillRectangle(controlRectangle, inspectorControlBackgroundColor);
          // Draw the jump trajectory for a jump from walking speed
          glDrawJumpTrajectory(
            controlRectangle, viewRectangle,
            abilities.WalkingSpeed, abilities.JumpHeight, Color.cyan
          );
          
          // Draw the jump trajectory for a jump from running speed
          glDrawJumpTrajectory(
            controlRectangle, viewRectangle,
            abilities.RunningSpeed, abilities.JumpHeight, Color.green
          );
          GL.PopMatrix();
        } // GUI clipping rectangle
      }
    }
    /// 
    ///   Draws the jump trajectory for a given horizontal velocity and jump height
    /// 
    /// Pixel coordinates covered by the control
    /// Rectangle of the world coordinates in the view
    /// Horizontal velocity of the jumping actor
    /// Height of the jump the actor will perform
    /// Color of the jump trajectory line
    private static void glDrawJumpTrajectory(
      Rect controlRectangle, Rect viewRectangle,
      float horizontalVelocity, float jumpHeight, Color color
    ) {
      using(new GlCommandScope(GL.LINE_STRIP)) {
        GL.Color(color);
        // Jump-off velocity required to reach the desired height
        float initialVelocity = Trajectory.GetJumpOffVelocity(jumpHeight);
        
        // Draw the jump trajectory curve by stepping 1 unit in world coordinates.
        // This will guarantee optimal resolution.
        int startX = Mathf.FloorToInt(controlRectangle.xMin);
        int endX = Mathf.CeilToInt(controlRectangle.xMax);
        for(int pixelX = startX; pixelX < endX; ++pixelX) {
          // Calculate the time at which the actor would reach this X coordinate
          float time;
          {
            float windowX = (pixelX - controlRectangle.x) / controlRectangle.width;
            float worldX = windowX * viewRectangle.width + viewRectangle.x;
            time = worldX / horizontalVelocity;
          }
          
          // Calculate at which height in the parabola trajectory the actor should be now 
          float y = Trajectory.GetParabolaY(initialVelocity, time);
          // Convert the world Y coordinate into pixel Y coordinate 
          float pixelY;
          {
            float windowY = (y - viewRectangle.y) / viewRectangle.height;
            pixelY = windowY * controlRectangle.height + controlRectangle.y;
          }
          GL.Vertex3((float)pixelX, pixelY, 0.0f);
          // If the actor has fallen back to the floor, stop drawing
          if(y < 0.0f) {
            break;
          }          
        }
      }
    }
    
    /// Issues the GL commands to fill a rectangle
    /// Coordinates of the filled rectangle
    /// Color the rectangle will be filled with
    private static void glFillRectangle(Rect rectangle, Color color) {
      using(new GlCommandScope(GL.QUADS)) {
        GL.Color(color);
        GL.Vertex3(rectangle.xMin, rectangle.yMin, 0.0f);
        GL.Vertex3(rectangle.xMax, rectangle.yMin, 0.0f);
        GL.Vertex3(rectangle.xMax, rectangle.yMax, 0.0f);
        GL.Vertex3(rectangle.xMin, rectangle.yMax, 0.0f);
      }
    }
    /// Issues the GL commands to draw a rectangle
    /// Coordinates of the rectangle
    /// Color of the rectangle's outline
    private static void glDrawRectangle(Rect rectangle, Color color) {
      using(new GlCommandScope(GL.LINE_STRIP)) {
        GL.Color(color);
        GL.Vertex3(rectangle.xMin, rectangle.yMin, 0.0f);
        GL.Vertex3(rectangle.xMax, rectangle.yMin, 0.0f);
        GL.Vertex3(rectangle.xMax, rectangle.yMax, 0.0f);
        GL.Vertex3(rectangle.xMin, rectangle.yMax, 0.0f);
      }
    }
    /// Background color the inspector panel itself
    private static Color inspectorPanelBackgroundColor {
      get {
        if(EditorGUIUtility.isProSkin) {
          return (Color)new Color32(56, 56, 56, 255);
        } else {
          return (Color)new Color32(224, 224, 224, 255);
        }
      }
    }
    
    /// Background color of controls in the inspector panel
    private static Color inspectorControlBackgroundColor {
      get {
        if(EditorGUIUtility.isProSkin) {
          return (Color)new Color32(76, 76, 76, 255);
        } else {
          return (Color)new Color32(224, 224, 224, 255);
        }
      }
    }
    
    /// Material that is used to draw in the inspector diagram
    private Material material;
    
  }
  
} // namespace Framework.Actors.Platformer