package com.matt.forgehax.mods;

import com.google.common.collect.EvictingQueue;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;
import com.matt.forgehax.asm.events.PacketEvent;
import com.matt.forgehax.events.RenderEvent;
import com.matt.forgehax.util.color.Colors;
import com.matt.forgehax.util.command.Setting;
import com.matt.forgehax.util.mod.Category;
import com.matt.forgehax.util.mod.ToggleMod;
import com.matt.forgehax.util.mod.loader.RegisterMod;
import com.matt.forgehax.util.tesselation.GeometryMasks;
import com.matt.forgehax.util.tesselation.GeometryTessellator;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import net.minecraft.network.play.server.SPacketChunkData;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.ChunkPos;
import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;

/**
 * Created on 10/12/2017 by fr1kin
 */
@RegisterMod
public class ChunkLogger extends ToggleMod {
  
  public ChunkLogger() {
    super(Category.MISC, "ChunkLogger", false, "Show new chunks");
  }
  
  enum ShowChunkEnum {
    ALL,
    NEW_ONLY,
    OLD_ONLY,
    ;
  }
  
  enum DetectionMethodEnum {
    IS_FULL_CHUNK,
    TIMING,
    BLOCK_CHANGE_THRESHOLD,
    DECORATOR_BLOCKS_DETECTED,
  }
  
  private final Setting<Integer> max_chunks =
      getCommandStub()
          .builders()
          .<Integer>newSettingBuilder()
          .name("max-chunks")
          .description("Maximum chunks to render (set to 0 for infinite)")
          .defaultTo(5120)
          .build();
  
  private final Setting<Boolean> clear_on_toggle =
      getCommandStub()
          .builders()
          .<Boolean>newSettingBuilder()
          .name("clear-on-toggle")
          .description("Clear chunk list on disable")
          .defaultTo(true)
          .build();
  
  private final Setting<ShowChunkEnum> show_only =
      getCommandStub()
          .builders()
          .<ShowChunkEnum>newSettingEnumBuilder()
          .name("show-only")
          .description("Specify which chunk to only show")
          .defaultTo(ShowChunkEnum.ALL)
          .build();
  
  private final Setting<DetectionMethodEnum> detection_method =
      getCommandStub()
          .builders()
          .<DetectionMethodEnum>newSettingEnumBuilder()
          .name("detection-method")
          .description(
              "Specify the method to detect new chunks. Currently only IS_FULL_CHUNK is supported.")
          .defaultTo(DetectionMethodEnum.IS_FULL_CHUNK)
          .build();
  
  private final Setting<Long> flag_timing =
      getCommandStub()
          .builders()
          .<Long>newSettingBuilder()
          .name("flag-timing")
          .description(
              "Maximum time in MS that another chunk load in succession will trigger it to be marked as a new chunk")
          .defaultTo(1000L)
          .build();
  
  private final Setting<Integer> block_change_threshold =
      getCommandStub()
          .builders()
          .<Integer>newSettingBuilder()
          .name("block-change-threshold")
          .description(
              "Maximum number of blocks required to change between chunk loading in order to be marked as a new chunk")
          .defaultTo(100)
          .build();
  
  private final Lock chunkLock = new ReentrantLock();
  
  private Queue<ChunkData> chunks = null;
  
  void addChunk(SPacketChunkData packet) {
    if (chunks != null) {
      chunkLock.lock();
      try {
        ChunkData temp = new ChunkData(packet);
        ChunkData data = chunks.stream().filter(temp::equals).findAny().orElse(null);
        if (data != null) {
          data.update(packet);
          chunks.remove(data);
          chunks.add(data); // remove and re-add to bring forward in the queue
        } else {
          chunks.add(temp);
        }
      } finally {
        chunkLock.unlock();
      }
    }
  }
  
  @Override
  protected void onEnabled() {
    chunkLock.lock();
    try {
      if (max_chunks.get() <= 0) {
        chunks = Queues.newArrayDeque();
      } else {
        chunks = EvictingQueue.create(max_chunks.get());
      }
    } finally {
      chunkLock.unlock();
    }
  }
  
  @Override
  protected void onDisabled() {
    if (clear_on_toggle.get() && chunks != null) {
      chunkLock.lock();
      try {
        chunks.clear();
        chunks = null;
      } finally {
        chunkLock.unlock();
      }
    }
  }
  
  @SubscribeEvent
  public void onChunkLoad(ChunkEvent.Load event) {
    if (chunks != null) {
    }
  }
  
  @SubscribeEvent
  public void onPacketInbound(PacketEvent.Incoming.Pre event) {
    if (event.getPacket() instanceof SPacketChunkData) {
      SPacketChunkData packet = event.getPacket();
      addChunk(packet);
    }
  }
  
  @SubscribeEvent
  public void onRender(RenderEvent event) {
    if (chunks == null) {
      return;
    }
    
    event.getTessellator().beginLines();
    
    List<ChunkData> copy;
    chunkLock.lock();
    try {
      copy = Lists.newArrayList(chunks);
    } finally {
      chunkLock.unlock();
    }
    
    copy.forEach(
        chunk -> {
          switch (show_only.get()) {
            case NEW_ONLY:
              if (!chunk.isNewChunk()) {
                return;
              }
              break;
            case OLD_ONLY:
              if (chunk.isNewChunk()) {
                return;
              }
              break;
            case ALL:
            default:
              break;
          }
          
          int color = chunk.isNewChunk() ? Colors.WHITE.toBuffer() : Colors.RED.toBuffer();
          
          GeometryTessellator.drawQuads(
              event.getBuffer(),
              chunk.bbox.minX,
              chunk.bbox.minY,
              chunk.bbox.minZ,
              chunk.bbox.maxX,
              chunk.bbox.maxY,
              chunk.bbox.maxZ,
              GeometryMasks.Quad.ALL,
              color);
        });
    
    event.getTessellator().draw();
  }
  
  private interface ChunkLoadThread extends Callable<Object> {
  
  }
  
  private class ChunkData {
    
    final ChunkPos pos;
    final AxisAlignedBB bbox;
    final boolean isFullChunk; // initial chunk
    
    boolean isNewByFullChunk = false;
    boolean isNewByTiming = false;
    boolean isNewByBlockCount = false;
    boolean isNewByDecoratorBlocks = false;
    
    boolean updatedIsFullChunk;
    
    long timeArrived = -1;
    long previousTimeArrived;
    
    int blockCount = 0;
    int previousBlockCount;
    
    ChunkData(SPacketChunkData packet) {
      pos = new ChunkPos(packet.getChunkX(), packet.getChunkZ());
      bbox =
          new AxisAlignedBB(pos.getXStart(), 0, pos.getZStart(), pos.getXEnd(), 255, pos.getZEnd());
      isFullChunk = packet.isFullChunk();
      update(packet);
    }
    
    void update(SPacketChunkData packet) {
      updatedIsFullChunk = packet.isFullChunk();
      if (!updatedIsFullChunk) {
        isNewByFullChunk = true;
      }
      
      previousTimeArrived = timeArrived;
      timeArrived = System.currentTimeMillis();
      
      if (getTimeDifference() != -1 && getTimeDifference() <= flag_timing.get()) {
        isNewByTiming = true;
      }
      
      previousBlockCount = blockCount;
    }
    
    long getTimeDifference() {
      return previousTimeArrived == -1 ? -1 : previousTimeArrived - timeArrived;
    }
    
    boolean isNewChunk() {
      switch (detection_method.get()) {
        case IS_FULL_CHUNK:
          return isNewByFullChunk;
        case TIMING:
          return isNewByTiming;
      }
      return false;
    }
    
    @Override
    public boolean equals(Object obj) {
      return obj == this
          || (obj instanceof ChunkData
          && this.pos.x == ((ChunkData) obj).pos.x
          && this.pos.z == ((ChunkData) obj).pos.z);
    }
    
    @Override
    public int hashCode() {
      return Objects.hash(pos.x, pos.z);
    }
  }
}