/*
 * SPDX-FileCopyrightText: 2026 KOKA99CAB
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

package net.koka99cab.portals.block;

import com.mojang.serialization.MapCodec;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import net.koka99cab.portals.portal.PortalFrame;
import net.koka99cab.portals.portal.PortalTeleporter;
import net.koka99cab.portals.world.ModDimensions;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.InsideBlockEffectApplier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.ScheduledTickAccess;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Portal;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.portal.TeleportTransition;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;

public class OrePortalBlock extends Block implements Portal {
	public static final MapCodec<OrePortalBlock> CODEC = simpleCodec(OrePortalBlock::new);
	public static final EnumProperty<Direction.Axis> AXIS = BlockStateProperties.HORIZONTAL_AXIS;
	private static final Map<Direction.Axis, VoxelShape> SHAPES = Shapes.rotateHorizontalAxis(Block.column(4.0, 16.0, 0.0, 16.0));
	private final ResourceKey<Level> destination;
	private final Block frameBlock;
	private final Predicate<BlockState> framePredicate;
	private final ParticleOptions particle;

	public OrePortalBlock(BlockBehaviour.Properties properties) {
		this(properties, ModDimensions.ORE_CAVES, Blocks.STONE, ParticleTypes.END_ROD);
	}

	public OrePortalBlock(BlockBehaviour.Properties properties, ResourceKey<Level> destination, Block frameBlock, ParticleOptions particle) {
		this(properties, destination, frameBlock, state -> state.is(frameBlock), particle);
	}

	public OrePortalBlock(BlockBehaviour.Properties properties, ResourceKey<Level> destination, Block frameBlock, Predicate<BlockState> framePredicate, ParticleOptions particle) {
		super(properties);
		this.destination = destination;
		this.frameBlock = frameBlock;
		this.framePredicate = framePredicate;
		this.particle = particle;
		registerDefaultState(stateDefinition.any().setValue(AXIS, Direction.Axis.X));
	}

	@Override
	protected MapCodec<? extends Block> codec() {
		return CODEC;
	}

	@Override
	protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
		return SHAPES.get(state.getValue(AXIS));
	}

	@Override
	protected BlockState updateShape(BlockState state, LevelReader level, ScheduledTickAccess ticks, BlockPos pos, Direction directionToNeighbour, BlockPos neighbourPos, BlockState neighbourState, RandomSource random) {
		Direction.Axis axis = state.getValue(AXIS);

		if (PortalFrame.findExisting(level, pos, axis, framePredicate, this).isPresent()) {
			return super.updateShape(state, level, ticks, pos, directionToNeighbour, neighbourPos, neighbourState, random);
		}

		return Blocks.AIR.defaultBlockState();
	}

	@Override
	protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity, InsideBlockEffectApplier effectApplier, boolean isPrecise) {
		if (entity.canUsePortal(false)) {
			entity.setAsInsidePortal(this, pos);
		}
	}

	@Override
	public int getPortalTransitionTime(ServerLevel level, Entity entity) {
		if (entity instanceof Player player) {
			return Math.max(0, level.getGameRules().get(player.getAbilities().invulnerable ? GameRules.PLAYERS_NETHER_PORTAL_CREATIVE_DELAY : GameRules.PLAYERS_NETHER_PORTAL_DEFAULT_DELAY));
		}

		return 0;
	}

	@Override
	public TeleportTransition getPortalDestination(ServerLevel currentLevel, Entity entity, BlockPos portalEntryPos) {
		ServerLevel targetLevel = currentLevel.getServer().getLevel(currentLevel.dimension().equals(destination) ? Level.OVERWORLD : destination);

		if (targetLevel == null) {
			return null;
		}

		Direction.Axis axis = currentLevel.getBlockState(portalEntryPos).getOptionalValue(AXIS).orElse(Direction.Axis.X);
		Optional<Vec3> exit = PortalTeleporter.findOrCreateExit(targetLevel, entity, portalEntryPos, axis, frameBlock, framePredicate, this);

		if (exit.isEmpty()) {
			return null;
		}

		return new TeleportTransition(
				targetLevel,
				exit.get(),
				Vec3.ZERO,
				entity.getYRot(),
				entity.getXRot(),
				TeleportTransition.PLAY_PORTAL_SOUND
		);
	}

	@Override
	public Transition getLocalTransition() {
		return Transition.CONFUSION;
	}

	@Override
	public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) {
		if (random.nextInt(100) == 0) {
			level.playLocalSound(pos, SoundEvents.PORTAL_AMBIENT, SoundSource.BLOCKS, 0.35F, random.nextFloat() * 0.4F + 0.8F, false);
		}

		for (int i = 0; i < 4; i++) {
			double x = pos.getX() + random.nextDouble();
			double y = pos.getY() + random.nextDouble();
			double z = pos.getZ() + random.nextDouble();
			double velocityX = (random.nextFloat() - 0.5D) * 0.5D;
			double velocityY = (random.nextFloat() - 0.5D) * 0.5D;
			double velocityZ = (random.nextFloat() - 0.5D) * 0.5D;

			level.addParticle(particle, x, y, z, velocityX * 0.25D, velocityY * 0.25D, velocityZ * 0.25D);
		}
	}

	@Override
	protected ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state, boolean includeData) {
		return ItemStack.EMPTY;
	}

	@Override
	protected BlockState rotate(BlockState state, Rotation rotation) {
		return switch (rotation) {
			case CLOCKWISE_90, COUNTERCLOCKWISE_90 -> switch (state.getValue(AXIS)) {
				case X -> state.setValue(AXIS, Direction.Axis.Z);
				case Z -> state.setValue(AXIS, Direction.Axis.X);
				default -> state;
			};
			default -> state;
		};
	}

	@Override
	protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
		builder.add(AXIS);
	}
}
