import { describe, it, beforeEach } from "mocha";
import expect from "expect";
import { Vec3 } from "vec3";
import md from "minecraft-data";
import block, { Block as PBlock } from "prismarine-block";
import { applyMdToNewEntity } from "../src/util/physicsUtils";
import { EPhysicsCtx, PhysicsWorldSettings } from "../src/physics/settings";
import { ControlStateHandler } from "../src/physics/player";
import { BotcraftPhysics, EntityPhysics, IPhysics } from "../src/physics/engines";
import { initSetup } from "../src/index";
import { PlayerState } from "../src/physics/states";
import { Bot, ControlState } from "mineflayer";
import { AABB } from "@nxg-org/mineflayer-util-plugin";

const version = "1.12.2";
const mcData = md(version);
const Engine = BotcraftPhysics;
const Block = block(version) as typeof PBlock;

const groundLevel = 67;
const floatingOffset = 100 - groundLevel;
const control: { [key: string]: boolean } = {};

class FakeWorld {
  overrideBlocks: { [key: string]: PBlock } = {};

  setOverrideBlock(pos: Vec3, type: number) {
    pos = pos.floored();
    const block = new Block(type, 0, 0);
    block.position = pos;
    this.overrideBlocks[`${pos.x},${pos.y},${pos.z}`] = block;
  }

  clearOverrides() {
    this.overrideBlocks = {};
  }

  getBlock(pos: Vec3) {
    pos = pos.floored();
    const key = `${pos.x},${pos.y},${pos.z}`;
    if (this.overrideBlocks[key]) {
      return this.overrideBlocks[key];
    }

    const type = pos.y < groundLevel ? mcData.blocksByName.stone.id : mcData.blocksByName.air.id;
    const b = new Block(type, 0, 0);
    b.position = pos;
    return b;
  }
}

function createFakePlayer(pos: Vec3, tmpGroundLevel: number = groundLevel) {
  const onGround = pos.y === tmpGroundLevel
  return {
    entity: {
      position: pos,
      velocity: new Vec3(0, onGround ? -0.08 : 0, 0),
      onGround: onGround,
      isInWater: false,
      isInLava: false,
      isInWeb: false,
      isCollidedHorizontally: false,
      isCollidedVertically: false,
      yaw: 0,
      effects: [],
      attributes: {}
    },
    jumpTicks: 0,
    jumpQueued: false,
    version: version,
    inventory: { slots: [] },
    equipment: [],
    food: 20,
    game: { gameMode: "survival" },
    registry: mcData,
    setControlState: (name: ControlState, value: boolean) => {
      control[name] = value;
    },
    getControlState: (name: ControlState) => {
      return control?.[name] ?? false;
    },
    getEquipmentDestSlot: () => {},
  };
}

initSetup(mcData);

const playerType = mcData.entitiesByName["player"];

describe("Physics Simulation Tests", () => {
  let fakePlayer: ReturnType<typeof createFakePlayer> | any;
  let physics: IPhysics;
  let playerCtx: EPhysicsCtx;
  let playerState: PlayerState;
  const fakeWorld = new FakeWorld();

  const setupEntity = (yOffset: number) => {
    fakePlayer = createFakePlayer(new Vec3(0, groundLevel + yOffset, 0), groundLevel);
    fakePlayer.entity = applyMdToNewEntity(EPhysicsCtx, playerType, fakePlayer.entity);
    physics = new Engine(mcData);
    playerCtx = EPhysicsCtx.FROM_BOT(physics, fakePlayer);
    playerState = playerCtx.state as PlayerState;
    playerState.control = ControlStateHandler.DEFAULT();
  };

  afterEach(() => {
    fakeWorld.clearOverrides();
  });

  it("should maintain position when gravity is zero", () => {
    setupEntity(floatingOffset);
    fakePlayer.entity.velocity = new Vec3(0, 0, 0);
    playerState.vel.y = 0;
    playerCtx.gravity = 0;

    for (let i = 0; i < 2; i++) {
      // playerState.update(fakePlayer);
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
    }

    expect(fakePlayer.entity.position).toEqual(new Vec3(0, groundLevel + floatingOffset, 0));
  });

  it("should move forward correctly given proper gravity", () => {
    setupEntity(0);
    playerState.control.forward = true;
    playerState.control.sprint = true;

    for (let i = 0; i < 10; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      // console.log(fakePlayer.entity.position, playerState.pos, playerState.vel, playerState.age)
    }

    if (playerState.control.forward) {
      expect(fakePlayer.entity.position).toEqual(new Vec3(0, groundLevel, -2.4694812397932626));
    } else {
      expect(fakePlayer.entity.position).toEqual(new Vec3(0, groundLevel, 0));
    }
  });

  it("sprint-jumping", () => {
    setupEntity(0);
    playerState.control.forward = true;
    playerState.control.sprint = true;

    for (let i = 0; i < 4; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      // console.log(playerState.sprinting, playerState.onGround, playerState.pos)
    }

    playerState.control.jump = true;

    for (let i = 0; i < 12; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      // console.log(playerState.sprinting, playerState.onGround, playerState.pos)
    }

    expect(fakePlayer.entity.position.y).toEqual(groundLevel);
    expect(fakePlayer.entity.position.z).toEqual(-4.085029471928113);
  });

  it("walk_fallspeed", () => {
    setupEntity(floatingOffset);
    playerState.control.forward = true;

    while (!fakePlayer.entity.onGround && playerState.age < 100) {
      physics.simulate(playerCtx, fakeWorld);
      // console.log(fakePlayer.entity.position, playerState.pos, playerState.vel, playerState.age)
      playerState.apply(fakePlayer);
    }

    expect(fakePlayer.entity.position.z).toEqual(-5.082680598494437);
    expect(fakePlayer.entity.position.y).toEqual(groundLevel);
  });

  it ("sprint_fallspeed", () => {
    setupEntity(floatingOffset);
    playerState.control.forward = true;
    playerState.control.sprint = true;

    // console.log(playerState.vel)
    while (!fakePlayer.entity.onGround && playerState.age < 100) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
    }

    // console.log(fakePlayer.entity.position, landingPos, playerState.pos, playerState.control)

    expect(fakePlayer.entity.position.z).toEqual(-6.607484778042766);
    expect(fakePlayer.entity.position.y).toEqual(groundLevel);
  })

  it("should restore position after gravity toggle", () => {
    setupEntity(floatingOffset);
    const orgGravity = playerCtx.gravity;
    fakePlayer.entity.velocity = new Vec3(0, 0, 0);
    playerState.vel.y = 0;
    playerCtx.gravity = 0;

    for (let i = 0; i < 5; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
    }
    expect(fakePlayer.entity.position).toEqual(new Vec3(0, groundLevel + floatingOffset, 0));
    playerCtx.gravity = orgGravity;

    while (!fakePlayer.entity.onGround && playerState.age < 100) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
    }

    expect(fakePlayer.entity.position.y).toEqual(groundLevel); // Verify movement in Z direction
  });

  it("should jump and fall correctly", () => {
    setupEntity(0);
    playerState.control.jump = true;

    for (let i = 0; i < 3; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
    }

    expect(fakePlayer.entity.position.y).toEqual(groundLevel + 1.001335979112147);

    for (let i = 0; i < 9; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
    }

    expect(fakePlayer.entity.position.y).toEqual(groundLevel);
  });

  it("hCol--z", () => {
    setupEntity(0);
    const blockPos = new Vec3(0, groundLevel + 1, -2);
    fakeWorld.setOverrideBlock(blockPos, mcData.blocksByName.dirt.id);
    playerState.look(0, 0);
    playerState.control.forward = true;

    for (let i = 0; i < 10; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      console.log(fakePlayer.entity.position, playerState.isCollidedHorizontally);
    }

    expect(playerState.pos.z).toEqual(-0.7);
    expect(playerState.isCollidedHorizontally).toEqual(true);
  });

  it("hCol-z", () => {
    setupEntity(0);
    const blockPos = new Vec3(0, groundLevel + 1, 1);
    fakeWorld.setOverrideBlock(blockPos, mcData.blocksByName.dirt.id);
    playerState.look(-359.9999 * (Math.PI / 360), 0);
    playerState.control.forward = true;

    for (let i = 0; i < 10; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      console.log(fakePlayer.entity.position, playerState.isCollidedHorizontally);
    }

    expect(playerState.pos.z).toEqual(0.7);
    expect(playerState.isCollidedHorizontally).toEqual(true);
  });

  it("hCol--x", () => {
    setupEntity(0);
    const blockPos = new Vec3(-2, groundLevel + 1, 0);
    fakeWorld.setOverrideBlock(blockPos, mcData.blocksByName.dirt.id);
    playerState.look(180 * (Math.PI / 360), 0)
    playerState.control.forward = true;

    for (let i = 0; i < 10; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      console.log(fakePlayer.entity.position, playerState.isCollidedHorizontally);
    }

    expect(playerState.pos.x).toEqual(-0.7);
    expect(playerState.isCollidedHorizontally).toEqual(true);
  });

  it("hCol-x", () => {
    setupEntity(0);
    const blockPos = new Vec3(1, groundLevel + 1, 0);
    fakeWorld.setOverrideBlock(blockPos, mcData.blocksByName.dirt.id);
    playerState.look(-180 * (Math.PI / 360), 0);
    playerState.control.forward = true;

    for (let i = 0; i < 10; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      console.log(fakePlayer.entity.position, playerState.isCollidedHorizontally);
    }

    expect(playerState.pos.x).toEqual(0.7);
    expect(playerState.isCollidedHorizontally).toEqual(true);
  });

  it("jumpIntoBlock", () => {
    setupEntity(0);

    const bl1 = new Vec3(0, groundLevel + 1, 1); 
    fakeWorld.setOverrideBlock(bl1, mcData.blocksByName.dirt.id);

    fakePlayer.entity.position = new Vec3(0.5, groundLevel, 0.7); // right up against a block
    playerState.pos = fakePlayer.entity.position.clone();
    playerState.look(-359.9999 * (Math.PI / 360), 0);

    playerState.control.jump = true;
    playerState.control.forward = true;
    playerState.control.sprint = true;

    for (let i = 0; i < 12; i++) {
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
      // console.log(fakePlayer.entity.position, playerState.isCollidedHorizontally);
    }

    expect(playerState.pos.z).toEqual(0.7);
    expect(playerState.isCollidedHorizontally).toEqual(true);
  });

  it("walkUpStairs", () => {
    setupEntity(0);

    const bl1 = new Vec3(0, groundLevel, -1); 
    fakeWorld.setOverrideBlock(bl1, mcData.blocksByName.stone_stairs.id);

    const shapes = fakeWorld.getBlock(bl1).shapes;
    const bbs = shapes.map((shape) => AABB.fromShape(shape, bl1));

    fakePlayer.entity.position = new Vec3(-0.3, groundLevel, -0.5); // right up against a block
    playerState.pos = fakePlayer.entity.position.clone();
    playerState.look(-180 * (Math.PI / 360), 0);

    playerState.control.forward = true;
    playerState.control.sprint = true;

    for (let i = 0; i < 4; i++) {
      // console.log(fakePlayer.entity.position, playerState.pos, playerState.isCollidedHorizontally);
      physics.simulate(playerCtx, fakeWorld);
      playerState.apply(fakePlayer);
    }

    expect(playerState.pos.y).toEqual(groundLevel + 1);
  });
});
