package edu.berkeley.nlp.starcraft.unit;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.bwapi.bridge.model.Color;
import org.bwapi.bridge.model.CoordinateType;
import org.bwapi.bridge.model.Game;
import org.bwapi.bridge.model.Unit;

import edu.berkeley.nlp.starcraft.HideableData;
import edu.berkeley.nlp.starcraft.drawing.DrawingContext;
import edu.berkeley.nlp.starcraft.drawing.PenDrawingContext;
import edu.berkeley.nlp.starcraft.drawing.StipplePen;
import edu.berkeley.nlp.starcraft.drawing.DrawUtils;
import edu.berkeley.nlp.starcraft.overmind.Underling;

/**
 * This is a basic tracker for non-building enemy and neutral units. Basically,
 * it does for non-building units what the Starcraft GUI does when building
 * units enter the fog of war: it replaces the unit with a phantom unit, and
 * when a hidden unit is shown to be not actually there, it disappears. It works
 * for all races and handles tricky corner cases such as mind-controlled
 * renegades. There is an option to delete hidden units after a certain amount
 * of time because a ton of hidden units may be created if units keep jumping in
 * and out of the fog of war such as when workers mine.
 * 
 * TODO: Currently, this tracker does not associate hidden units with the actual
 * units that created them. This can be done because units have IDs, but it may
 * be considered cheating because human players cannot tag units it sees with
 * IDs (and thus recognize them when they come out of the fog of war).
 * 
 * @author Eugene Ma
 */
public class HideableUnitsData extends HideableData implements Underling {
  protected static int TEXT_OFFSET_X = 3;
  protected static int TEXT_OFFSET_Y = 1;

  // *****************************************************************************************

  public static class UnitsPackage {
    protected Set<Unit> myVisibleUnits = new HashSet<Unit>();
    protected List<TimeStampedPhantomUnit> myHiddenUnits = new LinkedList<TimeStampedPhantomUnit>();

    protected StipplePen myPenForHiddenUnits = new StipplePen();

    // -----------------------------------------------------------------------------------------

    public Set<Unit> getVisibleUnits() {
      return myVisibleUnits;
    }

    public List<TimeStampedPhantomUnit> getHiddenUnits() {
      return myHiddenUnits;
    }

    // -----------------------------------------------------------------------------------------

    // Visible units.
    protected void addVisibleUnit(Unit visibleUnit) {
      myVisibleUnits.add(visibleUnit);
    }

    protected void removeVisibleUnit(Unit visibleUnit) {
      myVisibleUnits.remove(visibleUnit);
    }

    protected void convertToHiddenUnit(Unit visibleUnit, int frameCount) {
      myVisibleUnits.remove(visibleUnit);
      myHiddenUnits.add(new TimeStampedPhantomUnit(visibleUnit, frameCount));
    }

    protected void removeInvisibleVisibleUnits() {
      // We may need to clean up invisible units that should be visible when a
      // hallucination disappears.
      // (It may also be due to a bug.)
      for (Iterator<Unit> iterator = myVisibleUnits.iterator(); iterator.hasNext();) {
        Unit visibleUnit = iterator.next();
        if (!visibleUnit.isVisible()) {
          iterator.remove();
          // TODO: Remove debug code.
         // Game.getInstance().printf(
         //     "Warning: A unit that should be visible was removed "
         //         + "because it is invisible. Was this unit a hallucination?");
        }
      }
    }

    // -----------------------------------------------------------------------------------------

    // Hidden units.
    protected void addHiddenUnit(Unit hiddenUnit, int frameCount) {
      myHiddenUnits.add(new TimeStampedPhantomUnit(hiddenUnit, frameCount));
    }

    protected void removeVisibleHiddenUnits() {
      // Remove "hidden" units that have just become visible.
      for (Iterator<TimeStampedPhantomUnit> iterator = myHiddenUnits.iterator(); iterator.hasNext();) {
        TimeStampedPhantomUnit hiddenUnit = iterator.next();
        if (hiddenUnit.isVisible()) {
          iterator.remove();
        }
      }
    }

    protected void removeOldHiddenUnits(int frameCount, int lifetime) {
      // Remove units that have exceeded their lifetime.
      for (Iterator<TimeStampedPhantomUnit> iterator = myHiddenUnits.iterator(); iterator.hasNext();) {
        TimeStampedPhantomUnit hiddenUnit = iterator.next();
        // Note: We have >= instead of > because the time stamp
        // indicates how many frames have completed.
        // For example, a unit may have a time stamp of 0,
        // so after one frame is completed (frame count = 1), we delete the
        // unit.
        if (frameCount - hiddenUnit.getTimeStamp() >= lifetime) {
          iterator.remove();
        }
      }
    }

    // -----------------------------------------------------------------------------------------

    protected void draw(int insetSize, Color color, int frameCount, int hiddenDataLifetime,
        DrawingContext context) {
      final int radius = 6;

      // Draw the visible units.
      for (Unit visibleUnit : myVisibleUnits) {
        DrawUtils.drawUnitDimension(context, visibleUnit, insetSize, color, false);

        // Draw an X at the unit's position.
        DrawUtils.drawXAt(context, CoordinateType.MAP, visibleUnit.getPosition().x(), visibleUnit
            .getPosition().y(), radius, color);
      }

      // Draw the hidden units.
      DrawingContext contextForHiddenUnits = new PenDrawingContext(context, myPenForHiddenUnits);
      for (TimeStampedPhantomUnit hiddenUnit : myHiddenUnits) {
        myPenForHiddenUnits.setStipplePatternIndex(0);
        hiddenUnit.drawUnitDimension(contextForHiddenUnits, insetSize, color, false);

        // Draw a circle at the unit's position.
        myPenForHiddenUnits.setStipplePatternIndex(frameCount
            % myPenForHiddenUnits.getStipplePattern().length());
        // Circle radius is scaled by the unit's lifetime.
        DrawUtils.drawArc(contextForHiddenUnits, CoordinateType.MAP, hiddenUnit.getPosition().x(), hiddenUnit
            .getPosition().y(), radius, 0, 2 * Math.PI, 3, Color.ORANGE);

        /*
         * // TODO: Comment out for speedier overlays. // Draw the lifetime of
         * the unit. context.drawTextMap( hiddenUnit.getPosition().x() -
         * hiddenUnit.getType().dimensionLeft() + TEXT_OFFSET_X,
         * hiddenUnit.getPosition().y() - hiddenUnit.getType().dimensionUp() +
         * TEXT_OFFSET_Y, "" + (frameCount - hiddenUnit.getTimeStamp())); //
         */
      }
    }
  }

  // *****************************************************************************************

  protected UnitsPackage myEnemyUnitsPackage = new UnitsPackage();
  protected UnitsPackage myNeutralUnitsPackage = new UnitsPackage();

  // -----------------------------------------------------------------------------------------

  public HideableUnitsData(int hiddenDataLifetime) {
    super(hiddenDataLifetime);
  }

  // -----------------------------------------------------------------------------------------

  public UnitsPackage getEnemyUnitsPackage() {
    return myEnemyUnitsPackage;
  }

  public UnitsPackage getNeutralUnitsPackage() {
    return myNeutralUnitsPackage;
  }

  // -----------------------------------------------------------------------------------------

  public void onStart() {
    // Add neutral units, which may be visible or hidden.
    for (Unit neutralUnit : Game.getInstance().getStaticNeutralUnits()) {
      if (!neutralUnit.getType().isBuilding()) {
        if (neutralUnit.isVisible()) {
          myNeutralUnitsPackage.addVisibleUnit(neutralUnit);
        } else {
          myNeutralUnitsPackage.addHiddenUnit(neutralUnit, getFrameCount());
        }
      }
    }

    // Add visible enemy units.
    for (Unit unit : Game.getInstance().getAllUnits()) {
      if (unit.isVisible() && unit.getPlayer().isEnemy(Game.getInstance().self())
          && !unit.getType().isBuilding()) {
        myEnemyUnitsPackage.addVisibleUnit(unit);
      }
    }
  }

  public void onFrame() {
    myEnemyUnitsPackage.removeOldHiddenUnits(getFrameCount(), getHiddenDataLifetime());
    myNeutralUnitsPackage.removeOldHiddenUnits(getFrameCount(), getHiddenDataLifetime());

    myEnemyUnitsPackage.removeVisibleHiddenUnits();
    myNeutralUnitsPackage.removeVisibleHiddenUnits();

    myEnemyUnitsPackage.removeInvisibleVisibleUnits();

    // Call last because the frame counter is incremented here.
    super.onFrame();
  }

  @Override
  public void onFrame(int frameNum) {
    // TODO: Incorporate frame number.
    onFrame();
  }

  public void onUnitShow(Unit unit) {
    if (!unit.getType().isBuilding()) {
      if (unit.getPlayer().isEnemy(Game.getInstance().self())) {
        myEnemyUnitsPackage.addVisibleUnit(unit);
      } else if (unit.getType().isNeutral()) {
        myNeutralUnitsPackage.addVisibleUnit(unit);
      }
    }
  }

  public void onUnitHide(Unit unit) {
    if (!unit.getType().isBuilding()) {
      if (unit.getPlayer().isEnemy(Game.getInstance().self())) {
        myEnemyUnitsPackage.convertToHiddenUnit(unit, getFrameCount());
      } else if (unit.getType().isNeutral()) {
        myNeutralUnitsPackage.convertToHiddenUnit(unit, getFrameCount());
      }
    }
  }

  public void onUnitMorph(Unit unit) {
    if (unit.getPlayer().isEnemy(Game.getInstance().self())) {
      // When morphing, prevent double-adding.
      if (unit.getType().isBuilding()) {
        if (XUnitUtils.isBuildingThatMorphsFromDrone(unit.getType())) {
          if (myEnemyUnitsPackage.getVisibleUnits().contains(unit)) {
            myEnemyUnitsPackage.removeVisibleUnit(unit);
          }
        }
      } else {
        // This accounts for the tricky cases of building-to-drone and
        // non-building-to-non-building.
        if (!myEnemyUnitsPackage.getVisibleUnits().contains(unit)) {
          myEnemyUnitsPackage.addVisibleUnit(unit);
        }
      }
    }

    // Note: Neutral units do not morph.
  }

  public void onUnitRenegade(Unit unit) {
    if (!unit.getType().isBuilding()) {
      if (unit.getPlayer().isEnemy(Game.getInstance().self())) {
        // The enemy player's Protoss Dark Archon can mind control self player's
        // units.
        if (!myEnemyUnitsPackage.getVisibleUnits().contains(unit)) {
          myEnemyUnitsPackage.addVisibleUnit(unit);
        }
      } else if (unit.getPlayer().equals(Game.getInstance().self())) {
        // The self player's Protoss Dark Archon can mind control the enemy's
        // units.
        if (myEnemyUnitsPackage.getVisibleUnits().contains(unit)) {
          myEnemyUnitsPackage.removeVisibleUnit(unit);
        }
      }
    }
  }

  // -----------------------------------------------------------------------------------------

  public void draw(DrawingContext context) {
    myEnemyUnitsPackage.draw(0, Color.RED, getFrameCount(), getHiddenDataLifetime(), context);
    myNeutralUnitsPackage.draw(0, Color.WHITE, getFrameCount(), getHiddenDataLifetime(), context);
  }
}
