/*
 * SoulFire
 * Copyright (C) 2026  AlexProgrammerDE
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.soulfiremc.server.pathfinding.graph.actions;

import com.soulfiremc.server.pathfinding.SFVec3i;
import com.soulfiremc.server.pathfinding.cost.Costs;
import com.soulfiremc.server.pathfinding.execution.BlockBreakAction;
import com.soulfiremc.server.pathfinding.execution.MovementAction;
import com.soulfiremc.server.pathfinding.graph.BlockFace;
import com.soulfiremc.server.pathfinding.graph.GraphInstructions;
import com.soulfiremc.server.pathfinding.graph.MinecraftGraph;
import com.soulfiremc.server.pathfinding.graph.actions.movement.ActionDirection;
import com.soulfiremc.server.pathfinding.graph.actions.movement.BlockSafetyType;
import com.soulfiremc.server.pathfinding.graph.actions.movement.MovementMiningCost;
import com.soulfiremc.server.pathfinding.graph.actions.movement.SkyDirection;
import com.soulfiremc.server.util.SFBlockHelpers;
import net.minecraft.world.level.block.state.BlockState;

import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

public final class DownMovement extends GraphAction implements Cloneable {
  private static final SFVec3i FEET_POSITION_RELATIVE_BLOCK = SFVec3i.ZERO;
  private final SFVec3i targetToMineBlock;
  // Mutable
  private MovementMiningCost breakCost;
  // Mutable
  private int closestBlockToFallOn = Integer.MIN_VALUE;
  // Mutable
  private int closestObstructingBlock = Integer.MIN_VALUE;

  private DownMovement(SubscriptionConsumer blockSubscribers) {
    super(ActionDirection.DOWN);
    this.targetToMineBlock = FEET_POSITION_RELATIVE_BLOCK.sub(0, 1, 0);

    this.registerSafetyCheckBlocks(blockSubscribers);
    this.registerObstructFallCheckBlocks(blockSubscribers);
    this.registerBlockToBreak(blockSubscribers);
    this.registerCheckSafeMineBlocks(blockSubscribers);
  }

  public static void registerDownMovements(Consumer<GraphAction> callback, SubscriptionConsumer blockSubscribers) {
    callback.accept(new DownMovement(blockSubscribers));
  }

  // These blocks are possibly safe blocks we can fall on top of
  private void registerSafetyCheckBlocks(SubscriptionConsumer blockSubscribers) {
    // Falls one block
    blockSubscribers.subscribe(FEET_POSITION_RELATIVE_BLOCK.sub(0, 2, 0), DownSafetyCheckSubscription.INSTANCE);

    // Falls two blocks
    blockSubscribers.subscribe(FEET_POSITION_RELATIVE_BLOCK.sub(0, 3, 0), DownSafetyCheckSubscription.INSTANCE);

    // Falls three blocks
    blockSubscribers.subscribe(FEET_POSITION_RELATIVE_BLOCK.sub(0, 4, 0), DownSafetyCheckSubscription.INSTANCE);
  }

  private void registerObstructFallCheckBlocks(SubscriptionConsumer blockSubscribers) {
    // Block below the block we mine can obstruct a 2 block fall
    blockSubscribers.subscribe(FEET_POSITION_RELATIVE_BLOCK.sub(0, 2, 0), ObstructingFallCheckSubscription.INSTANCE);

    // Block below the block we mine can obstruct a 3 block fall
    blockSubscribers.subscribe(FEET_POSITION_RELATIVE_BLOCK.sub(0, 3, 0), ObstructingFallCheckSubscription.INSTANCE);
  }

  private void registerBlockToBreak(SubscriptionConsumer blockSubscribers) {
    blockSubscribers.subscribe(targetToMineBlock, new MovementFreeSubscription(BlockFace.TOP));
  }

  private void registerCheckSafeMineBlocks(SubscriptionConsumer blockSubscribers) {
    for (var skyDirection : SkyDirection.VALUES) {
      blockSubscribers.subscribe(
        skyDirection.offset(targetToMineBlock),
        new MovementBreakSafetyCheckSubscription(BlockSafetyType.FLUIDS));
    }
  }

  @Override
  public List<GraphInstructions> getInstructions(MinecraftGraph graph, SFVec3i node) {
    if (closestBlockToFallOn == Integer.MIN_VALUE || closestObstructingBlock > closestBlockToFallOn) {
      return Collections.emptyList();
    }

    var cost = 0D;

    cost +=
      switch (closestBlockToFallOn) {
        case -2 -> Costs.FALL_1;
        case -3 -> Costs.FALL_2;
        case -4 -> Costs.FALL_3;
        default -> throw new IllegalStateException("Unexpected value: " + closestBlockToFallOn);
      };

    cost += breakCost.miningCost();

    var absoluteTargetFeetBlock = node.add(0, closestBlockToFallOn + 1, 0);

    return Collections.singletonList(new GraphInstructions(
      absoluteTargetFeetBlock,
      graph.pathConstraint().canBlocksDropWhenBroken() && breakCost.willDropUsableBlockItem() ? 1 : 0,
      false,
      actionDirection,
      cost,
      List.of(
        new BlockBreakAction(breakCost),
        new MovementAction(
          absoluteTargetFeetBlock,
          false,
          graph.pathConstraint()
        )
      )));
  }

  @Override
  public DownMovement copy() {
    return this.clone();
  }

  @Override
  public DownMovement clone() {
    try {
      return (DownMovement) super.clone();
    } catch (CloneNotSupportedException _) {
      throw new InternalError();
    }
  }

  private interface DownMovementSubscription extends MinecraftGraph.MovementSubscription<DownMovement> {
  }

  private record MovementFreeSubscription(BlockFace blockBreakSideHint) implements DownMovementSubscription {
    @Override
    public MinecraftGraph.SubscriptionSingleResult processBlock(MinecraftGraph graph, SFVec3i key, DownMovement downMovement,
                                                                BlockState blockState, SFVec3i absoluteKey) {
      if (!graph.pathConstraint().canBreakBlock(absoluteKey, blockState)) {
        // No way to break this block
        return MinecraftGraph.SubscriptionSingleResult.IMPOSSIBLE;
      }

      var cacheableMiningCost = graph.inventory().getMiningCosts(blockState);
      // We can mine this block, lets add costs and continue
      downMovement.breakCost = new MovementMiningCost(
        absoluteKey,
        cacheableMiningCost.miningCost(),
        cacheableMiningCost.willDropUsableBlockItem()
          && graph.pathConstraint().isPlaceableBlockDrop(blockState),
        blockBreakSideHint);
      return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
    }
  }

  private record DownSafetyCheckSubscription() implements DownMovementSubscription {
    private static final DownSafetyCheckSubscription INSTANCE = new DownSafetyCheckSubscription();

    @Override
    public MinecraftGraph.SubscriptionSingleResult processBlock(MinecraftGraph graph, SFVec3i key, DownMovement downMovement,
                                                                BlockState blockState, SFVec3i absoluteKey) {
      var yLevel = key.y;

      if (yLevel < downMovement.closestBlockToFallOn) {
        // We already found a block to fall on, above this one
        return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
      }

      if (SFBlockHelpers.isWalkableFloorBlock(blockState)) {
        // We found a block to fall on
        downMovement.closestBlockToFallOn = yLevel;
      }

      return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
    }
  }

  private record MovementBreakSafetyCheckSubscription(BlockSafetyType safetyType) implements DownMovementSubscription {
    @Override
    public MinecraftGraph.SubscriptionSingleResult processBlock(MinecraftGraph graph, SFVec3i key, DownMovement downMovement,
                                                                BlockState blockState, SFVec3i absoluteKey) {
      var unsafe = safetyType.isUnsafeBlock(blockState);

      if (unsafe) {
        // We know already WE MUST dig the block below for this action
        // So if one block around the block below is unsafe, we can't do this action
        return MinecraftGraph.SubscriptionSingleResult.IMPOSSIBLE;
      }

      // All good, we can continue
      return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
    }
  }

  private record ObstructingFallCheckSubscription() implements DownMovementSubscription {
    private static final ObstructingFallCheckSubscription INSTANCE = new ObstructingFallCheckSubscription();

    @Override
    public MinecraftGraph.SubscriptionSingleResult processBlock(MinecraftGraph graph, SFVec3i key, DownMovement downMovement,
                                                                BlockState blockState, SFVec3i absoluteKey) {
      var yLevel = key.y;

      if (yLevel < downMovement.closestObstructingBlock) {
        // We already found a higher obstructing block
        return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
      }

      if (yLevel < downMovement.closestBlockToFallOn) {
        // We only search blocks above the closest block to fall on
        return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
      }

      if (SFBlockHelpers.isBlockFree(blockState)) {
        return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
      }

      // We found a block that obstructs our fall
      downMovement.closestObstructingBlock = yLevel;
      return MinecraftGraph.SubscriptionSingleResult.CONTINUE;
    }
  }
}
