package com.velocitypowered.proxy.util.bossbar;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.util.bossbar.BossBarColor;
import com.velocitypowered.api.util.bossbar.BossBarFlag;
import com.velocitypowered.api.util.bossbar.BossBarOverlay;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.BossBar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import net.kyori.text.Component;
import net.kyori.text.serializer.gson.GsonComponentSerializer;

public class VelocityBossBar implements com.velocitypowered.api.util.bossbar.BossBar {

  private final List<Player> players;
  private final Set<BossBarFlag> flags;
  private final UUID uuid;
  private boolean visible;
  private Component title;
  private float percent;
  private BossBarColor color;
  private BossBarOverlay overlay;

  /**
   * Creates a new boss bar.
   * @param title the title for the bar
   * @param color the color of the bar
   * @param overlay the overlay to use
   * @param percent the percent of the bar
   */
  public VelocityBossBar(
      Component title, BossBarColor color, BossBarOverlay overlay, float percent) {
    this.title = checkNotNull(title, "title");
    this.color = checkNotNull(color, "color");
    this.overlay = checkNotNull(overlay, "overlay");
    this.percent = percent;
    checkPercent(percent);
    this.uuid = UUID.randomUUID();
    visible = true;
    players = new ArrayList<>();
    flags = EnumSet.noneOf(BossBarFlag.class);
  }

  @Override
  public void addPlayers(Iterable<Player> players) {
    checkNotNull(players, "players");
    for (Player player : players) {
      addPlayer(player);
    }
  }

  @Override
  public void addPlayer(Player player) {
    checkNotNull(player, "player");
    if (!players.contains(player)) {
      players.add(player);
    }
    if (player.isActive() && visible) {
      sendPacket(player, addPacket());
    }
  }

  @Override
  public void removePlayer(Player player) {
    checkNotNull(player, "player");
    players.remove(player);
    if (player.isActive()) {
      sendPacket(player, removePacket());
    }
  }

  @Override
  public void removePlayers(Iterable<Player> players) {
    checkNotNull(players, "players");
    for (Player player : players) {
      removePlayer(player);
    }
  }

  @Override
  public void removeAllPlayers() {
    removePlayers(ImmutableList.copyOf(players));
  }

  @Override
  public Component getTitle() {
    return title;
  }

  @Override
  public void setTitle(Component title) {
    this.title = checkNotNull(title, "title");
    if (visible) {
      BossBar bar = new BossBar();
      bar.setUuid(uuid);
      bar.setAction(BossBar.UPDATE_NAME);
      bar.setName(GsonComponentSerializer.INSTANCE.serialize(title));
      sendToAffected(bar);
    }
  }

  @Override
  public float getPercent() {
    return percent;
  }

  @Override
  public void setPercent(float percent) {
    checkPercent(percent);
    this.percent = percent;
    if (visible) {
      BossBar bar = new BossBar();
      bar.setUuid(uuid);
      bar.setAction(BossBar.UPDATE_PERCENT);
      bar.setPercent(percent);
      sendToAffected(bar);
    }
  }

  private void checkPercent(final float percent) {
    if (percent < 0f || percent > 1f) {
      throw new IllegalArgumentException("Percent must be between 0 and 1");
    }
  }

  @Override
  public Collection<Player> getPlayers() {
    return ImmutableList.copyOf(players);
  }

  @Override
  public BossBarColor getColor() {
    return color;
  }

  @Override
  public void setColor(BossBarColor color) {
    this.color = checkNotNull(color, "color");
    if (visible) {
      sendDivisions(color, overlay);
    }
  }

  @Override
  public BossBarOverlay getOverlay() {
    return overlay;
  }

  @Override
  public void setOverlay(BossBarOverlay overlay) {
    this.overlay = checkNotNull(overlay, "overlay");
    if (visible) {
      sendDivisions(color, overlay);
    }
  }

  private void sendDivisions(BossBarColor color, BossBarOverlay overlay) {
    BossBar bar = new BossBar();
    bar.setUuid(uuid);
    bar.setAction(BossBar.UPDATE_STYLE);
    bar.setColor(color.ordinal());
    bar.setOverlay(overlay.ordinal());
    sendToAffected(bar);
  }

  @Override
  public boolean isVisible() {
    return visible;
  }

  @Override
  public void setVisible(boolean visible) {
    boolean previous = this.visible;
    if (previous && !visible) {
      // The bar is being hidden
      sendToAffected(removePacket());
    } else if (!previous && visible) {
      // The bar is being shown
      sendToAffected(addPacket());
    }
    this.visible = visible;
  }

  @Override
  public Collection<BossBarFlag> getFlags() {
    return ImmutableList.copyOf(flags);
  }

  @Override
  public void addFlags(BossBarFlag... flags) {
    if (this.flags.addAll(Arrays.asList(flags)) && visible) {
      sendToAffected(updateFlags());
    }
  }

  @Override
  public void removeFlag(BossBarFlag flag) {
    checkNotNull(flag, "flag");
    if (this.flags.remove(flag) && visible) {
      sendToAffected(updateFlags());
    }
  }

  @Override
  public void removeFlags(BossBarFlag... flags) {
    if (this.flags.removeAll(Arrays.asList(flags)) && visible) {
      sendToAffected(updateFlags());
    }
  }

  private short serializeFlags() {
    short flagMask = 0x0;
    if (flags.contains(BossBarFlag.DARKEN_SCREEN)) {
      flagMask |= 0x1;
    }
    if (flags.contains(BossBarFlag.PLAY_BOSS_MUSIC)) {
      flagMask |= 0x2;
    }
    if (flags.contains(BossBarFlag.CREATE_WORLD_FOG)) {
      flagMask |= 0x4;
    }
    return flagMask;
  }

  private BossBar addPacket() {
    BossBar bossBar = new BossBar();
    bossBar.setUuid(uuid);
    bossBar.setAction(BossBar.ADD);
    bossBar.setName(GsonComponentSerializer.INSTANCE.serialize(title));
    bossBar.setColor(color.ordinal());
    bossBar.setOverlay(overlay.ordinal());
    bossBar.setPercent(percent);
    bossBar.setFlags(serializeFlags());
    return bossBar;
  }

  private BossBar removePacket() {
    BossBar bossBar = new BossBar();
    bossBar.setUuid(uuid);
    bossBar.setAction(BossBar.REMOVE);
    return bossBar;
  }

  private BossBar updateFlags() {
    BossBar bossBar = new BossBar();
    bossBar.setUuid(uuid);
    bossBar.setAction(BossBar.UPDATE_PROPERTIES);
    bossBar.setFlags(serializeFlags());
    return bossBar;
  }

  private void sendToAffected(MinecraftPacket packet) {
    for (Player player : players) {
      if (player.isActive() && player.getProtocolVersion().getProtocol()
          >= ProtocolVersion.MINECRAFT_1_9.getProtocol()) {
        sendPacket(player, packet);
      }
    }
  }

  private void sendPacket(Player player, MinecraftPacket packet) {
    ConnectedPlayer connected = (ConnectedPlayer) player;
    connected.getMinecraftConnection().write(packet);
  }
}