@Entity
@Table(name = "games")
public class Game implements Serializable {
  private static Logger log = Utils.getLogger();

  private static TournamentProperties properties = TournamentProperties.getProperties();

  private Integer gameId = 0;
  private String gameName;
  private Round round;
  private Machine machine = null;
  private GameState state = GameState.boot_pending;
  private Date startTime;
  private Date readyTime;
  private String serverQueue = "";
  private String visualizerQueue = "";
  private String location = "";
  private String simStartTime;
  private Integer gameLength = 0;
  private Integer lastTick = 0;
  private Integer urgency;

  private Map<Integer, Agent> agentMap = new HashMap<>();

  private Random random = new Random();

  public Game() {}

  public void delete(Session session) {
    // Delete all agent belonging to this broker
    for (Agent agent : agentMap.values()) {
      session.delete(agent);
      session.flush();
    }
    session.delete(this);
  }

  @Transient
  public String getBrokersInGameString() {
    StringBuilder result = new StringBuilder();
    String prefix = "";

    for (Agent agent : agentMap.values()) {
      result.append(prefix).append(agent.getBroker().getBrokerName());
      prefix = ", ";
    }

    return result.toString();
  }

  @Transient
  @SuppressWarnings("unchecked")
  public String getBrokerIdsInGameString() {
    List<Agent> agents = new ArrayList(agentMap.values());
    Collections.sort(agents, new Utils.agentIdComparator());

    StringBuilder result = new StringBuilder();
    String prefix = "";
    for (Agent agent : agents) {
      result.append(prefix).append(agent.getBroker().getBrokerId());
      prefix = ", ";
    }

    return result.toString();
  }

  public void removeBootFile() {
    String bootLocation = properties.getProperty("bootLocation") + gameId + "-boot.xml";
    File f = new File(bootLocation);

    if (!f.exists()) {
      return;
    }

    if (!f.canWrite()) {
      log.error("Write protected: " + bootLocation);
    }

    if (!f.delete()) {
      log.error("Failed to delete : " + bootLocation);
    }
  }

  public String startTimeUTC() {
    return Utils.dateToStringFull(startTime);
  }

  public String readyTimeUTC() {
    return Utils.dateToStringFull(readyTime);
  }

  @Transient
  public int getSize() {
    return agentMap.size();
  }

  // Computes a random game length as outlined in the game specification
  public int computeGameLength() {
    String p = round.getRoundName().toLowerCase().contains("test") ? "test" : "competition";
    int minLength = properties.getPropertyInt(p + ".minimumTimeslotCount");
    int expLength = properties.getPropertyInt(p + ".expectedTimeslotCount");

    if (expLength == minLength) {
      return minLength;
    } else {
      double roll = random.nextDouble();
      // compute k = ln(1-roll)/ln(1-p) where p = 1/(exp-min)
      double k = (Math.log(1.0 - roll) / Math.log(1.0 - 1.0 / (expLength - minLength + 1)));
      return minLength + (int) Math.floor(k);
    }
  }

  public String jenkinsMachineUrl() {
    if (machine == null) {
      return "";
    }

    return String.format(
        "%scomputer/%s/", properties.getProperty("jenkins.location"), machine.getMachineName());
  }

  public void saveStandings() {
    try {
      int bootLength = properties.getPropertyInt("bootLength");
      gameLength = MemStore.getGameLengths().get(gameId) - bootLength;
      lastTick = Integer.parseInt(MemStore.getGameHeartbeats().get(gameId)[0]) - bootLength;
    } catch (Exception ignored) {
    }
  }

  private static String randomLocation(Round round) {
    double randLocation = Math.random() * round.getLocationsList().size();
    return round.getLocationsList().get((int) Math.floor(randLocation));
  }

  private static String randomSimStartTime(String locationString) {
    Location location = Location.getLocationByName(locationString);
    long dateTo = location.getDateTo().getTime();
    long dateFrom = location.getDateFrom().getTime();

    // Number of msecs in a year divided by 4
    double gameLength = (3.1556926 * Math.pow(10, 10)) / 4;

    // Max amount of time between the fromTime to the toTime to start a game
    long msLength = (long) gameLength;

    Date starting = new Date();
    if ((dateTo - dateFrom) < msLength) {
      // Use fromTime in all games in the round as the start time
      starting.setTime(dateFrom);
    } else {
      long end = dateTo - msLength;
      long startTime = (long) (Math.random() * (end - dateFrom) + dateFrom);
      starting.setTime(startTime);
    }

    return Utils.dateToStringSmall(starting);
  }

  public static Game createGame(Round round, int counter) {
    String gameName =
        String.format("%s_%d", round.getLevel().getTournament().getTournamentName(), counter);

    Game game = new Game();
    game.setGameName(gameName);
    game.setRound(round);
    game.setState(GameState.boot_pending);
    game.setStartTime(round.getStartTime());
    game.setLocation(randomLocation(round));
    game.setSimStartTime(randomSimStartTime(game.getLocation()));
    game.setServerQueue(Utils.createQueueName());
    game.setVisualizerQueue(Utils.createQueueName());

    return game;
  }

  public static Game getGameFromId(int gameId) {
    Game game = null;
    Session session = HibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    try {
      game = getGame(session, gameId);
      transaction.commit();
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    } finally {
      session.close();
    }

    return game;
  }

  public static Game getGame(Session session, int gameId) {
    Query query = session.createQuery(Constants.HQL.GET_GAME_BY_ID);
    query.setInteger("gameId", gameId);
    return (Game) query.uniqueResult();
  }

  // <editor-fold desc="Collections">
  @OneToMany
  @JoinColumn(name = "gameId")
  @MapKey(name = "brokerId")
  public Map<Integer, Agent> getAgentMap() {
    return agentMap;
  }

  public void setAgentMap(Map<Integer, Agent> agentMap) {
    this.agentMap = agentMap;
  }

  @SuppressWarnings("unchecked")
  public static List<Game> getNotCompleteGamesList() {
    List<Game> games = new ArrayList<Game>();

    Session session = HibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    try {
      Query query = session.createQuery(Constants.HQL.GET_GAMES_NOT_COMPLETE);
      games = (List<Game>) query.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).list();
      transaction.commit();
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    }
    session.close();

    return games;
  }

  @SuppressWarnings("unchecked")
  public static List<Game> getCompleteGamesList() {
    List<Game> games = new ArrayList<Game>();

    Session session = HibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    try {
      Query query = session.createQuery(Constants.HQL.GET_GAMES_COMPLETE);
      games = (List<Game>) query.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).list();
      transaction.commit();
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
    }
    session.close();

    return games;
  }
  // </editor-fold>

  // <editor-fold desc="Setter and getters">
  @Id
  @GeneratedValue(strategy = IDENTITY)
  @Column(name = "gameId", unique = true, nullable = false)
  public Integer getGameId() {
    return gameId;
  }

  public void setGameId(Integer gameId) {
    this.gameId = gameId;
  }

  @Column(name = "gameName")
  public String getGameName() {
    return gameName;
  }

  public void setGameName(String gameName) {
    this.gameName = gameName;
  }

  @ManyToOne
  @JoinColumn(name = "roundId")
  public Round getRound() {
    return round;
  }

  public void setRound(Round round) {
    this.round = round;
  }

  @ManyToOne
  @JoinColumn(name = "machineId")
  public Machine getMachine() {
    return machine;
  }

  public void setMachine(Machine machine) {
    this.machine = machine;
  }

  @Column(name = "state", nullable = false)
  @Enumerated(EnumType.STRING)
  public GameState getState() {
    return state;
  }

  public void setState(GameState state) {
    this.state = state;
  }

  @Temporal(TemporalType.TIMESTAMP)
  @Column(name = "startTime")
  public Date getStartTime() {
    return startTime;
  }

  public void setStartTime(Date startTime) {
    this.startTime = startTime;
  }

  @Temporal(TemporalType.TIMESTAMP)
  @Column(name = "readyTime")
  public Date getReadyTime() {
    return readyTime;
  }

  public void setReadyTime(Date readyTime) {
    this.readyTime = readyTime;
  }

  @Column(name = "visualizerQueue")
  public String getVisualizerQueue() {
    return visualizerQueue;
  }

  public void setVisualizerQueue(String name) {
    this.visualizerQueue = name;
  }

  @Column(name = "serverQueue")
  public String getServerQueue() {
    return serverQueue;
  }

  public void setServerQueue(String name) {
    this.serverQueue = name;
  }

  @Column(name = "location")
  public String getLocation() {
    return location;
  }

  public void setLocation(String location) {
    this.location = location;
  }

  @Column(name = "simStartDate")
  public String getSimStartTime() {
    return simStartTime;
  }

  public void setSimStartTime(String simStartTime) {
    this.simStartTime = simStartTime;
  }

  @Column(name = "gameLength")
  public Integer getGameLength() {
    return gameLength;
  }

  public void setGameLength(Integer gameLength) {
    this.gameLength = gameLength;
  }

  @Column(name = "lastTick")
  public Integer getLastTick() {
    return lastTick;
  }

  public void setLastTick(Integer lastTick) {
    this.lastTick = lastTick;
  }

  @Transient
  public Integer getUrgency() {
    return urgency;
  }

  public void setUrgency(int urgency) {
    this.urgency = urgency;
  }
  // </editor-fold>

  // Used by timeline
  @Override
  public String toString() {
    return gameId + " : " + getBrokerIdsInGameString();
  }
}
@WebServlet(
    description = "REST API for game servers",
    urlPatterns = {"/serverInterface.jsp"})
public class RestServer extends HttpServlet {
  private static Logger log = Utils.getLogger();

  private static String responseType = "text/plain; charset=UTF-8";

  private TournamentProperties properties = TournamentProperties.getProperties();

  public RestServer() {
    super();
  }

  protected synchronized void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    String result = handleGET(request);
    writeResult(response, result);
  }

  protected synchronized void doPut(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    String result = handlePUT(request);
    writeResult(response, result);
  }

  protected synchronized void doPost(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    String result = handlePOST(request);
    writeResult(response, result);
  }

  private void writeResult(HttpServletResponse response, String result) throws IOException {
    response.setContentType(responseType);
    response.setContentLength(result.length());

    PrintWriter out = response.getWriter();
    out.print(result);
    out.flush();
    out.close();
  }

  private String handleGET(HttpServletRequest request) {
    try {
      String actionString = request.getParameter(Rest.REQ_PARAM_ACTION);
      if (actionString.equalsIgnoreCase(Rest.REQ_PARAM_STATUS)) {
        if (!MemStore.checkMachineAllowed(request.getRemoteAddr())) {
          return "error";
        }

        return handleStatus(request);
      } else if (actionString.equalsIgnoreCase(Rest.REQ_PARAM_BOOT)) {
        return serveBoot(getGameId(request));
      } else if (actionString.equalsIgnoreCase(Rest.REQ_PARAM_HEARTBEAT)) {
        if (!MemStore.checkMachineAllowed(request.getRemoteAddr())) {
          return "error";
        }

        return handleHeartBeat(request);
      }
    } catch (Exception ignored) {
    }
    return "error";
  }

  /** Handle 'PUT' to serverInterface.jsp, either boot.xml or (Boot|Sim) log */
  private String handlePUT(HttpServletRequest request) {
    if (!MemStore.checkMachineAllowed(request.getRemoteAddr())) {
      return "error";
    }

    try {
      String fileName = request.getParameter(Rest.REQ_PARAM_FILENAME);
      log.info("Received a file " + fileName);

      String logLoc =
          fileName.endsWith("boot.xml")
              ? properties.getProperty("bootLocation")
              : properties.getProperty("logLocation");
      String pathString = logLoc + fileName;

      // Write to file
      InputStream is = request.getInputStream();
      FileOutputStream fos = new FileOutputStream(pathString);
      byte buf[] = new byte[1024];
      int letti;
      while ((letti = is.read(buf)) > 0) {
        fos.write(buf, 0, letti);
      }
      is.close();
      fos.close();

      // If sim-logs received, extract end-of-game standings
      new ParserSimlog(pathString).run();

      // Create softlinks to named versions
      String gameName = request.getParameter(Rest.REQ_PARAM_GAMENAME);
      createSoftLinks(fileName, logLoc, gameName);
    } catch (Exception e) {
      return "error";
    }
    return "success";
  }

  /** Handle 'POST' to serverInterface.jsp, this is an end-of-game message */
  private String handlePOST(HttpServletRequest request) {
    if (!MemStore.checkMachineAllowed(request.getRemoteAddr())) {
      return "error";
    }

    try {
      String actionString = request.getParameter(Rest.REQ_PARAM_ACTION);
      if (!actionString.equalsIgnoreCase(Rest.REQ_PARAM_GAMERESULTS)) {
        log.debug("The message didn't have the right action-string!");
        return "error";
      }

      int gameId = getGameId(request);
      if (!(gameId > 0)) {
        log.debug("The message didn't have a gameId!");
        return "error";
      }

      Session session = HibernateUtil.getSession();
      Transaction transaction = session.beginTransaction();
      try {
        Game game = (Game) session.get(Game.class, gameId);
        String standings = request.getParameter(Rest.REQ_PARAM_MESSAGE);
        return new GameHandler(game).handleStandings(session, standings, true);
      } catch (Exception e) {
        transaction.rollback();
        e.printStackTrace();
        return "error";
      } finally {
        session.close();
      }
    } catch (Exception e) {
      log.error("Something went wrong with receiving the POST message!");
      log.error(e.getMessage());
      return "error";
    }
  }

  private String serveBoot(int gameId) {
    StringBuilder result = new StringBuilder();

    try {
      // Determine boot-file location
      String bootLocation = properties.getProperty("bootLocation") + "game-" + gameId + "-boot.xml";

      // Read the file
      FileInputStream fstream = new FileInputStream(bootLocation);
      DataInputStream in = new DataInputStream(fstream);
      BufferedReader br = new BufferedReader(new InputStreamReader(in));
      String strLine;
      while ((strLine = br.readLine()) != null) {
        result.append(strLine).append("\n");
      }

      // Close the streams
      fstream.close();
      in.close();
      br.close();
    } catch (Exception e) {
      log.error(e.getMessage());
      return "error";
    }

    return result.toString();
  }

  private String handleStatus(HttpServletRequest request) {
    String statusString = request.getParameter(Rest.REQ_PARAM_STATUS);
    int gameId = getGameId(request);

    log.info(String.format("Received %s message from game: %s", statusString, gameId));

    Session session = HibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    try {
      Query query = session.createQuery(Constants.HQL.GET_GAME_BY_ID);
      query.setInteger("gameId", gameId);
      Game game = (Game) query.uniqueResult();

      if (game == null) {
        log.warn(
            String.format(
                "Trying to set status %s on non-existing " + "game : %s", statusString, gameId));
        return "error";
      }

      new GameHandler(game).handleStatus(session, statusString);

      transaction.commit();
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
      return "error";
    } finally {
      session.close();
    }

    String gameLength = request.getParameter(Rest.REQ_PARAM_GAMELENGTH);
    if (gameLength != null && transaction.wasCommitted()) {
      log.info(String.format("Received gamelength %s for game %s", gameLength, gameId));
      MemStore.addGameLength(gameId, gameLength);
    }

    return "success";
  }

  private String handleHeartBeat(HttpServletRequest request) {
    int gameId;

    // Write heartbeat + elapsed time to the MemStore
    try {
      String message = request.getParameter(Rest.REQ_PARAM_MESSAGE);
      gameId = getGameId(request);

      if (!(gameId > 0)) {
        log.debug("The message didn't have a gameId!");
        return "error";
      }
      MemStore.addGameHeartbeat(gameId, message);

      long elapsedTime = Long.parseLong(request.getParameter(Rest.REQ_PARAM_ELAPSED_TIME));
      if (elapsedTime > 0) {
        MemStore.addElapsedTime(gameId, elapsedTime);
      }
    } catch (Exception e) {
      e.printStackTrace();
      return "error";
    }

    // Write heartbeat to the DB
    Session session = HibernateUtil.getSession();
    Transaction transaction = session.beginTransaction();
    try {
      Game game = (Game) session.get(Game.class, gameId);
      String standings = request.getParameter(Rest.REQ_PARAM_STANDINGS);

      return new GameHandler(game).handleStandings(session, standings, false);
    } catch (Exception e) {
      transaction.rollback();
      e.printStackTrace();
      return "error";
    } finally {
      session.close();
    }
  }

  private int getGameId(HttpServletRequest request) {
    int gameId;

    try {
      gameId = Integer.parseInt(request.getParameter(Rest.REQ_PARAM_GAMEID));
    } catch (Exception ignored) {
      String niceName = request.getParameter(Rest.REQ_PARAM_GAMEID);
      gameId = MemStore.getGameId(niceName);
    }

    return gameId;
  }

  private void createSoftLinks(String fileName, String logLoc, String gameName) {
    String linkName;
    if (fileName.endsWith("boot.xml")) {
      linkName = String.format("%s%s.boot.xml", logLoc, gameName);
    } else if (fileName.contains("boot")) {
      linkName = String.format("%s%s.boot.tar.gz", logLoc, gameName);
    } else {
      linkName = String.format("%s%s.sim.tar.gz", logLoc, gameName);
    }

    try {
      Path link = Paths.get(linkName);
      Path target = Paths.get(fileName);
      Files.createSymbolicLink(link, target);
    } catch (FileAlreadyExistsException faee) {
      // Ignored
    } catch (IOException | UnsupportedOperationException e) {
      e.printStackTrace();
    }
  }
}