#include "stdafx.h"
#include "Map.h"
#include "../../../../SparAIContainer.h"
#include "../../../PerceptualState.h"
#include "../../LocationFactory.h"
#include "AirLocation.h"
#include "Chokepoint.h"
#include "BaseLocation.h"

// TODO: include loaded units in locations/nb units calculations?

Map::Map()
  : m_unitRecordDestroyEventHandler(*this)
  , m_unitRecordMorphEventHandler(*this)
  , m_unitRecordRenegadeEventHandler(*this)
  , m_unitDiscoverEventHandler(*this)
  , m_unitEvadeEventHandler(*this)
  , m_ownStartBaseLocation(NULL)
  , m_airLocation(new AirLocation(this))
  , m_tiles(BWAPI::Broodwar->mapWidth(), BWAPI::Broodwar->mapHeight())
{
  m_airLocation->acquire(this);

  for (unsigned int x = 0; x < m_tiles.getWidth(); ++x)
  {
    for (unsigned int y = 0; y < m_tiles.getHeight(); ++y)
    {
      Tile& tile = m_tiles[x][y];
      tile.acquire(this);
      tile.setXY(x, y);
    }
  }

  m_unitDiscoverID = BWAPI::getUnitDiscoverEvent().subscribe(&m_unitDiscoverEventHandler, NULL);
  m_unitEvadeID = BWAPI::getUnitEvadeEvent().subscribe(&m_unitEvadeEventHandler, NULL);
  m_unitRecordDestroyID = Spar->getPerceptualState()->getUnitRecordDestroyEvent().subscribe(&m_unitRecordDestroyEventHandler, NULL);
  m_unitRecordMorphID = Spar->getPerceptualState()->getUnitRecordMorphEvent().subscribe(&m_unitRecordMorphEventHandler, NULL);
  m_unitRecordRenegadeID = Spar->getPerceptualState()->getUnitRecordRenegadeEvent().subscribe(&m_unitRecordRenegadeEventHandler, NULL);
}

Map::~Map()
{
  BWAPI::getUnitDiscoverEvent().unsubscribe(m_unitDiscoverID);
  BWAPI::getUnitEvadeEvent().unsubscribe(m_unitEvadeID);
  Spar->getPerceptualState()->getUnitRecordDestroyEvent().unsubscribe(m_unitRecordDestroyID);
  Spar->getPerceptualState()->getUnitRecordMorphEvent().unsubscribe(m_unitRecordMorphID);
  Spar->getPerceptualState()->getUnitRecordRenegadeEvent().unsubscribe(m_unitRecordRenegadeID);

  delete m_airLocation;

  for (stdext::hash_map<const BWTA::BaseLocation*, BaseLocation*>::iterator it = m_baseLocations.begin(); it != m_baseLocations.end(); ++it)
  {
    delete it->second;
  }
  for (stdext::hash_map<const BWTA::Chokepoint*, Chokepoint*>::iterator it = m_chokepoints.begin(); it != m_chokepoints.end(); ++it)
  {
    delete it->second;
  }
  for (stdext::hash_map<const BWTA::Region*, Region*>::iterator it = m_regions.begin(); it != m_regions.end(); ++it)
  {
    delete it->second;
  }
}

void Map::initialize(LocationFactory& locationFactory)
{
  addSubLocation(m_airLocation);

  for(std::set<BWTA::Region*>::const_iterator itRegion = BWTA::getRegions().begin(); itRegion != BWTA::getRegions().end(); ++itRegion)
  {
    Region* region = new Region(*itRegion, this);
    region->acquire(this);
    m_regions.insert(std::make_pair(*itRegion, region));
    addSubLocation(region);

    const std::set<BWTA::BaseLocation*>& baseLocations = (*itRegion)->getBaseLocations();
    for(std::set<BWTA::BaseLocation*>::const_iterator it = baseLocations.begin(); it != baseLocations.end(); ++it)
    {
      BaseLocation* baseLocation = locationFactory.instantiateBaseLocation(*it, region);
      baseLocation->acquire(this);
      m_baseLocations.insert(std::make_pair(*it, baseLocation));
      region->addBaseLocation(baseLocation);
    }
  }

  for (unsigned int x = 0; x < m_tiles.getWidth(); ++x)
  {
    for (unsigned int y = 0; y < m_tiles.getHeight(); ++y)
    {
      Tile& tile = m_tiles[x][y];
      Region* region = getRegion(BWAPI::TilePosition(x,y));
      region->addSubLocation(&tile);
    }
  }

  for(std::set<BWTA::Chokepoint*>::const_iterator itChokepoint = BWTA::getChokepoints().begin(); itChokepoint != BWTA::getChokepoints().end(); ++itChokepoint)
  {
    std::pair<BWTA::Region*, BWTA::Region*> connectedRegions = (*itChokepoint)->getRegions();
    Region* firstRegion = m_regions.find(connectedRegions.first)->second;
    Region* secondRegion = m_regions.find(connectedRegions.second)->second;
    Chokepoint* chokepoint = new Chokepoint(*itChokepoint, firstRegion, secondRegion, this);
    chokepoint->acquire(this);
    m_chokepoints.insert(std::make_pair(*itChokepoint, chokepoint));
    firstRegion->addChokepoint(chokepoint);
    secondRegion->addChokepoint(chokepoint);
    addSubLocation(chokepoint);
  }

  // Try to find own start base location

  for (std::set<BWAPI::TilePosition>::const_iterator itStartLocation = BWAPI::Broodwar->getStartLocations().begin(); itStartLocation != BWAPI::Broodwar->getStartLocations().end(); ++itStartLocation)
  {
    stdext::hash_map<const BWTA::BaseLocation*, BaseLocation*>::iterator it = m_baseLocations.find(BWTA::getNearestBaseLocation(*itStartLocation));
    if (it != m_baseLocations.end())
      m_startBaseLocationsCache.insert(it->second);
  }

  double minDistance = boost::numeric::bounds<double>::highest();
  for(std::set<BWAPI::Unit*>::const_iterator itUnit = BWAPI::Broodwar->self()->getUnits().begin(); itUnit != BWAPI::Broodwar->self()->getUnits().end(); ++itUnit)
  {
    if((*itUnit)->getType() == BWAPI::UnitTypes::Protoss_Nexus)
    {
      for (stdext::hash_map<const BWTA::BaseLocation*, BaseLocation*>::const_iterator it2 = m_baseLocations.begin(); it2 != m_baseLocations.end(); ++it2)
      {
        double distance = it2->second->getCurrentCenter().getDistance((*itUnit)->getPosition());
        if (distance <= 500)
        {
          m_startBaseLocationsCache.erase(it2->second);
          if (distance < minDistance)
          {
            m_ownStartBaseLocation = it2->second;
            minDistance = distance;
          }
        }
      }
    }
  }

#ifdef _DEBUG
  for(std::set<BWTA::BaseLocation*>::const_iterator itBaseLocation = BWTA::getBaseLocations().begin(); itBaseLocation != BWTA::getBaseLocations().end(); ++itBaseLocation)
  {
    assert(m_baseLocations.find(*itBaseLocation) != m_baseLocations.end());
  }
#endif
}

std::pair<BWAPI::TilePosition, int> Map::getOldestVisibleTile() const
{
  struct OldestVisitedTile
  {
    OldestVisitedTile()
      :m_min(boost::numeric::bounds<int>::highest()) 
      ,m_tile(BWAPI::TilePositions::None)
    {}

    void operator()(int x, int y, const InfluenceMap<Visibility> & map)
    {
      const Visibility& visibility = map.getData(x, y);
      BWAPI::TilePosition tile(x, y);
      if (visibility.lastVisible < m_min)
      {
        m_min = visibility.lastVisible;
        m_tile = tile;
      }
    }

    std::pair<BWAPI::TilePosition, int> getResult() const
    {
      return std::make_pair(m_tile, m_min);
    }

  private:
    int m_min;
    BWAPI::TilePosition m_tile;
  };

  const InfluenceMapRef<Visibility>::map_ptr map = Spar->getPerceptualState()->getVisibility();
  return map->for_each(OldestVisitedTile()).getResult();
}

BWAPI::TilePosition Map::getRandomTile() const
{
  return BWAPI::TilePosition(rand() % BWAPI::Broodwar->mapWidth(), rand() % BWAPI::Broodwar->mapHeight());
}

std::vector<Tile*> Map::getTiles(const UnitRecord* unitRecord)
{
  std::vector<Tile*> previousTiles;
  previousTiles.reserve(unitRecord->m_hostLocations.size());
  for (stdext::hash_map<AutoPopulatedLocation*, void*>::const_iterator it = unitRecord->m_hostLocations.begin();
       it != unitRecord->m_hostLocations.end();
       ++it)
  {
    Tile* tile;
    if ((tile = dynamic_cast<Tile*>(it->first)) != NULL)
    {
      previousTiles.push_back(tile);
    }
  }
  return previousTiles;
}

void Map::addUnit(UnitRecord* unitRecord)
{
  assert(getTiles(unitRecord).empty());
  assert(!unitRecord->isDestroyed());

  RaiseEventTaskList deferredEvents(*this);
  BWAPI::forEachTileUnderUnit(unitRecord->getTilePosition(), unitRecord->getPosition(), unitRecord->getType(), AddUnitToTile(this, unitRecord, deferredEvents));

  if (unitRecord->getType().isResourceContainer())
  {
    const BWTA::BaseLocation* closest = BWTA::getNearestBaseLocation(unitRecord->getPosition());
    if (closest != NULL)
    {
      getBaseLocation(closest)->onUnitAddedToLocation(unitRecord, this, deferredEvents);
    }
  }
  else if (unitRecord->getType().isResourceDepot())
  {
    const BWTA::BaseLocation* closest = BWTA::getNearestBaseLocation(unitRecord->getPosition());
    if (closest != NULL)
    {
      const int distance = unitRecord->getPosition().getApproxDistance(closest->getPosition());
      if (distance <= 6 * TILE_SIZE)
      {
        getBaseLocation(closest)->onUnitAddedToLocation(unitRecord, this, deferredEvents);
      }
    }
  }
}

void Map::removeUnit(UnitRecord* unitRecord)
{
  RaiseEventTaskList deferredEvents(*this);

  std::vector<Tile*> tiles = getTiles(unitRecord);
  for (std::vector<Tile*>::const_iterator it = tiles.begin(); it != tiles.end(); ++it)
  {
    (*it)->onUnitRemovedFromLocation(unitRecord, *it, deferredEvents);
  }

  const BWTA::BaseLocation* closest = BWTA::getNearestBaseLocation(unitRecord->getPosition());
  if (closest != NULL)
  {
    BaseLocation* baseLocation = getBaseLocation(closest);
    UnitRecord::DataID dataID = unitRecord->getData(baseLocation);
    if (unitRecord->isValid(dataID))
    {
      baseLocation->onUnitRemovedFromLocation(unitRecord, this, deferredEvents);
    }
  }
}

void Map::onUnitDiscover(void*, BWAPI::Unit* unit)
{
  UnitRecord* unitRecord = Spar->getPerceptualState()->getUnitRecord(unit);
  updateLocation(unitRecord);
}

void Map::onUnitEvade(void*, BWAPI::Unit* unit)
{
  // The unit is removed immediately from the map if it disappeared in a visible zone 
  //(which means the unit was loaded into a transport unit).

  if (!BWAPI::isDestroyed(unit))
  {
    UnitRecord* unitRecord = Spar->getPerceptualState()->getUnitRecord(unit);

  //
  // The UpdateUnits process will remove the unit anyway if it actually went into the transport.
  // Additionally, if there was a mistake and the unit didn't actually go into the transport, then we still have it here.
  //

    //if (unitRecord->getTransport() != NULL) // The actual condition should be whether the unit is in the field of view, but this will do...
  //{
    //  assert(BWAPI::isInFieldOfView(unitRecord->getTilePosition(), unitRecord->getPosition(), unitRecord->getType()));
    //  removeUnit(unitRecord);
  //}
  //else
  //{
      updateLocation(unitRecord);
  //}
}
}

void Map::onUnitRecordDestroy(void*, UnitRecord* unitRecord)
{
  removeUnit(unitRecord);
}

void Map::onUnitRecordMorph(void*, UnitRecord* unitRecord, BWAPI::UnitType previousType)
{
  RaiseEventTaskList deferredEvents(*this);
  for (stdext::hash_map<AutoPopulatedLocation*, void*>::iterator it = unitRecord->m_hostLocations.begin(); it != unitRecord->m_hostLocations.end(); ++it)
  {
    it->first->onUnitMorph(it->second, unitRecord, previousType, deferredEvents);
  }

  updateLocation(unitRecord); // The unit may have changed size
}

void Map::onUnitRecordRenegade(void*, UnitRecord* unitRecord, BWAPI::Player* previousPlayer)
{
  RaiseEventTaskList deferredEvents(*this);
  for (stdext::hash_map<AutoPopulatedLocation*, void*>::iterator it = unitRecord->m_hostLocations.begin(); it != unitRecord->m_hostLocations.end(); ++it)
  {
    it->first->onUnitRenegade(it->second, unitRecord, previousPlayer, deferredEvents);
  }
}

void Map::updateLocation(UnitRecord* unitRecord)
{
  RaiseEventTaskList deferredEvents(*this);

  // This tiles update process could certainly be made more efficient

  UnitRecord::TilesLocationParameters tilesParameters(unitRecord->getTilePosition(), unitRecord->getPosition(), unitRecord->getType());

  if (unitRecord->m_tilesLocationParametersCache != tilesParameters) // Optimization to avoid recalculating the tiles for an immobile unit
  {
    std::vector<Tile*> previousTiles = getTiles(unitRecord);
    if (previousTiles.empty())
    {
      addUnit(unitRecord);
    }
    else
    {
      BWAPI::forEachTileUnderUnit(unitRecord->getTilePosition(), unitRecord->getPosition(), unitRecord->getType(), AddUnitToTile(this, unitRecord, deferredEvents));

      for (std::vector<Tile*>::const_iterator it = previousTiles.begin();
           it != previousTiles.end();
           ++it)
      {
        Tile* tile = *it;
        tile->onUnitRemovedFromLocation(unitRecord, tile, deferredEvents);
      }
    }
    unitRecord->m_tilesLocationParametersCache = tilesParameters;
  }
}

void Map::update(UnitRecord* unitRecord)
{
  if (unitRecord->isAccessible())
  {   
    updateLocation(unitRecord);
  }
  else
  {
    // TODO: Here invisible units like spider mines should be removed only if the tile on which they are
    //       is in the "detection" field of view as well

    // Remove unit if its last known position is now in our field of view but the unit isn't there anymore 
    const UnitRecord::SavedState& state = unitRecord->getLastKnownState();
    if (BWAPI::isInFieldOfView(state.getTilePosition(), state.getPosition(), state.getType()))
    {
      unitRecord->onSavedPositionScouted();
      removeUnit(unitRecord);
    }
  }
}

Region* Map::getRegion(BWAPI::TilePosition tile) const
{
  BWTA::Region* region = BWTA::getRegion(tile);
  if (region != NULL)
    return getRegion(region);
  else
    return m_airLocation;
}
