#include "court.h"

#include "draw.h"

#include <iostream>

Court::Court(const b2WorldId world_id)
  : left_start(true), ball_id(b2_nullBodyId), ball_shape_id(b2_nullShapeId), ground_id(b2_nullBodyId),
    ground_shape_id(b2_nullShapeId), world_id(world_id), player_left(b2_nullShapeId), player_right(b2_nullShapeId),
    net_id(b2_nullBodyId), left_points(0), right_points(0), allowed_touches_left(ALLOWED_TOUCHES),
    allowed_touches_right(ALLOWED_TOUCHES), ball_reset_time(std::numeric_limits<double>::max()), in_play(true) {
  std::random_device rd;
  rng = std::mt19937(rd());

  b2BodyDef ground_body_def = b2DefaultBodyDef();
  ground_body_def.position = (b2Vec2) {0.0f, -5.0f};
  b2BodyId new_ground_id = b2CreateBody(world_id, &ground_body_def);
  b2Polygon ground_box = b2MakeBox(30.0f, 5.0f);
  b2ShapeDef ground_shape_def = b2DefaultShapeDef();
  ground_shape_def.enableHitEvents = true;
  this->ground_shape_id = b2CreatePolygonShape(new_ground_id, &ground_shape_def, &ground_box);
  this->ground_id = new_ground_id;

  b2BodyDef ball_body_def = b2DefaultBodyDef();
  ball_body_def.type = b2_dynamicBody;
  ball_body_def.position = (b2Vec2) {-5.0f, 6.5f};
  b2BodyId ball_body_id = b2CreateBody(world_id, &ball_body_def);
  b2Circle ball_circle;
  ball_circle.center = (b2Vec2) {0.0f, 0.0f};
  ball_circle.radius = 0.5f;
  b2ShapeDef ball_shape_def = b2DefaultShapeDef();
  ball_shape_def.density = 1.0f;
  ball_shape_def.material.friction = 0.3f;
  ball_shape_def.material.restitution = 0.6f;
  ball_shape_def.enableHitEvents = true;
  this->ball_shape_id = b2CreateCircleShape(ball_body_id, &ball_shape_def, &ball_circle);

  ball_id = ball_body_id;

  b2Body_SetLinearVelocity(ball_body_id, (b2Vec2) {5.0f, 20.0f});

  b2BodyDef mid_border_def = b2DefaultBodyDef();
  mid_border_def.position = (b2Vec2) {0, 30.0f};
  b2BodyId mid_border_id = b2CreateBody(world_id, &mid_border_def);
  b2Polygon mid_border_box = b2MakeBox(0.5f, 30.0f);
  b2ShapeDef mid_border_shape = b2DefaultShapeDef();
  mid_border_shape.material.friction = 0.3f;
  mid_border_shape.filter.maskBits = 0x0002;
  b2CreatePolygonShape(mid_border_id, &mid_border_shape, &mid_border_box);

  b2BodyDef left_border_def = b2DefaultBodyDef();
  left_border_def.position = (b2Vec2) {-20.5f, 30.0f};
  b2BodyId left_border_id = b2CreateBody(world_id, &left_border_def);
  b2Polygon left_border_box = b2MakeBox(0.5f, 30.0f);
  b2ShapeDef left_border_shape_def = b2DefaultShapeDef();
  left_border_shape_def.material.friction = 0.3f;
  b2CreatePolygonShape(left_border_id, &left_border_shape_def, &left_border_box);

  b2BodyDef right_border_def = b2DefaultBodyDef();
  right_border_def.position = (b2Vec2) {20.5f, 30.0f};
  b2BodyId right_border_id = b2CreateBody(world_id, &right_border_def);
  b2Polygon right_border_box = b2MakeBox(0.5f, 30.0f);
  b2ShapeDef right_border_shape_def = b2DefaultShapeDef();
  right_border_shape_def.material.friction = 0.3f;
  b2CreatePolygonShape(right_border_id, &right_border_shape_def, &right_border_box);

  b2BodyDef net_def = b2DefaultBodyDef();
  net_def.position = (b2Vec2) {0.0f, 3.0f};
  net_def.type = b2_dynamicBody;
  net_def.gravityScale = 0.0f;
  b2BodyId net_id = b2CreateBody(world_id, &net_def);
  b2Polygon net_box = b2MakeBox(0.1f, 3.0f);
  b2ShapeDef net_shape_def = b2DefaultShapeDef();
  net_shape_def.material.friction = 0.3f;
  net_shape_def.material.restitution = 0.5f;
  b2CreatePolygonShape(net_id, &net_shape_def, &net_box);
  this->net_id = net_id;

  b2Vec2 pivot = {0.0f, 0.0f};
  b2RevoluteJointDef joint_def = b2DefaultRevoluteJointDef();
  joint_def.bodyIdA = new_ground_id;
  joint_def.bodyIdB = net_id;
  joint_def.localAnchorA = b2Body_GetLocalPoint(joint_def.bodyIdA, pivot);
  joint_def.localAnchorB = b2Body_GetLocalPoint(joint_def.bodyIdB, pivot);
  joint_def.enableSpring = true;
  joint_def.targetAngle = 0.0f;
  joint_def.hertz = 1.0f;
  joint_def.dampingRatio = 0.5f;
  joint_def.maxMotorTorque = 0.0f;
  joint_def.enableMotor = false;

  b2CreateRevoluteJoint(world_id, &joint_def);

  constexpr float BRIDGE_HEIGHT = 25.0f;
  b2Polygon box = b2MakeBox(0.5f, 0.125f);

  b2ShapeDef shapeDef = b2DefaultShapeDef();
  shapeDef.density = 0.5f;

  b2RevoluteJointDef jointDef = b2DefaultRevoluteJointDef();
  jointDef.enableMotor = true;
  jointDef.maxMotorTorque = 200.0f;
  jointDef.enableSpring = true;
  jointDef.hertz = 2.0f;
  jointDef.dampingRatio = 0.7f;

  b2BodyId prevBodyId = left_border_id;

  for (int i = 0; i < 40; ++i) {
    float xbase = -20.0f;
    b2BodyDef bodyDef = b2DefaultBodyDef();
    bodyDef.type = b2_dynamicBody;
    bodyDef.position = {xbase + 0.5f + 1.0f * static_cast<float>(i), BRIDGE_HEIGHT};
    bodyDef.linearDamping = 0.1f;
    bodyDef.angularDamping = 0.1f;

    b2BodyId current_body_id = b2CreateBody(world_id, &bodyDef);

    b2CreatePolygonShape(current_body_id, &shapeDef, &box);

    b2Vec2 bridge_pivot = {xbase + 1.0f * static_cast<float>(i), BRIDGE_HEIGHT};
    jointDef.bodyIdA = prevBodyId;
    jointDef.bodyIdB = current_body_id;
    jointDef.localAnchorA = b2Body_GetLocalPoint(jointDef.bodyIdA, bridge_pivot);
    jointDef.localAnchorB = b2Body_GetLocalPoint(jointDef.bodyIdB, bridge_pivot);
    b2CreateRevoluteJoint(world_id, &jointDef);

    prevBodyId = current_body_id;
    flag_ids[i] = current_body_id;
  }

  b2Vec2 final_pivot = {20.0f, BRIDGE_HEIGHT};
  jointDef.bodyIdA = prevBodyId;
  jointDef.bodyIdB = right_border_id;
  jointDef.localAnchorA = b2Body_GetLocalPoint(jointDef.bodyIdA, final_pivot);
  jointDef.localAnchorB = b2Body_GetLocalPoint(jointDef.bodyIdB, final_pivot);
  b2CreateRevoluteJoint(world_id, &jointDef);

  this->ball_texture = LoadTexture("assets/ball.png");
  this->flag_texture = LoadTexture("assets/flag.png");
  this->juice_texture = LoadTexture("assets/juice.png");
  this->net_texture = LoadTexture("assets/vert_plank.png");
  this->ground_texture = LoadTexture("assets/ground.png");
  this->background_texture = LoadTexture("assets/background.png");
  this->tower_texture = LoadTexture("assets/tower.png");
}

Court::~Court() {
  UnloadTexture(ball_texture);
  UnloadTexture(flag_texture);
  UnloadTexture(juice_texture);
  UnloadTexture(net_texture);
  UnloadTexture(ground_texture);
  UnloadTexture(background_texture);
  UnloadTexture(tower_texture);
}

// TODO: Remove this when box2d library provides a way to compare shape IDs.
bool Court::compare_shape_ids(const b2ShapeId& a, const b2ShapeId& b) {
  if (a.index1 != b.index1) {
    return false;
  }

  if (a.generation != b.generation) {
    return false;
  }

  if (a.world0 != b.world0) {
    return false;
  }

  return true;
}

void Court::update() {
  const b2ContactEvents contact_events = b2World_GetContactEvents(world_id);

  for (int i = 0; i < contact_events.hitCount; ++i) {
    const b2ContactHitEvent* const hit_event = contact_events.hitEvents + i;

    if (compare_shape_ids(hit_event->shapeIdA, ball_shape_id) ||
        compare_shape_ids(hit_event->shapeIdB, ball_shape_id)) {
      if (compare_shape_ids(hit_event->shapeIdA, ground_shape_id) ||
          compare_shape_ids(hit_event->shapeIdB, ground_shape_id)) {
        const b2Vec2 hit_pos = hit_event->point;

        if (hit_pos.x < 0.0f && in_play) {
          ++right_points;
          in_play = false;
          ball_reset_time = GetTime() + BALL_RESET_SECS;
        } else if (hit_pos.x >= 0.0 && in_play) {
          ++left_points;
          in_play = false;
          ball_reset_time = GetTime() + BALL_RESET_SECS;
        }
      }

      if (compare_shape_ids(hit_event->shapeIdA, player_left) || compare_shape_ids(hit_event->shapeIdB, player_left)) {
        --allowed_touches_left;
        allowed_touches_right = ALLOWED_TOUCHES;
      }

      if (compare_shape_ids(hit_event->shapeIdA, player_right) ||
          compare_shape_ids(hit_event->shapeIdB, player_right)) {
        --allowed_touches_right;
        allowed_touches_left = ALLOWED_TOUCHES;
      }
    }
  }

  if (allowed_touches_left < 0 && in_play) {
    ++right_points;
    ball_reset_time = GetTime() + BALL_RESET_SECS;
    in_play = false;
  }

  if (allowed_touches_right < 0 && in_play) {
    ++left_points;
    ball_reset_time = GetTime() + BALL_RESET_SECS;
    in_play = false;
  }

  if (GetTime() > ball_reset_time) {
    reset_court();
    in_play = true;
  }
}

void Court::draw() const {
  draw_background();
  draw_ground();
  draw_towers();

  draw_ball();

  for (const auto flag_id: flag_ids) {
    const b2Transform flag_transform = b2Body_GetTransform(flag_id);
    draw_flag(flag_transform, flag_texture);
  }

  draw_net();
}

void Court::draw_ui(const int screen_width, const int screen_height) const {
  constexpr int FONT_SIZE = 30;
  const std::string score_text(std::to_string(left_points) + " : " + std::to_string(right_points));
  const int text_width = MeasureText(score_text.c_str(), FONT_SIZE);
  DrawText(score_text.c_str(), screen_width / 2 - text_width / 2, 10, FONT_SIZE, WHITE);

  constexpr int RADIUS = 15;
  constexpr int SPACING = 35;
  constexpr int NUM_CIRCLES = ALLOWED_TOUCHES;

  const int vertical_start = screen_height / 2 - (NUM_CIRCLES * SPACING) / 2 + SPACING / 2;

  constexpr int left_x = RADIUS + 10;
  for (int i = 0; i < NUM_CIRCLES; ++i) {
    const int y = vertical_start + i * SPACING;
    if (i >= allowed_touches_left) {
      const Rectangle juice_rect = {left_x, static_cast<float>(y), RADIUS * 2, RADIUS * 2};

      DrawTexturePro(
        juice_texture,
        (Rectangle) {0.0f, 0.0f, static_cast<float>(juice_texture.width), static_cast<float>(juice_texture.height)},
        juice_rect, (Vector2) {RADIUS, RADIUS}, 0.0f, WHITE);
    }
  }

  const int right_x = screen_width - RADIUS - 10;
  for (int i = 0; i < NUM_CIRCLES; ++i) {
    const int y = vertical_start + i * SPACING;
    if (i >= allowed_touches_right) {
      const Rectangle juice_rect = {float(right_x), static_cast<float>(y), RADIUS * 2, RADIUS * 2};

      DrawTexturePro(
        juice_texture,
        (Rectangle) {0.0f, 0.0f, static_cast<float>(juice_texture.width), static_cast<float>(juice_texture.height)},
        juice_rect, (Vector2) {RADIUS, RADIUS}, 0.0f, WHITE);
    }
  }
}

void Court::reset_court() {
  const auto start_velocity = [&] { return b2Vec2{left_start ? -BALL_START_VELOCITY : BALL_START_VELOCITY, 0}; }();

  const float random_addition_x = std::uniform_real_distribution(-1.0f, 1.0f)(rng);
  const float random_addition_y = std::uniform_real_distribution(-3.0f, 3.0f)(rng);

  b2Body_SetTransform(ball_id, b2Vec2{random_addition_x, BALL_START_Y + random_addition_y}, b2MakeRot(0));
  b2Body_SetLinearVelocity(ball_id, start_velocity);
  b2Body_SetAngularVelocity(ball_id, 0.0f);
  b2Body_SetAwake(ball_id, true);

  allowed_touches_left = ALLOWED_TOUCHES;
  allowed_touches_right = ALLOWED_TOUCHES;

  left_start = !left_start;

  ball_reset_time = std::numeric_limits<double>::max();
}

b2BodyId Court::get_ball_id() const { return ball_id; }

void Court::set_left_shape_id(const b2ShapeId shape_id) { player_left = shape_id; }

void Court::set_right_shape_id(const b2ShapeId shape_id) { player_right = shape_id; }

void Court::draw_background() const {
  DrawTexturePro(background_texture,
                 (Rectangle) {0.0f, 0.0f, static_cast<float>(background_texture.width),
                              static_cast<float>(background_texture.height)},
                 (Rectangle) {0.0f, -15.0f * PIXELS_PER_METER, 60.0f * PIXELS_PER_METER, 30.0f * PIXELS_PER_METER},
                 (Vector2) {30.0f * PIXELS_PER_METER,
                            15.0f * PIXELS_PER_METER}, 0.0f, WHITE);
}

void Court::draw_ground() const {
  const b2Transform transform = b2Body_GetTransform(ground_id);

  DrawTexturePro(
    ground_texture,
    (Rectangle) {0.0f, 0.0f, static_cast<float>(ground_texture.width), static_cast<float>(ground_texture.height)},
    (Rectangle) {(transform.p.x) * PIXELS_PER_METER, (-transform.p.y) * PIXELS_PER_METER,
                 60.0f * PIXELS_PER_METER, 10.0f * PIXELS_PER_METER},
    (Vector2) {30.0f * PIXELS_PER_METER, 5.0f * PIXELS_PER_METER},
    -atan2f(transform.q.s, transform.q.c) * 180.0f / static_cast<float>(M_PI), WHITE);
}

void Court::draw_towers() const {
  DrawTexturePro(
    tower_texture,
    (Rectangle) {0.0f, 0.0f, static_cast<float>(tower_texture.width), static_cast<float>(tower_texture.height)},
    (Rectangle) {-24.f * PIXELS_PER_METER, -28.0f * PIXELS_PER_METER, 4.0f * PIXELS_PER_METER,
                 28.0f * PIXELS_PER_METER},
    (Vector2) {0, 0}, 0.0f, WHITE);

  DrawTexturePro(
    tower_texture,
    (Rectangle) {0.0f, 0.0f, -static_cast<float>(tower_texture.width), static_cast<float>(tower_texture.height)},
    (Rectangle) {20.f * PIXELS_PER_METER, -28.0f * PIXELS_PER_METER, 4.0f * PIXELS_PER_METER, 28.0f * PIXELS_PER_METER},
    (Vector2) {0, 0}, 0.0f, WHITE);
}

void Court::draw_ball() const {
  constexpr float RADIUS = 0.5f;
  const b2Transform transform = b2Body_GetTransform(ball_id);

  DrawTexturePro(
    ball_texture,
    (Rectangle) {0.0f, 0.0f, static_cast<float>(ball_texture.width), static_cast<float>(ball_texture.height)},
    (Rectangle) {(transform.p.x) * PIXELS_PER_METER, (-transform.p.y) * PIXELS_PER_METER,
                 RADIUS * 2.0f * PIXELS_PER_METER, RADIUS * 2.0f * PIXELS_PER_METER},
    (Vector2) {RADIUS * PIXELS_PER_METER, RADIUS * PIXELS_PER_METER},
    -atan2f(transform.q.s, transform.q.c) * 180.0f / static_cast<float>(M_PI), WHITE);
}

void Court::draw_flag(const b2Transform transform, const Texture2D& texture) {
  DrawTexturePro(texture,
                 (Rectangle) {0.0f, 0.0f, static_cast<float>(texture.width), static_cast<float>(texture.height)},
                 (Rectangle) {(transform.p.x) * PIXELS_PER_METER, (-transform.p.y + 0.75f) * PIXELS_PER_METER,
                              1.5f * PIXELS_PER_METER, 1.5f * PIXELS_PER_METER},
                 (Vector2) {0.75f * PIXELS_PER_METER, 0.75f * PIXELS_PER_METER},
                 -atan2f(transform.q.s, transform.q.c) * 180.0f / static_cast<float>(M_PI), WHITE);
}

void Court::draw_net() const {
  const b2Transform transform = b2Body_GetTransform(net_id);

  DrawTexturePro(
    net_texture,
    (Rectangle) {0.0f, 0.0f, static_cast<float>(net_texture.width), static_cast<float>(net_texture.height)},
    (Rectangle) {(transform.p.x) * PIXELS_PER_METER, (-transform.p.y) * PIXELS_PER_METER,
                 0.5f * PIXELS_PER_METER, 6.25f * PIXELS_PER_METER},
    (Vector2) {.25f * PIXELS_PER_METER, 6.25f / 2.0f * PIXELS_PER_METER},
    -atan2f(transform.q.s, transform.q.c) * 180.0f / static_cast<float>(M_PI), WHITE);
}

Vector2 Court::get_ball_position() const {
  const b2Transform transform = b2Body_GetTransform(ball_id);
  return (Vector2) {transform.p.x * PIXELS_PER_METER, -transform.p.y * PIXELS_PER_METER};
}
