package demo.mesharena.stadium;

import demo.mesharena.common.Commons;
import demo.mesharena.common.Point;
import demo.mesharena.common.Segment;
import demo.mesharena.common.TracingContext;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.contrib.vertx.ext.web.TracingHandler;
import io.opentracing.propagation.Format.Builtin;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.WebClient;
import io.vertx.micrometer.PrometheusScrapingHandler;

import java.security.SecureRandom;
import java.util.Optional;
import java.util.Random;

import static demo.mesharena.common.Commons.*;

public class Stadium extends AbstractVerticle {

  private static final Optional<Tracer> TRACER = getTracer("stadium");
  private static final String LOCALS = Commons.getStringEnv("STADIUM_LOCALS", "Locals");
  private static final String VISITORS = Commons.getStringEnv("STADIUM_VISITORS", "Visitors");
  private static final String NAME = Commons.getStringEnv("STADIUM_NAME", "stadium");
  private static final double SCALE = Commons.getDoubleEnv("STADIUM_SCALE", 1.0);
  private static final int TX_TOP = Commons.getIntEnv("STADIUM_TOP", 50);
  private static final int TX_LEFT = Commons.getIntEnv("STADIUM_LEFT", 20);
  private static final int TOP = TX_TOP + (int)(47 * SCALE);
  private static final int LEFT = TX_LEFT + (int)(63 * SCALE);
  private static final int TX_WIDTH = (int)(490 * SCALE);
  private static final int TX_HEIGHT = (int)(700 * SCALE);
  private static final int WIDTH = (int)(363 * SCALE);
  private static final int HEIGHT = (int)(605 * SCALE);
  private static final int GOAL_SIZE = (int)(54 * SCALE);
  private static final int MATCH_TIME = 1000 * Commons.getIntEnv("STADIUM_MATCH_TIME", 60*2);
  private static final Segment GOAL_A = new Segment(new Point(LEFT + WIDTH / 2 - GOAL_SIZE / 2, TOP), new Point(LEFT + WIDTH / 2 + GOAL_SIZE / 2, TOP));
  private static final Segment GOAL_B = new Segment(new Point(LEFT + WIDTH / 2 - GOAL_SIZE / 2, TOP + HEIGHT), new Point(LEFT + WIDTH / 2 + GOAL_SIZE / 2, TOP + HEIGHT));
  private static final Segment[] ARENA_SEGMENTS = {
      new Segment(new Point(LEFT, TOP), new Point(LEFT+WIDTH, TOP)),
      new Segment(new Point(LEFT+WIDTH, TOP), new Point(LEFT+WIDTH, TOP+HEIGHT)),
      new Segment(new Point(LEFT+WIDTH, TOP+HEIGHT), new Point(LEFT, TOP+HEIGHT)),
      new Segment(new Point(LEFT, TOP+HEIGHT), new Point(LEFT, TOP))
  };

  private final Random rnd = new SecureRandom();
  private final WebClient client;
  private final JsonObject stadiumJson;
  private final JsonObject scoreJson;

  private int scoreA = 0;
  private int scoreB = 0;
  private long startTime = System.currentTimeMillis();

  private Stadium(Vertx vertx) {
    client = WebClient.create(vertx);
    stadiumJson = new JsonObject()
        .put("id", NAME + "-stadium")
        .put("style", "position: absolute; top: " + TX_TOP + "px; left: " + TX_LEFT + "px; width: " + TX_WIDTH + "px; height: " + TX_HEIGHT + "px; background-image: url(./football-ground.png); background-size: cover; color: black")
        .put("text", "");

    scoreJson = new JsonObject()
        .put("id", NAME + "-score")
        .put("style", "position: absolute; top: " + (TX_TOP + 5) + "px; left: " + (TX_LEFT + 5) + "px; color: black; font-weight: bold; z-index: 10;")
        .put("text", "");
  }

  public static void main(String[] args) {
    Vertx vertx = Commons.vertx();
    vertx.deployVerticle(new Stadium(vertx));
  }

  @Override
  public void start() {
    // Register stadium API
    HttpServerOptions serverOptions = new HttpServerOptions().setPort(Commons.STADIUM_PORT);
    Router router = Router.router(vertx);

    if (Commons.METRICS_ENABLED == 1) {
      router.route("/metrics").handler(PrometheusScrapingHandler.create());
    }

    router.get("/health").handler(ctx -> ctx.response().end());
    router.get("/centerBall").handler(this::startGame);
    router.get("/randomBall").handler(this::randomBall);
    router.post("/bounce").handler(this::bounce);
    router.get("/info").handler(this::info);
    vertx.createHttpServer().requestHandler(router)
        .listen(serverOptions.getPort(), serverOptions.getHost());

    // Ping-display
    vertx.setPeriodic(2000, loopId -> this.display());
  }

  private void startGame(RoutingContext ctx) {
    scoreA = scoreB = 0;
    startTime = System.currentTimeMillis();
    vertx.setPeriodic(1000, loopId -> {
      if (System.currentTimeMillis() - startTime >= MATCH_TIME) {
        // End of game!
        vertx.cancelTimer(loopId);
      }
      display();
    });
    resetBall(ctx);
    ctx.response().end();
  }

  private void randomBall(RoutingContext ctx) {
    JsonObject json = new JsonObject()
        .put("x", LEFT + rnd.nextInt(WIDTH))
        .put("y", TOP + rnd.nextInt(HEIGHT));

    HttpRequest<Buffer> request = client.put(BALL_PORT, BALL_HOST, "/setPosition");
    TRACER.ifPresent(tracer -> tracer.inject(TracingHandler.serverSpanContext(ctx), Builtin.HTTP_HEADERS, new TracingContext(request.headers())));
    request.sendJson(json, ar -> {
      if (!ar.succeeded()) {
        ar.cause().printStackTrace();
      }
    });
    ctx.response().end();
  }

  private void bounce(RoutingContext ctx) {
    ctx.request().bodyHandler(buf -> {
      JsonObject json = buf.toJsonObject();
      JsonObject result = bounce(ctx, new Segment(
          new Point(json.getDouble("xStart"), json.getDouble("yStart")),
          new Point(json.getDouble("xEnd"), json.getDouble("yEnd"))), -1);
      ctx.response().end(result.toString());
    });
  }

  private void resetBall(RoutingContext ctx) {
    JsonObject json = new JsonObject()
        .put("x", LEFT + WIDTH / 2)
        .put("y", TOP + HEIGHT / 2);

    HttpRequest<Buffer> request = client.put(BALL_PORT, BALL_HOST, "/setPosition");
    TRACER.ifPresent(tracer -> tracer.inject(TracingHandler.serverSpanContext(ctx), Builtin.HTTP_HEADERS, new TracingContext(request.headers())));
    request.sendJson(json, ar -> {
      if (!ar.succeeded()) {
        ar.cause().printStackTrace();
      }
    });
  }

  private JsonObject bounce(RoutingContext ctx, Segment segment, int excludeWall) {
    if (isOutside(segment.start())) {
      return new JsonObject();
    }
    Point goalA = GOAL_A.getCrossingPoint(segment);
    if (goalA != null) {
      // Team B scored!
      scoreB++;
      resetBall(ctx);
      return new JsonObject().put("scored", "visitors");
    }
    Point goalB = GOAL_B.getCrossingPoint(segment);
    if (goalB != null) {
      // Team A scored!
      scoreA++;
      resetBall(ctx);
      return new JsonObject().put("scored", "locals");
    }
    double minDistance = -1;
    Point collision = null;
    int bounceWall = -1;
    for (int i = 0; i < ARENA_SEGMENTS.length; i++) {
      if (i == excludeWall) {
        continue;
      }
      Segment wall = ARENA_SEGMENTS[i];
      Point tempCollision = wall.getCrossingPoint(segment);
      if (tempCollision != null) {
        double dist = tempCollision.diff(segment.start()).size();
        // minDistance is used to keep only the first wall encountered; if any wall was encounted first, forget this one.
        if (minDistance < 0 || dist < minDistance) {
          minDistance = dist;
          collision = tempCollision;
          bounceWall = i;
        }
      }
    }
    if (collision != null) {
      // Calculate bouncing vector and position of resulting position
      Point d = segment.end().diff(collision).normalize();
      Point p = ARENA_SEGMENTS[bounceWall].start().diff(collision).normalize();
      double dAngle = Math.acos(d.x());
      if (d.y() > 0) {
        dAngle = 2 * Math.PI - dAngle;
      }
      double pAngle = Math.acos(p.x());
      if (p.y() > 0) {
        pAngle = 2 * Math.PI - pAngle;
      }
      double dpAngle = 2 * pAngle - dAngle;
      while (dpAngle >= 2 * Math.PI) {
        dpAngle -= 2 * Math.PI;
      }
      Point result = collision.add(new Point(Math.cos(dpAngle), -Math.sin(dpAngle)).mult(segment.size() - minDistance));
      Segment resultVector = new Segment(collision, result);
      // Recursive call to check if the result vector itself is bouncing again
      JsonObject recResult = bounce(ctx, resultVector, bounceWall);
      if (recResult.isEmpty()) {
        // No bounce in recursive call => return new vector
        Point normalized = resultVector.derivate().normalize();
        return new JsonObject()
            .put("x", result.x())
            .put("y", result.y())
            .put("dx", normalized.x())
            .put("dy", normalized.y());
      }
      return recResult;
    }
    return new JsonObject();
  }

  private boolean isOutside(Point pos) {
    return pos.x() < LEFT || pos.x() > LEFT + WIDTH
        || pos.y() < TOP || pos.y() > TOP + HEIGHT;
  }

  private void info(RoutingContext ctx) {
    ctx.request().bodyHandler(buf -> {
      JsonObject input = buf.toJsonObject();
      boolean isVisitors = input.getBoolean("isVisitors");
      Point oppGoal = (isVisitors ? GOAL_A : GOAL_B).middle();
      Point ownGoal = (isVisitors ? GOAL_B : GOAL_A).middle();
      Point direction = oppGoal.diff(ownGoal).normalize();
      Segment defendZone = getDefendZone(ownGoal, direction);
      JsonObject output = new JsonObject()
        .put("goalX", oppGoal.x())
        .put("goalY", oppGoal.y())
        .put("scoreA", scoreA)
        .put("scoreB", scoreB)
        .put("defendZoneTop", defendZone.start().y())
        .put("defendZoneBottom", defendZone.end().y())
        .put("defendZoneLeft", defendZone.start().x())
        .put("defendZoneRight", defendZone.end().x());
      ctx.response().end(output.toString());
    });
  }

  private static Segment getDefendZone(Point goalMiddle, Point txUnitVec) {
    double boxHalfSize = Math.min(WIDTH, HEIGHT) / 4;
    Point topLeft = new Point(-boxHalfSize, -boxHalfSize);
    Point bottomRight = new Point(boxHalfSize, boxHalfSize);
    return new Segment(topLeft, bottomRight)
        .add(goalMiddle)
        .add(txUnitVec.mult(boxHalfSize));
  }

  private String getScoreText() {
    int elapsed = Math.min(MATCH_TIME, (int) (System.currentTimeMillis() - startTime) / 1000);
    int minutes = elapsed / 60;
    int seconds = elapsed % 60;
    String text = LOCALS + ": " + scoreA + " - " + VISITORS + ": " + scoreB + " ~~ Time: " + adjust(minutes) + ":" + adjust(seconds);
    if (elapsed == MATCH_TIME) {
      if (scoreA == scoreB) {
        return text + " ~~ Draw game!";
      }
      return text + " ~~ " + (scoreA > scoreB ? LOCALS : VISITORS) + " win!";
    }
    return text;
  }

  private static String adjust(int number) {
    return number < 10 ? "0" + number : String.valueOf(number);
  }

  private void display() {
    // Stadium
    client.post(UI_PORT, UI_HOST, "/display").sendJson(stadiumJson, ar -> {
      if (!ar.succeeded()) {
        ar.cause().printStackTrace();
      }
    });

    // Score
    scoreJson.put("text", NAME + " - " + getScoreText());
    client.post(UI_PORT, UI_HOST, "/display").sendJson(scoreJson, ar -> {
      if (!ar.succeeded()) {
        ar.cause().printStackTrace();
      }
    });
  }
}