using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Obi;

public class Swinging : MonoBehaviour
{
    [Header("Swinging")]
    public LineRenderer lr;
    private Vector3 swingPoint;
    private float maxSwingDistance = 100f;
    private float swingRadius = 2f;
    private SpringJoint joint;
    public Transform predictionPoint;
    public RaycastHit predictionHit;
    private bool shouldPredict;


    [Header("References")]
    public LayerMask whatIsGrappleable;
    public Transform gunTip, cam, player;
    private PlayerMovement pm;
    
    [Header("Obi Rope")]
    public ObiCollider playerCollider;
    public float hookExtendRetractSpeed = 2;
    public float hookShootSpeed = 30;
    public ObiSolver solver;
    public Material material;
    public ObiRopeSection section;
    public int particlePoolSize = 500;
    public float hookResolution = 0.5f;

    private ObiRope rope;
    private ObiRopeBlueprint blueprint;
    private ObiRopeExtrudedRenderer ropeRenderer;

    private ObiRopeCursor cursor;


    private void Awake()
    {
        shouldPredict = true;
        // Create both the rope and the solver:	
        rope = gameObject.AddComponent<ObiRope>();
        ropeRenderer = gameObject.AddComponent<ObiRopeExtrudedRenderer>();
        ropeRenderer.section = section;
        ropeRenderer.uvScale = new Vector2(1, 4);
        ropeRenderer.normalizeV = false;
        ropeRenderer.uvAnchor = 1;
        rope.GetComponent<MeshRenderer>().material = material;

        // Setup a blueprint for the rope:
        blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
        blueprint.resolution = 0.5f;
        blueprint.pooledParticles = particlePoolSize;

        // Tweak rope parameters:
        rope.maxBending = 0.02f;

        // Add a cursor to be able to change rope length:
        cursor = rope.gameObject.AddComponent<ObiRopeCursor>();
        cursor.cursorMu = 0;
        cursor.direction = true;
    }

    private void OnDestroy()
    {
        DestroyImmediate(blueprint);
    }
     
    private void LaunchHook()
    {
        if (predictionHit.point == Vector3.zero) return;
        
        shouldPredict = false;

        // We actually hit something, so attach the hook!
        StartCoroutine(AttachHook());
    }

    private IEnumerator AttachHook()
    {
        yield return null;

        // Clear pin constraints:
        var pinConstraints = rope.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
        pinConstraints.Clear();
        
        Vector3 localHit = rope.transform.InverseTransformPoint(predictionHit.point);

        // Procedurally generate the rope path (a simple straight line):
        int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything,0);
        blueprint.path.Clear();
        blueprint.path.AddControlPoint(Vector3.zero, -localHit.normalized, localHit.normalized, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook start");
        blueprint.path.AddControlPoint(localHit, -localHit.normalized, localHit.normalized, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook end");
        blueprint.path.FlushEvents();

        // Generate the particle representation of the rope (wait until it has finished):
        yield return blueprint.Generate();
        
        // Set the blueprint (this adds particles/constraints to the solver and starts simulating them).
        rope.ropeBlueprint = blueprint;
        
        // wait one frame:
        yield return null;
        
        rope.GetComponent<MeshRenderer>().enabled = true;
        
        // set masses to zero, as we're going to override positions while we extend the rope:
        for (int i = 0; i < rope.activeParticleCount; ++i)
            solver.invMasses[rope.solverIndices[i]] = 0;

        float currentLength = 0;

        // while the last particle hasn't reached the hit, extend the rope:
        while (true)
        {
            // calculate rope origin in solver space:
            Vector3 origin = solver.transform.InverseTransformPoint(rope.transform.position);

            // update direction and distance to hook point:
            Vector3 direction = predictionHit.point - origin;
            float distance = direction.magnitude;
            direction.Normalize();

            // increase length:
            currentLength += hookShootSpeed * Time.deltaTime;

            // if we have reached the desired length, break the loop:
            if (currentLength >= distance)
            {
                cursor.ChangeLength(distance);
                break;
            }

            // change rope length (clamp to distance between rope origin and hook to avoid overshoot)
            cursor.ChangeLength(Mathf.Min(distance, currentLength));

            // iterate over all particles in sequential order, placing them in a straight line while
            // respecting element length:
            float length = 0;
            for (int i = 0; i < rope.elements.Count; ++i)
            {
                solver.positions[rope.elements[i].particle1] = origin + direction * length;
                solver.positions[rope.elements[i].particle2] = origin + direction * (length + rope.elements[i].restLength);
                length += rope.elements[i].restLength;
            }

            // wait one frame:
            yield return null;
        }

        // restore masses so that the simulation takes over now that the rope is in place:
        for (int i = 0; i < rope.activeParticleCount; ++i)
            solver.invMasses[rope.solverIndices[i]] = 10; // 1/0.1 = 10
        
        // Pin both ends of the rope (this enables two-way interaction between character and rope):
        var batch = new ObiPinConstraintsBatch();
        batch.AddConstraint(rope.solverIndices[0], playerCollider, transform.localPosition, Quaternion.identity, 0, 0, float.PositiveInfinity);
        batch.AddConstraint(rope.solverIndices[blueprint.activeParticleCount - 1], predictionHit.collider.GetComponent<ObiColliderBase>(),
            predictionHit.collider.transform.InverseTransformPoint(predictionHit.point), Quaternion.identity, 0, 0, float.PositiveInfinity);
        batch.activeConstraintCount = 2;
        pinConstraints.AddBatch(batch);

        rope.SetConstraintsDirty(Oni.ConstraintType.Pin);
    }

    private void DetachHook()
    {
        shouldPredict = true;
        // Set the rope blueprint to null (automatically removes the previous blueprint from the solver, if any).
        rope.ropeBlueprint = null;
        rope.GetComponent<MeshRenderer>().enabled = false;
    }
    
    
    private void Update()
    {
        CheckForSwingPoints();
        var heading = swingPoint  - gunTip.position;

        if (Input.GetMouseButtonDown(0))
        {
            LaunchHook();
        }
        else if (Input.GetMouseButtonUp(0))
        {
            DetachHook();
        }
        
        if (rope.isLoaded)
        {
            if (Input.GetKey(KeyCode.LeftControl))
            {
                cursor.ChangeLength(rope.restLength - hookExtendRetractSpeed * Time.deltaTime);
            }
            if (Input.GetKey(KeyCode.Space))
            {
                cursor.ChangeLength(rope.restLength + hookExtendRetractSpeed * Time.deltaTime);
            }
        }
    }

    private void CheckForSwingPoints()
    {
        if (!shouldPredict) { /* Do Nothing */ }
            else
            {
                RaycastHit sphereCastHit;
                Physics.SphereCast(cam.position, swingRadius, cam.forward, out sphereCastHit, maxSwingDistance, whatIsGrappleable);

                RaycastHit raycastHit;
                Physics.Raycast(cam.position, cam.forward, out raycastHit, maxSwingDistance, whatIsGrappleable);

                Vector3 realHitPoint;

                // Option 1 - Direct Hit
                if (raycastHit.point != Vector3.zero)
                    realHitPoint = raycastHit.point;

                // Option 2 - Indirect (predicted) Hit
                else if (sphereCastHit.point != Vector3.zero)
                    realHitPoint = sphereCastHit.point;

                // Option 3 - Miss
                else
                    realHitPoint = Vector3.zero;

                // realHitPoint found
                if (realHitPoint != Vector3.zero)
                {
                    predictionPoint.gameObject.SetActive(true);
                    predictionPoint.position = realHitPoint;
                }
                // realHitPoint not found
                else
                {
                    predictionPoint.gameObject.SetActive(false);
                }

                predictionHit = raycastHit.point == Vector3.zero ? sphereCastHit : raycastHit;
            }
    }
    
    
}