/*
 * Anim Core mod
 *  Copyright (C)  2026  LeisureTimeDock
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU 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 General Public License for more details.
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package top.leisuretimedock.animationcore.animation;

import io.zershyan.sccore.animation.data.AnimationData;
import io.zershyan.sccore.animation.data.GenericAnimationData;
import io.zershyan.sccore.animation.data.RawAnimationData;
import io.zershyan.sccore.animation.data.Ride;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;

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

/**
 * Builder for creating {@link AnimationData} instances.
 * Supports building both {@link GenericAnimationData} and {@link RawAnimationData}.
 */
public class AnimationDataBuilder {
    private final ResourceLocation key;
    private boolean isGeneric = false;
    private float camYaw = 0.0f;
    private float camPitch = 0.0f;
    private float camRoll = 0.0f;
    private int camComputePriority = 0;
    private Vec3 camPosOffset = new Vec3(0.0F, 0.0F, 0.0F);
    private boolean camPosOffsetRelative = false;
    private @Nullable GenericAnimationData.LyingType lyingType = null;
    private @Nullable Ride ride = null;
    private @Nullable String genericName = null;
    private float heightModifier = 1.0f;

    private final List<Consumer<AnimationData>> postBuildActions = new ArrayList<>();

    /**
     * Private constructor. Use {@link #create(ResourceLocation)} to create a builder.
     */
    private AnimationDataBuilder(ResourceLocation key) {
        this.key = key;
    }

    /**
     * Creates a new builder for the given animation key.
     *
     * @param key The resource location identifying the animation
     * @return A new builder instance
     */
    public static AnimationDataBuilder create(ResourceLocation key) {
        return new AnimationDataBuilder(key);
    }

    /**
     * Creates a new builder for the given animation key from a string.
     *
     * @param key The string representation of the resource location
     * @return A new builder instance
     */
    public static AnimationDataBuilder create(String key) {
        return new AnimationDataBuilder(new ResourceLocation(key));
    }

    /**
     * Configures this builder to create a {@link GenericAnimationData} instead of a raw one.
     *
     * @return This builder for chaining
     */
    public AnimationDataBuilder asGeneric() {
        this.isGeneric = true;
        return this;
    }

    /**
     * Sets the name for generic animation data.
     *
     * @param name The animation name (must match regex: ^[a-zA-Z0-9_-]+$)
     * @return This builder for chaining
     * @throws IllegalArgumentException if the name doesn't match the required pattern
     */
    public AnimationDataBuilder withName(String name) {
        this.genericName = name;
        this.isGeneric = true; // Implicitly set as generic when name is provided
        return this;
    }

    /**
     * Sets the height modifier for generic animation data.
     *
     * @param heightModifier The height modifier value
     * @return This builder for chaining
     */
    public AnimationDataBuilder withHeightModifier(float heightModifier) {
        this.heightModifier = heightModifier;
        this.isGeneric = true; // Implicitly set as generic when height modifier is provided
        return this;
    }

    /**
     * Sets the lying type for the animation.
     * When set, automatically configures camera position and rotation for lying animations.
     *
     * @param lyingType The lying type
     * @return This builder for chaining
     */
    public AnimationDataBuilder withLyingType(@Nullable GenericAnimationData.LyingType lyingType) {
        this.lyingType = lyingType;
        if (lyingType != null) {
            this.camPosOffset = new Vec3(0, -1.3f, 0);
            this.camPitch = -90.0f;

            switch (lyingType) {
                case RIGHT -> {
                    this.camRoll = 90.0f;
                    this.camYaw = 90.0f;
                }
                case LEFT -> {
                    this.camRoll = -90.0f;
                    this.camYaw = -90.0f;
                }
                case BACK -> this.camPitch = 90.0f;
            }

            // For generic animations, set height modifier when lying
            if (this.isGeneric) {
                this.heightModifier = 0.3f;
            }
        }
        return this;
    }

    /**
     * Sets the ride configuration for the animation.
     *
     * @param ride The ride configuration
     * @return This builder for chaining
     */
    public AnimationDataBuilder withRide(Ride ride) {
        this.ride = ride;
        return this;
    }

    /**
     * Sets the ride configuration using a ride builder.
     *
     * @param rideBuilderConsumer Consumer that configures a {@link RideBuilder}
     * @return This builder for chaining
     */
    public AnimationDataBuilder withRide(Consumer<RideBuilder> rideBuilderConsumer) {
        RideBuilder rideBuilder = RideBuilder.create();
        rideBuilderConsumer.accept(rideBuilder);
        this.ride = rideBuilder.build();
        return this;
    }

    /**
     * Sets the camera yaw rotation.
     *
     * @param camYaw The camera yaw in degrees
     * @return This builder for chaining
     */
    public AnimationDataBuilder withCamYaw(float camYaw) {
        this.camYaw = camYaw;
        return this;
    }

    /**
     * Sets the camera pitch rotation.
     *
     * @param camPitch The camera pitch in degrees
     * @return This builder for chaining
     */
    public AnimationDataBuilder withCamPitch(float camPitch) {
        this.camPitch = camPitch;
        return this;
    }

    /**
     * Sets the camera roll rotation.
     *
     * @param camRoll The camera roll in degrees
     * @return This builder for chaining
     */
    public AnimationDataBuilder withCamRoll(float camRoll) {
        this.camRoll = camRoll;
        return this;
    }

    /**
     * Sets the camera computation priority.
     *
     * @param camComputePriority The priority value
     * @return This builder for chaining
     */
    public AnimationDataBuilder withCamComputePriority(int camComputePriority) {
        this.camComputePriority = camComputePriority;
        return this;
    }

    /**
     * Sets the camera position offset.
     *
     * @param camPosOffset The position offset vector
     * @return This builder for chaining
     */
    public AnimationDataBuilder withCamPosOffset(Vec3 camPosOffset) {
        this.camPosOffset = camPosOffset;
        return this;
    }

    /**
     * Adds to the camera position offset.
     *
     * @param camPosOffset The position offset to add
     * @return This builder for chaining
     */
    public AnimationDataBuilder addCamPosOffset(Vec3 camPosOffset) {
        this.camPosOffset = this.camPosOffset.add(camPosOffset);
        return this;
    }

    /**
     * Sets whether the camera position offset is relative.
     *
     * @param camPosOffsetRelative True if offset is relative, false if absolute
     * @return This builder for chaining
     */
    public AnimationDataBuilder withCamPosOffsetRelative(boolean camPosOffsetRelative) {
        this.camPosOffsetRelative = camPosOffsetRelative;
        return this;
    }

    /**
     * Adds a post-build action that will be executed after the animation data is created.
     * Useful for additional customizations.
     *
     * @param action The action to perform on the created animation data
     * @return This builder for chaining
     */
    public AnimationDataBuilder withPostBuildAction(Consumer<AnimationData> action) {
        this.postBuildActions.add(action);
        return this;
    }

    /**
     * Builds the animation data.
     *
     * @return The constructed {@link AnimationData}
     */
    public AnimationData build() {
        AnimationData animationData;

        if (isGeneric) {
            animationData = GenericAnimationData.create(key)
                    .withHeightModifier(heightModifier)
                    .withName(genericName);
        } else {
            animationData = RawAnimationData.create(key);
        }

        // Apply common properties
        animationData
                .withCamYaw(camYaw)
                .withCamPitch(camPitch)
                .withCamRoll(camRoll)
                .withCamComputePriority(camComputePriority)
                .setCamPosOffset(camPosOffset)
                .withCamPosOffsetRelative(camPosOffsetRelative)
                .withRide(ride)
                .setLyingType(lyingType);
        // Apply post-build actions
        for (Consumer<AnimationData> action : postBuildActions) {
            action.accept(animationData);
        }

        return animationData;
    }

    /**
     * Builds the animation data and casts it to the specified type.
     * Useful when you know the exact type you want.
     *
     * @param <T> The expected animation data type
     * @param type The class of the expected type
     * @return The constructed animation data cast to the specified type
     * @throws ClassCastException if the built animation data is not of the expected type
     */
    @SuppressWarnings("unchecked")
    public <T extends AnimationData> T buildAs(Class<T> type) {
        AnimationData data = build();
        if (!type.isInstance(data)) {
            throw new ClassCastException("Cannot cast " + data.getClass().getSimpleName() +
                    " to " + type.getSimpleName());
        }
        return (T) data;
    }

    /**
     * Builds a {@link GenericAnimationData}.
     * This method ensures the builder is configured for generic animations.
     *
     * @return The constructed {@link GenericAnimationData}
     */
    public GenericAnimationData buildGeneric() {
        this.isGeneric = true;
        return buildAs(GenericAnimationData.class);
    }

    /**
     * Builds a {@link RawAnimationData}.
     * This method ensures the builder is configured for raw animations.
     *
     * @return The constructed {@link RawAnimationData}
     */
    public RawAnimationData buildRaw() {
        this.isGeneric = false;
        return buildAs(RawAnimationData.class);
    }
}
/**
 * Builder for creating {@link Ride} instances.
 */
class RideBuilder {
    private Vec3 offset = new Vec3(0.0F, 0.0F, 0.0F);
    private int existTick = 0;
    private float xRot = 0.0f;
    private float yRot = 0.0f;
    private List<ResourceLocation> componentAnimations = new ArrayList<>();

    private RideBuilder() {}

    /**
     * Creates a new ride builder.
     *
     * @return A new ride builder instance
     */
    public static RideBuilder create() {
        return new RideBuilder();
    }

    /**
     * Sets the offset for the ride.
     *
     * @param offset The offset vector
     * @return This builder for chaining
     */
    public RideBuilder withOffset(Vec3 offset) {
        this.offset = offset;
        return this;
    }

    /**
     * Sets the offset for the ride using coordinates.
     *
     * @param x X coordinate
     * @param y Y coordinate
     * @param z Z coordinate
     * @return This builder for chaining
     */
    public RideBuilder withOffset(double x, double y, double z) {
        this.offset = new Vec3(x, y, z);
        return this;
    }

    /**
     * Sets the existence tick count for the ride.
     *
     * @param existTick The number of ticks the ride exists
     * @return This builder for chaining
     */
    public RideBuilder withExistTick(int existTick) {
        this.existTick = existTick;
        return this;
    }

    /**
     * Sets the X rotation for the ride.
     *
     * @param xRot The X rotation in degrees
     * @return This builder for chaining
     */
    public RideBuilder withXRot(float xRot) {
        this.xRot = xRot;
        return this;
    }

    /**
     * Sets the Y rotation for the ride.
     *
     * @param yRot The Y rotation in degrees
     * @return This builder for chaining
     */
    public RideBuilder withYRot(float yRot) {
        this.yRot = yRot;
        return this;
    }

    /**
     * Adds a component animation to the ride.
     *
     * @param animation The resource location of the component animation
     * @return This builder for chaining
     */
    public RideBuilder addComponentAnimation(ResourceLocation animation) {
        this.componentAnimations.add(animation);
        return this;
    }

    /**
     * Adds a component animation to the ride from a string.
     *
     * @param animation The string representation of the resource location
     * @return This builder for chaining
     */
    public RideBuilder addComponentAnimation(String animation) {
        this.componentAnimations.add(new ResourceLocation(animation));
        return this;
    }

    /**
     * Sets all component animations for the ride.
     *
     * @param componentAnimations List of component animation resource locations
     * @return This builder for chaining
     */
    public RideBuilder setComponentAnimations(List<ResourceLocation> componentAnimations) {
        this.componentAnimations = new ArrayList<>(componentAnimations);
        return this;
    }

    /**
     * Builds the ride configuration.
     *
     * @return The constructed {@link Ride}
     */
    public Ride build() {
        return Ride.create()
                .withOffset(offset)
                .withExistTick(existTick)
                .withXRot(xRot)
                .withYRot(yRot)
                .setComponentAnimations(componentAnimations);
    }
}
