package funkin.util.plugins;

import flixel.FlxG;
import flixel.FlxSprite;
import flixel.FlxCamera;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.graphics.FunkinCamera;

// TODO: Replace all the touchBuddy littered around the game's code with the ACTUAL touchBuddy.
// Thnk u agua and toffee <3

/**
 * @author moondroidcoder
 * Tracks your touch points in your game.
 */
class TouchPointerPlugin extends FlxTypedSpriteGroup<TouchPointer>
{
  /**
   * Whether the plugin is enabled.
   */
  public static var enabled(default, set):Bool = true;

  /**
   * A singleton instance of the plugin.
   */
  private static var instance:TouchPointerPlugin = null;

  /**
   * A camera dedicated to displaying the pointers.
   */
  private static var pointerCamera:FlxCamera;

  public function new()
  {
    super();
  }

  /**
   * Initializes the TouchPointerPlugin by creating a new camera and setting it up to be drawn on top of other elements.
   */
  public static function initialize():Void
  {
    pointerCamera = new FlxCamera();
    pointerCamera.bgColor.alpha = 0;
    instance = new TouchPointerPlugin();
    instance.cameras = [pointerCamera];

    FlxG.cameras.add(pointerCamera, false);
    FlxG.plugins.drawOnTop = true;
    FlxG.plugins.addPlugin(instance);

    function moveCameraToTop(camera:FlxCamera):Void
    {
      if (camera == pointerCamera && pointerCamera == null) return;

      // If there aren't any cameras then the CameraFrontEnd got reset so we have to wait for that finish (which is most of the time after state switch)
      if (FlxG.cameras.list.length == 0)
      {
        FlxG.signals.postStateSwitch.addOnce(moveCameraToTop.bind(null));
        return;
      }

      if (FlxG.cameras.list.contains(pointerCamera))
      {
        FlxG.cameras.list.remove(pointerCamera);
      }

      if (FlxG.game.contains(pointerCamera.flashSprite))
      {
        FlxG.game.removeChild(pointerCamera.flashSprite);
      }

      @:privateAccess
      FlxG.game.addChildAt(pointerCamera.flashSprite, FlxG.game.getChildIndex(FlxG.game._inputContainer));
      FlxG.cameras.list.push(pointerCamera);
    }

    FlxG.cameras.cameraAdded.add(moveCameraToTop);

    FlxG.cameras.cameraRemoved.add(function(camera:FlxCamera) {
      if (camera == pointerCamera)
      {
        if (!camera.exists) // The camera got destroyed, we make a new one!
        {
          instance.cameras = [pointerCamera = new FlxCamera()];
          moveCameraToTop(null);
          pointerCamera.bgColor.alpha = 0;
          pointerCamera.ID = FlxG.cameras.list.length - 1;
        }
        else // It's not destroyed so just move it to the top!
        {
          moveCameraToTop(null);
        }
      }
      else
      {
        moveCameraToTop(null);
      }
    });

    FlxG.signals.preStateSwitch.add(function() {
      instance.removeAll();
    });
  }

  override public function update(elapsed:Float):Void
  {
    super.update(elapsed);

    for (touch in FlxG.touches.list)
    {
      if (touch == null) continue;

      if (touch.justPressed) removeAll(true);

      var pointer:TouchPointer = findPointerByTouchId(touch.touchPointID);

      if (pointer == null)
      {
        pointer = recycle(TouchPointer);
        pointer.initialize(touch.touchPointID);
        add(pointer);
      }

      pointer.updateFromTouch(touch, pointerCamera);
    }

    for (pointer in members)
    {
      if (pointer == null || touchExists(pointer.touchId)) continue;
      if (pointer.touchId != -2)
      {
        pointer.alpha = 0.8;
        FlxTween.tween(pointer, {alpha: 0}, FlxG.random.float(0.8, 0.9),
          {
            ease: FlxEase.cubeIn,
            onComplete: function(_) {
              remove(pointer, true);
            }
          });
        pointer.touchId = -2;
      }
    }
  }

  /**
   * Finds a TouchPointer object in the members list by its touch ID.
   *
   * @param touchId The ID of the touch to find.
   * @return The TouchPointer object with the specified touch ID, or null if not found.
   */
  private function findPointerByTouchId(touchId:Int):TouchPointer
  {
    for (pointer in members)
    {
      if (pointer == null || pointer.touchId != touchId) continue;

      return pointer;
    }
    return null;
  }

  /**
   * Checks if a touch with the specified ID exists in the current touch list.
   *
   * @param touchId The ID of the touch to check for.
   * @return True if a touch with the specified ID exists, false otherwise.
   */
  private function touchExists(touchId:Int):Bool
  {
    for (touch in FlxG.touches.list)
    {
      if (touch.touchPointID != touchId) continue;

      return true;
    }
    return false;
  }

  @:noCompletion
  private static function set_enabled(value:Bool):Bool
  {
    if (instance != null)
    {
      instance.exists = instance.visible = instance.active = instance.alive = value;
    }

    return enabled = value;
  }

  public function removeAll(skipTween:Bool = false)
  {
    for (pointer in members)
    {
      if (pointer == null) continue;

      if (skipTween)
      {
        FlxTween.cancelTweensOf(pointer);
        remove(pointer, true);
        continue;
      }

      pointer.alpha = 0.8;
      FlxTween.tween(pointer, {alpha: 0}, FlxG.random.float(0.8, 1),
        {
          ease: FlxEase.quadIn,
          onComplete: function(_) {
            remove(pointer, true);
          }
        });
    }
  }
}

/**
 * Represents a touch pointer in the game.
 */
class TouchPointer extends FlxSprite
{
  /**
   * Represents a touch pointer plugin.
   */
  public var touchId:Int = -1;

  /**
   * An internal point for grabbing the view position of the camera.
   * Useful for reducing point allocation.
   */
  private var viewPoint:FlxPoint;

  /**
   * Stores the last position of the touch pointer.
   */
  private var lastPosition:FlxPoint;

  /**
   * Constructor for the TouchPointerPlugin class.
   * Initializes the touch pointer graphic and sets the scroll factor.
   */
  public function new()
  {
    super();
    makeGraphic(16, 16, FlxColor.RED);
    scrollFactor.set(0, 0);
    viewPoint = FlxPoint.get();
    lastPosition = FlxPoint.get();
  }

  /**
   * Initializes the touch pointer object itself with the specified touch ID.
   * Loads the graphic for the touch pointer.
   *
   * @param touchId The ID of the touch event to initialize.
   */
  public function initialize(touchId:Int):Void
  {
    this.touchId = touchId;
    loadGraphic("assets/images/cursor/michael.png");
  }

  /**
   * Updates the position and angle of the touch pointer based on the given touch input.
   * Used in TouchPointerPlugin's update method.
   *
   * @param touch The FlxTouch object containing the current touch input data.
   * @param camera The FlxCamera to grab the touch's view position from.
   */
  public function updateFromTouch(touch:FlxTouch, camera:FlxCamera):Void
  {
    // Grab the view coordinates
    touch.getViewPosition(camera, viewPoint);

    // Update position
    x = viewPoint.x - width / 2;
    y = viewPoint.y - height / 2;

    if (camera.target != null)
    {
      x -= camera.target.x;
      y -= camera.target.y;
    }

    // Calculate angle if moving
    if (lastPosition.distanceTo(FlxPoint.weak(viewPoint.x, viewPoint.y)) > 3)
    {
      var angle = FlxAngle.angleBetweenPoint(this, lastPosition, true);
      this.angle = angle;
      loadGraphic("assets/images/cursor/kevin.png");
    }
    else
    {
      angle = 0;
      loadGraphic("assets/images/cursor/michael.png");
    }

    lastPosition.copyFrom(viewPoint);
  }

  override public function destroy():Void
  {
    viewPoint.put();
    lastPosition.put();
    super.destroy();
  }

  override public function loadGraphic(graphic:FlxGraphicAsset, animated = false, frameWidth = 0, frameHeight = 0, unique = false, ?key:String):FlxSprite
  {
    super.loadGraphic(graphic, animated, frameWidth, frameHeight, unique, key);
    color = 0xff6666e1;
    blend = "screen";
    return this;
  }
}
