using System;
using System.Collections;
using System.Collections.Generic;
using Framework.Layers;
using UnityEngine;
using UnityEditor;
namespace Framework.Navigation.Platformer {
  /// 
  ///   Assists the user in editing ground definitions for the navigation system
  /// 
  [CustomEditor(typeof(Ground))]
  public class GroundEditor : Editor {
    /// Tolerance below which poly lines are collapsed into single segments
    /// 
    ///   This is used by the automatic ground-polyline generation, which first samples
    ///   the whole of the continuous ground and then simplifies this into a polyline
    ///   that has as few points as possible.
    /// 
    private const float CollapseTolerance = 0.01f;
    /// How far from the floor the navigation ground will be placed
    /// 
    ///   This helps prevent flickering and feet sinking into the floor. If the value is
    ///   too high, it would look like actors have magic flying boots on.
    /// 
    private const float FloorDistance = 0.015f;
    /// Creates a new ground assigned to the right layer and all
    [MenuItem("GameObject/Create Other/Ground (Platformer Navigation)")]
    public static void CreateGround() {
      int layerIndex = LayerMask.NameToLayer("Navigation");
      if(layerIndex == -1) {
        Debug.LogWarning(
          "Could not find the 'Navigation' layer. You should add a custom layer with this" +
          "name and configure Unity's physics settings so that it doesn't collide with " +
          "anything. Falling back to 'Default' layer for this Ground object."
        );
      }
      GameObject groundGameObject = new GameObject("NavigationGround");
      if(layerIndex != -1) {
        groundGameObject.layer = layerIndex;
      }
      // This allows the navigation system to tell agents where they are
      BoxCollider collider = groundGameObject.AddComponent();
      collider.isTrigger = true;
      // Create a ground node with two default points
      Ground ground = groundGameObject.AddComponent();
      ground.Heights = new Vector2[] { new Vector2(-0.5f, 0.0f), new Vector2(+0.5f, 0.0f) };
    }
    /// Called when the Unity editor draws the scene overlay GUI
    protected void OnSceneGUI() {
      drawGroundVertices();
      if(GUI.changed) {
        EditorUtility.SetDirty(target);
      }
      if(this.selectedHeightSampleIndex != -1) {
        Handles.BeginGUI();
        Vector2 buttonPosition = HandleUtility.WorldToGUIPoint(getSelectedHandlePosition());
        if(GUI.Button(new Rect(buttonPosition.x + 45, buttonPosition.y - 60, 30, 30), "+")) {
          insertNewSample();
        }
        if(GUI.Button(new Rect(buttonPosition.x - 75, buttonPosition.y - 60, 30, 30), "-")) {
          removeCurrentSample();
        }
        Handles.EndGUI();
      }
    }
    /// Called when Unity wants to layout or draw the inspector
    public override void OnInspectorGUI() {
      base.OnInspectorGUI();
      if(GUILayout.Button("Fit Ground")) {
        var ground = (Ground)target;
        FitGround(ground);
      }
      if(GUILayout.Button("Fit Collider")) {
        var ground = (Ground)target;
        FitGround(ground);
      }
    }
    /// Retrieves the position of the current selected handle
    /// The position of the currently selected handle
    private Vector3 getSelectedHandlePosition() {
      var ground = (Ground)target;
      if(this.selectedHeightSampleIndex == -1) {
        return ground.transform.position;
      } else {
        Vector2 height = ground.Heights[this.selectedHeightSampleIndex];
        Vector3 position = ground.transform.position;
        return position + new Vector3(height.x, height.y, 0.0f);
      }
    }
    /// Removes the currently selected height sample from the ground
    private void removeCurrentSample() {
      if(this.selectedHeightSampleIndex == -1) {
        return;
      }
      var ground = (Ground)target;
      Vector2[] previousHeights = ground.Heights;
      ground.Heights = new Vector2[ground.Heights.Length - 1];
      for(int index = 0; index < ground.Heights.Length; ++index) {
        int previousIndex = index;
        if(index >= this.selectedHeightSampleIndex) {
          ++previousIndex;
        }
        ground.Heights[index] = previousHeights[previousIndex];
      }
      if(this.selectedHeightSampleIndex >= ground.Heights.Length) {
        this.selectedHeightSampleIndex = ground.Heights.Length - 1;
      }
    }
    /// Inserts a new sample behind the current selected sample
    private void insertNewSample() {
      if(this.selectedHeightSampleIndex == -1) {
        return; // Or use last sample?
      }
      var ground = (Ground)target;
      Vector2[] previousHeights = ground.Heights;
      ground.Heights = new Vector2[ground.Heights.Length + 1];
      for(int index = 0; index < ground.Heights.Length; ++index) {
        int previousIndex = index;
        if(index > this.selectedHeightSampleIndex) {
          --previousIndex;
        }
        ground.Heights[index] = previousHeights[previousIndex];
      }
      // If a height sample is inserted between two other height samples, place it in
      // the middle of the two samples
      if(ground.Heights.Length > this.selectedHeightSampleIndex + 2) {
        Vector2 center = (
          ground.Heights[this.selectedHeightSampleIndex] +
          ground.Heights[this.selectedHeightSampleIndex + 2]
        ) / 2.0f;
        ground.Heights[this.selectedHeightSampleIndex + 1] = center;
      } else if(this.selectedHeightSampleIndex > 0) {
        Vector2 extrapolated = ground.Heights[this.selectedHeightSampleIndex] + (
          ground.Heights[this.selectedHeightSampleIndex] -
          ground.Heights[this.selectedHeightSampleIndex - 1]
        );
        ground.Heights[this.selectedHeightSampleIndex + 1] = extrapolated;
      }
      ++this.selectedHeightSampleIndex;
    }
    /// 
    ///   Draws editing handles at the locations of the ground's height samples
    /// 
    private void drawGroundVertices() {
      var ground = (Ground)target;
      Vector3 position = ground.transform.position;
      // Go over each height sample and calculate its position in world coordinates
      for(int index = 0; index < ground.Heights.Length; ++index) {
        Vector2 vertex = ground.Heights[index];
        var center = new Vector3(position.x, position.y, position.z);
        center.x += vertex.x;
        center.y += vertex.y;
        // Always draw the normal handle
        drawNormalHandle(index, center);
        // Allow the user to move the currently selected height sample around
        if(index == this.selectedHeightSampleIndex) {
          drawMoveHandle(center);
        }
      }
    }
    /// Draws a normal handle for a height sample
    /// Index if the height sample for which a handle will be drawn
    /// Absolute position of the height sample
    private void drawNormalHandle(int index, Vector3 center) {
      bool wasHandleClicked = Handles.Button(
        center,
        Quaternion.identity,
        0.2f, // size
        0.3f, // pickSize
        Handles.CylinderHandleCap
      );
      if(wasHandleClicked) {
        this.selectedHeightSampleIndex = index;
      }
    }
    /// Draws a handle that supports moving for a height sample
    /// Absolute position of the height sample
    private void drawMoveHandle(Vector3 center) {
      var ground = (Ground)target;
      Vector3 position = ground.transform.position;
      // Let Unity know that this object is being changed so it can take care
      // of recording its state in the undo history when moving ceases.
      Undo.RecordObject(ground, "Move Ground Point");
      // Draw the handle and update the height sample with the new position
      Vector3 newPosition = Handles.PositionHandle(center, Quaternion.identity);
      ground.Heights[this.selectedHeightSampleIndex].x = newPosition.x - position.x;
      ground.Heights[this.selectedHeightSampleIndex].y = newPosition.y - position.y;
    }
    /// Scans the floor and automatically fits a polyline to the walkable area
    /// Ground that will be fitted to the floor mesh
    public void FitGround(Ground ground) {
      IList samples = gatherHeightSamples(ground);
      simplifyHeightSamples(samples);
#if DEBUG_GROUND_FITTING
      Debug.Log("  Simplified to " + samples.Count + " height samples");
      for(int index = 0; index < samples.Count; ++index) {
        Debug.Log("Sample #" + index.ToString() + " at " + samples[index].ToString());
      }
#endif
      if(samples.Count >= 2) {
        float left = samples[0].x;
        float right = samples[samples.Count - 1].x;
        float center = Mathf.Floor((left + right) / 2.0f);
        float lowestY = samples[0].y;
        for(int index = 1; index < samples.Count; ++index) {
          lowestY = samples[index].y;
        }
        Vector3 position = ground.transform.position;
        position.x = center;
        position.y = lowestY + FloorDistance;
        position.z = 0;
        ground.transform.position = position;
        ground.Heights = new Vector2[samples.Count];
        for(int index = 0; index < samples.Count; ++index) {
          ground.Heights[index] = new Vector2(
            samples[index].x - center, samples[index].y - lowestY
          );
        }
      }
    }
    /// Gathers height samples for the navigation ground
    /// All height samples from end to end of the navigation ground
    private static IList gatherHeightSamples(Ground ground) {
      var samples = new List();
      Vector3 position = ground.transform.position;
      position.y += 1.0f;
      int defaultLayerMask = LayerMaskHelper.GetLayerMaskFor("Default");
      RaycastHit hitInfo;
      if(!Physics.Raycast(position, Vector3.down, out hitInfo, 2.0f, defaultLayerMask)) {
        throw new InvalidOperationException(
          "Could not find any solid objects below the navigation ground's location"
        );
      }
      position = hitInfo.point;
      samples.Add(new Vector2(position.x, position.y));
      // Collect samples in negative direction
      for(var offset = new Vector2(); ; offset.x -= 0.1f) {
        Vector3 origin = new Vector3(position.x + offset.x, position.y + offset.y + 1.0f, 0.0f);
        if(!Physics.Raycast(origin, Vector3.down, out hitInfo, 2.0f, defaultLayerMask)) {
          Debug.Log("Ground fitting: Left end at " + origin.ToString() + " in chasm.");
          break;
        }
        float heightDifference = hitInfo.distance - 1.0f;
        if(Mathf.Abs(heightDifference) > 0.1f) {
          Debug.Log("Ground fitting: Left end at " + origin.ToString() + " in wall.");
          break;
        }
        Vector2 sample = new Vector2(hitInfo.point.x, hitInfo.point.y);
        samples.Insert(0, sample);
        offset.y = hitInfo.point.y - position.y;
      }
      // Collect samples in positive direction
      for(var offset = new Vector2(); ; offset.x += 0.1f) {
        Vector3 origin = new Vector3(position.x + offset.x, position.y + offset.y + 1.0f, 0.0f);
        if(!Physics.Raycast(origin, Vector3.down, out hitInfo, 2.0f, defaultLayerMask)) {
          Debug.Log("Ground fitting: Right end at " + origin.ToString() + " in chasm.");
          break;
        }
        float heightDifference = hitInfo.distance - 1.0f;
        if(Mathf.Abs(heightDifference) > 0.1f) {
          Debug.Log("Ground fitting: Right end at " + origin.ToString() + " in wall.");
          break;
        }
        Vector2 sample = new Vector2(hitInfo.point.x, hitInfo.point.y);
        samples.Add(sample);
        offset.y = hitInfo.point.y - position.y;
      }
      return samples;
    }
    /// Simplifies the speicfied polyline
    /// Polyline vertices that will be simplified
    private static void simplifyHeightSamples(IList samples) {
      if((samples != null) && (samples.Count > 2)) {
        eliminateOrSplit(samples, 0, samples.Count - 1);
      }
    }
    /// 
    ///   Eliminates the vertices between the two indices or finds a new split point
    /// 
    /// Samples of the poly line
    /// Index of the first vertex considered for simplification
    /// Index of the last vertex considered for simplification
    private static void eliminateOrSplit(
      IList samples, int leftIndex, int rightIndex
    ) {
      float largestDistance = 0.0f;
      int largestIndex = -1;
      // Look for the vertex with the largest distance to the line segment connecting
      // the corner vertices between which we're working
      for(int index = leftIndex + 1; index < rightIndex; ++index) {
        float distance = squaredDistanceToSegment(
          samples[leftIndex], samples[rightIndex], samples[index]
        );
        if(distance > largestDistance) {
          largestDistance = distance;
          largestIndex = index;
        }
      }
      // If all vertices were within the tolerance, throw them away and replace them by
      // a single line segment
      if(largestDistance < CollapseTolerance) {
        for(int index = rightIndex - 1; index > leftIndex; --index) {
          samples.RemoveAt(index);
        }
      } else { // Otherwise, use the biggest deviation as a new supporting vertex
        eliminateOrSplit(samples, largestIndex, rightIndex);
        eliminateOrSplit(samples, leftIndex, largestIndex);
      }
    }
    /// Calculates the distance of a point to a line segment
    /// Start of the line segment
    /// End of the line segment
    /// Point whose distance to the line segment will be calculated
    /// The distance of the point to the line segment
    private static float squaredDistanceToSegment(Vector2 start, Vector2 end, Vector2 point) {
      Vector2 lineDirection = end - start;
      Vector2 pointOffset = point - start;
      float dot = Vector2.Dot(pointOffset, lineDirection);
      float interval = dot / lineDirection.sqrMagnitude;
#if POINTS_CAN_LIE_PAST_LINE_ENDS
      if(interval <= 0.0f) {
        return pointOffset.sqrMagnitude;
      } else if(interval >= 1.0f) {
        return (point - end).sqrMagnitude;
      } else {
#endif
        return (point - (start + lineDirection * interval)).sqrMagnitude;
#if POINTS_CAN_LIE_PAST_LINE_ENDS
      }
#endif
    }
#if false
    /// Makes the ground's collider fit the covered area
    public void FitCollider() {
      enforceAtLeastTwoHeightsAssigned();
      BoxCollider collider = GetComponent();
      if(collider == null) {
        throw new InvalidOperationException(
          "Cannot fit collider on '" + name + "' because no BoxCollider has been added"
        );
      }
      Vector2 min = this.Heights[0];
      Vector2 max = this.Heights[0];
      for(int index = 1; index < this.Heights.Length; ++index) {
        float x = this.Heights[index].x;
        float y = this.Heights[index].y;
        if(x < min.x) {
          min.x = x;
        }
        if(x > max.x) {
          max.x = x;
        }
        if(y < min.y) {
          min.y = y;
        }
        if(y > max.y) {
          max.y = y;
        }
      }
      max.y += 3.5f; // Player height + jump safety zone
      collider.size = new Vector3(max.x - min.x, max.y - min.y, 2.0f);
      collider.center = new Vector3((min.x + max.x) / 2.0f, (min.y + max.y) / 2.0f, 0.0f);
    }
#endif
    
    /// Index of the currently selected height sample
    private int selectedHeightSampleIndex = -1;
  }
} // namespace Framework.Navigation.Platformer