/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   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 de.steamwar.entity;

import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Display;
import org.bukkit.entity.Player;
import org.bukkit.util.Consumer;
import org.bukkit.util.Transformation;
import org.bukkit.util.Vector;
import org.joml.Quaternionf;
import org.joml.Vector3f;

import java.util.Objects;

/**
 * Can be used for Axis Aligned Lines of near infinite length.
 */
@Getter
public class CLine extends CEntity {

    public static final float DEFAULT_WIDTH = 1 / 16f;
    private static final float offset = 1 / 1024f;
    private static final Vector offsetVec = new Vector(offset, offset, offset);

    private Location from;
    private Location to;
    private float width = DEFAULT_WIDTH;
    private BlockData blockData = RBlockDisplay.DEFAULT_BLOCK;
    private boolean hide = false;

    public CLine(REntityServer server) {
        super(server);
    }

    private <T> CLine checkAndSet(T currentValue, T newValue, Consumer<T> setter) {
        if (Objects.equals(currentValue, newValue)) return this;
        setter.accept(newValue);
        tick();
        return this;
    }

    public CLine setFromAndTo(Location from, Location to) {
        if (Objects.equals(from, this.from) && Objects.equals(to, this.to)) return this;
        this.from = from;
        this.to = to;
        hide(true);
        tick();
        hide(false);
        return this;
    }

    public CLine setFrom(Location from) {
        return checkAndSet(this.from, from, location -> this.from = location);
    }

    public CLine setTo(Location to) {
        return checkAndSet(this.to, to, location -> this.to = location);
    }

    public CLine setWidth(float width) {
        return checkAndSet(this.width, width, w -> this.width = w);
    }

    public CLine setBlock(BlockData blockData) {
        if (this.blockData.equals(blockData)) return this;
        if (blockData == null) {
            this.blockData = RBlockDisplay.DEFAULT_BLOCK;
        } else {
            this.blockData = blockData;
        }
        getEntitiesByType(RBlockDisplay.class).forEach(display -> {
            display.setBlock(blockData);
        });
        return this;
    }

    @Override
    public void hide(boolean hide) {
        if (hide == this.hide) return;
        this.hide = hide;
        if (hide) {
            if (startLine != null) startLine.hide(true);
            if (middleLine != null) middleLine.hide(true);
            if (endLine != null) endLine.hide(true);
        } else {
            tick();
        }
    }

    @Override
    void tick() {
        if (from == null || to == null) {
            if (startLine != null) startLine.hide(true);
            if (middleLine != null) middleLine.hide(true);
            if (endLine != null) endLine.hide(true);
            return;
        } else {
            if (startLine != null) startLine.hide(false);
            if (middleLine != null) middleLine.hide(false);
            if (endLine != null) endLine.hide(false);
        }
        if (hide) return;
        updateStart();
        updateMiddle();
        updateEnd();
    }

    private RBlockDisplay startLine;

    private void updateStart() {
        Vector vec = to.clone().subtract(from).toVector();
        if (vec.length() > 35) vec.normalize().multiply(35);

        if (startLine == null) {
            startLine = new RBlockDisplay(server, new Location(null, 0, 0, 0));
            startLine.setBrightness(new Display.Brightness(15, 15));
            startLine.setViewRange(560);
            startLine.setBlock(blockData);
            entities.add(startLine);
        } else {
            startLine.hide(false);
        }

        startLine.move(from.clone().subtract(offsetVec));
        startLine.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1)));
    }

    private RBlockDisplay middleLine;

    private void updateMiddle() {
        Vector vec = to.clone().subtract(from).toVector();
        if (vec.length() <= 70) {
            if (middleLine != null) middleLine.hide(true);
            return;
        }
        if (vec.length() > 280) vec.normalize().multiply(280);
        else vec = vec.clone().normalize().multiply(vec.length() - 60);

        if (middleLine == null) {
            middleLine = new RBlockDisplay(server, new Location(null, 0, 0, 0));
            middleLine.setBrightness(new Display.Brightness(15, 15));
            middleLine.setViewRange(560);
            middleLine.setBlock(blockData);
            entities.add(middleLine);
        } else {
            middleLine.hide(false);
        }

        Player player = server.getPlayers().stream().findFirst().orElse(null);
        if (player == null) return;

        Vector tempVector = vec.clone().normalize().multiply(30);
        Location from = this.from.clone().add(tempVector);
        Location to = this.to.clone().subtract(tempVector);

        Vector lineVec = to.clone().subtract(from).toVector();
        Vector playerVec = player.getLocation().toVector().subtract(from.toVector());
        double lineVecDotItself = lineVec.dot(lineVec);
        Vector projectionVec = lineVec.clone().multiply(lineVec.dot(playerVec)).divide(new Vector(lineVecDotItself, lineVecDotItself, lineVecDotItself));

        Vector moveVec = from.toVector().add(projectionVec);
        if (moveVec.getX() < from.getX()) {
            moveVec.setX(from.getX());
        }
        if (moveVec.getX() > to.getX()) {
            moveVec.setX(to.getX());
        }
        if (moveVec.getY() < from.getY()) {
            moveVec.setY(from.getY());
        }
        if (moveVec.getY() > to.getY()) {
            moveVec.setY(to.getY());
        }
        if (moveVec.getZ() < from.getZ()) {
            moveVec.setZ(from.getZ());
        }
        if (moveVec.getZ() > to.getZ()) {
            moveVec.setZ(to.getZ());
        }

        Vector translation = vec.clone().divide(new Vector(2, 2, 2));
        translation.setX(-translation.getX());
        translation.setY(-translation.getY());
        translation.setZ(-translation.getZ());

        Vector first = moveVec.clone().add(translation).subtract(from.toVector());
        if (first.getX() < 0) {
            translation.setX(translation.getX() - first.getX());
        }
        if (first.getY() < 0) {
            translation.setY(translation.getY() - first.getY());
        }
        if (first.getZ() < 0) {
            translation.setZ(translation.getZ() - first.getZ());
        }

        Vector second = to.toVector().subtract(moveVec.clone().subtract(translation));
        if (second.getX() < 0) {
            translation.setX(translation.getX() + second.getX());
        }
        if (second.getY() < 0) {
            translation.setY(translation.getY() + second.getY());
        }
        if (second.getZ() < 0) {
            translation.setZ(translation.getZ() + second.getZ());
        }

        middleLine.move(moveVec.toLocation(player.getWorld()).subtract(offsetVec));
        middleLine.setTransform(new Transformation(translation.toVector3f(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1)));
    }

    private RBlockDisplay endLine;

    private void updateEnd() {
        Vector vec = to.clone().subtract(from).toVector();
        if (vec.length() <= 35) {
            if (endLine != null) endLine.hide(true);
            return;
        }
        if (vec.length() > 35) vec.normalize().multiply(35);

        if (endLine == null) {
            endLine = new RBlockDisplay(server, new Location(null, 0, 0, 0));
            endLine.setBrightness(new Display.Brightness(15, 15));
            endLine.setViewRange(560);
            endLine.setBlock(blockData);
            entities.add(endLine);
        } else {
            endLine.hide(false);
        }

        endLine.move(to.clone().subtract(offsetVec));
        endLine.setTransform(new Transformation(vec.toVector3f().negate(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1)));
    }

    private Vector addWidth(Vector vector) {
        vector = vector.clone();
        if (vector.getX() == 0) {
            vector.setX(vector.getX() + width);
        }
        if (vector.getY() == 0) {
            vector.setY(vector.getY() + width);
        }
        if (vector.getZ() == 0) {
            vector.setZ(vector.getZ() + width);
        }
        vector.add(offsetVec).add(offsetVec);
        return vector;
    }
}
