/*
 * This file is part of Hawk Anticheat.
 * Copyright (C) 2018 Hawk Development Team
 *
 * 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 me.islandscout.hawk.check.movement.position;

import me.islandscout.hawk.HawkPlayer;
import me.islandscout.hawk.check.MovementCheck;
import me.islandscout.hawk.event.MoveEvent;
import me.islandscout.hawk.util.*;
import me.islandscout.hawk.wrap.block.WrappedBlock;
import me.islandscout.hawk.wrap.entity.WrappedEntity;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.material.Openable;
import org.bukkit.util.Vector;

import java.util.*;

/**
 * The Phase check tests collision with blocks between players'
 * moves. Theoretically, this will effectively detect and block
 * any sort of phase, including v-clip. The bounding box of the
 * player is shrunk to reduce false positives, of course.
 */
public class Phase extends MovementCheck {

    //The way that this works is by geometry. Two AABBs represent the previous
    //and current position. They are inscribed within a new AABB. Then, planes
    //will cut through this AABB along the inscribed AABBs to form a tesseract.
    //This tesseract will then test against nearby blocks' collision boxes for
    //intersection.

    //decrease bounding box size if there are false positives.
    //(these values shrink the bounding box)
    private static final double TOP_EPSILON = 0.1;
    private static final double BOTTOM_EPSILON = 0.4;
    private static final double SIDE_EPSILON = 0.1;

    //Maximum distance per move for ignoring whitelisted blocks
    //too big, and you may have a gap for bypasses
    //too small, and you may have false positives
    //optimal threshold < (block depth + 0.6) - (2 * NMS_SIDE_EPSILON)
    private static final double HORIZONTAL_DISTANCE_THRESHOLD = Math.pow(0.4, 2);
    private static final double VERTICAL_DISTANCE_THRESHOLD = 1;

    public Phase() {
        super("phase", true, 0, 10, 0.995, 5000, "%player% failed phase. Moved through %block%. VL: %vl%", null);
    }

    @Override
    protected void check(MoveEvent event) {
        Location locTo = event.getTo();
        Location locFrom = event.getFrom();
        Player p = event.getPlayer();
        HawkPlayer pp = event.getHawkPlayer();
        if(!locFrom.getWorld().equals(locTo.getWorld()))
            return;

        //this stops an NPE
        double distanceSquared = locFrom.distanceSquared(locTo);
        if (distanceSquared == 0)
            return;

        double horizDistanceSquared = Math.pow(locTo.getX() - locFrom.getX(), 2) + Math.pow(locTo.getZ() - locFrom.getZ(), 2);
        double vertDistance = Math.abs(locTo.getY() - locFrom.getY());

        Vector moveDirection = new Vector(locTo.getX() - locFrom.getX(), locTo.getY() - locFrom.getY(), locTo.getZ() - locFrom.getZ());

        AABB playerFrom = WrappedEntity.getWrappedEntity(p).getCollisionBox(locFrom.toVector());
        playerFrom.shrink(SIDE_EPSILON, 0, SIDE_EPSILON);
        playerFrom.getMin().setY(playerFrom.getMin().getY() + BOTTOM_EPSILON);
        playerFrom.getMax().setY(playerFrom.getMax().getY() - TOP_EPSILON);
        AABB playerTo = playerFrom.clone();
        playerTo.translate(moveDirection);

        Vector minBigBox = new Vector(Math.min(playerFrom.getMin().getX(), playerTo.getMin().getX()), Math.min(playerFrom.getMin().getY(), playerTo.getMin().getY()), Math.min(playerFrom.getMin().getZ(), playerTo.getMin().getZ()));
        Vector maxBigBox = new Vector(Math.max(playerFrom.getMax().getX(), playerTo.getMax().getX()), Math.max(playerFrom.getMax().getY(), playerTo.getMax().getY()), Math.max(playerFrom.getMax().getZ(), playerTo.getMax().getZ()));
        AABB bigBox = new AABB(minBigBox, maxBigBox);

        AABB selection = bigBox.clone();
        selection.getMin().setY(selection.getMin().getY() - 0.6); //we need to grab blocks below us too, such as fences

        Set<Location> ignored = pp.getIgnoredBlockCollisions();

        GameMode gm = p.getGameMode();
        if(gm == GameMode.SURVIVAL || gm == GameMode.ADVENTURE || gm == GameMode.CREATIVE) {
            for (int x = selection.getMin().getBlockX(); x <= selection.getMax().getBlockX(); x++) {
                for (int y = selection.getMin().getBlockY(); y <= selection.getMax().getBlockY(); y++) {
                    for (int z = selection.getMin().getBlockZ(); z <= selection.getMax().getBlockZ(); z++) {

                        Location blockLoc = new Location(locTo.getWorld(), x, y, z);

                        //Skip block if it updated within player AABB (only if they move slowly)
                        if(ignored.contains(blockLoc) && horizDistanceSquared <= HORIZONTAL_DISTANCE_THRESHOLD && vertDistance <= VERTICAL_DISTANCE_THRESHOLD)
                            continue;

                        Block bukkitBlock = ServerUtils.getBlockAsync(blockLoc);

                        if (bukkitBlock == null)
                            continue;

                        WrappedBlock block = WrappedBlock.getWrappedBlock(bukkitBlock, pp.getClientVersion());
                        if (!block.isSolid())
                            continue;

                        if(bukkitBlock.getType() == Material.PISTON_MOVING_PIECE) {
                            continue;
                        }

                        if (bukkitBlock.getState().getData() instanceof Openable && horizDistanceSquared <= HORIZONTAL_DISTANCE_THRESHOLD && vertDistance <= VERTICAL_DISTANCE_THRESHOLD) {
                            continue;
                        }

                        for (AABB test : block.getCollisionBoxes()) {
                            //check if "test" box is even in "bigBox"
                            if (!test.isColliding(bigBox))
                                continue;

                            boolean xCollide = collides2d(test.getMin().getZ(), test.getMax().getZ(), test.getMin().getY(), test.getMax().getY(), playerFrom.getMin().getZ(), playerFrom.getMax().getZ(), playerFrom.getMin().getY(), playerFrom.getMax().getY(), moveDirection.getZ(), moveDirection.getY());
                            boolean yCollide = collides2d(test.getMin().getX(), test.getMax().getX(), test.getMin().getZ(), test.getMax().getZ(), playerFrom.getMin().getX(), playerFrom.getMax().getX(), playerFrom.getMin().getZ(), playerFrom.getMax().getZ(), moveDirection.getX(), moveDirection.getZ());
                            boolean zCollide = collides2d(test.getMin().getX(), test.getMax().getX(), test.getMin().getY(), test.getMax().getY(), playerFrom.getMin().getX(), playerFrom.getMax().getX(), playerFrom.getMin().getY(), playerFrom.getMax().getY(), moveDirection.getX(), moveDirection.getY());
                            if (xCollide && yCollide && zCollide) {
                                punish(pp, false, event, new Placeholder("block", bukkitBlock.getType()));
                                tryRubberband(event);
                                return;
                            }
                        }
                    }
                }
            }
        }

        reward(pp);
    }

    //2d collision test. check if hexagon collides with rectangle
    private boolean collides2d(double testMinX, double testMaxX, double testMinY, double testMaxY, double otherMinX, double otherMaxX, double otherMinY, double otherMaxY, double otherExtrudeX, double otherExtrudeY) {
        if (otherExtrudeX == 0)
            return true; //prevent division by 0
        double slope = otherExtrudeY / otherExtrudeX;
        double height;
        double height2;
        Coordinate2D lowerPoint;
        Coordinate2D upperPoint;
        if (otherExtrudeX > 0) { //extruding to the right
            height = -(slope * (otherExtrudeY > 0 ? otherMaxX : otherMinX)) + otherMinY;
            height2 = -(slope * (otherExtrudeY > 0 ? otherMinX : otherMaxX)) + otherMaxY;
            lowerPoint = new Coordinate2D((otherExtrudeY > 0 ? testMaxX : testMinX), testMinY);
            upperPoint = new Coordinate2D((otherExtrudeY > 0 ? testMinX : testMaxX), testMaxY);
        } else { //extruding to the left
            height = -(slope * (otherExtrudeY <= 0 ? otherMaxX : otherMinX)) + otherMinY;
            height2 = -(slope * (otherExtrudeY <= 0 ? otherMinX : otherMaxX)) + otherMaxY;
            lowerPoint = new Coordinate2D((otherExtrudeY <= 0 ? testMaxX : testMinX), testMinY);
            upperPoint = new Coordinate2D((otherExtrudeY <= 0 ? testMinX : testMaxX), testMaxY);
        }
        Line lowerLine = new Line(height, slope);
        Line upperLine = new Line(height2, slope);
        return (lowerPoint.getY() <= upperLine.getYatX(lowerPoint.getX()) && upperPoint.getY() >= lowerLine.getYatX(upperPoint.getX()));
    }
}