content: /*
   * Copyright 2008-present, David Karnok & Contributors
   * The file is part of the Open Imperium Galactica project.
   *
   * The code should be distributed under the LGPL license.
   * See http://www.gnu.org/licenses/lgpl.html for details.
   */
  
  package hu.openig.mechanics;
  
  import hu.openig.core.Action0;
  import hu.openig.core.Action1;
  import hu.openig.core.Difficulty;
  import hu.openig.model.AIAttackMode;
  import hu.openig.model.AIBuilding;
  import hu.openig.model.AIControls;
  import hu.openig.model.AIFleet;
  import hu.openig.model.AIInventoryItem;
  import hu.openig.model.AIPlanet;
  import hu.openig.model.AIWorld;
  import hu.openig.model.ApproachType;
  import hu.openig.model.CallType;
  import hu.openig.model.DiplomaticOffer;
  import hu.openig.model.DiplomaticRelation;
  import hu.openig.model.ExplorationMap;
  import hu.openig.model.FleetKnowledge;
  import hu.openig.model.FleetTask;
  import hu.openig.model.InventoryItem;
  import hu.openig.model.InventorySlot;
  import hu.openig.model.ModelUtils;
  import hu.openig.model.Planet;
  import hu.openig.model.PlanetKnowledge;
  import hu.openig.model.PlanetStatistics;
  import hu.openig.model.Player;
  import hu.openig.model.ResearchSubCategory;
  import hu.openig.model.SoundTarget;
  import hu.openig.model.SoundType;
  
  import java.awt.geom.Point2D;
  import java.awt.geom.Point2D.Double;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.Date;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * The starmap exploration and satellite planner.
   * @author akarnokd, 2011.12.27.
   */
  public class AttackPlanner extends Planner {
      /**
       * @author akarnokd, 2012.05.02.
       *
       */
      private final class PlanetTargetValueComparator implements
      Comparator<AIPlanet> {
          /**
           * The center of our empire.
           */
          final Double center;
  
          /**
           * Constructor.
           * @param center the center of our empire.
           */
          private PlanetTargetValueComparator(Double center) {
              this.center = center;
          }
  
          @Override
          public int compare(AIPlanet o1, AIPlanet o2) {
              double value1 = planetValue(o1);
              double value2 = planetValue(o2);
  
              double relation1 = getDiplomaticMultiplier(o1.owner);
              double relation2 = getDiplomaticMultiplier(o2.owner);
  
              double distance1 = Math.hypot(o1.planet.x - center.x, o1.planet.y - center.y);
              double distance2 = Math.hypot(o2.planet.x - center.x, o2.planet.y - center.y);
  
              int c = java.lang.Double.compare(relation1, relation2);
  
              if (c == 0) {
                  c = java.lang.Double.compare(value1, value2);
              }
              if (c == 0) {
                  c = java.lang.Double.compare(distance1, distance2);
              }
  
              return c;
          }
      }
      /** The war limit. */
      protected static final int WAR_LIMIT = 75;
      /** Sets the new attack date. */
      final Action1<Date> setNewAttack;
      /**
       * Constructor. Initializes the fields.
       * @param world the world object
       * @param controls the controls to affect the world in actions
       * @param exploration the exploration map
       * @param setNewAttack set the new attack date
       */
      public AttackPlanner(AIWorld world, AIControls controls,
  
              ExplorationMap exploration, Action1<Date> setNewAttack) {
          super(world, controls);
          this.setNewAttack = setNewAttack;
      }
      @Override
      public void plan() {
          if (world.ownPlanets.isEmpty()) {
              return;
          }
          // don't bother when exploring
          //        for (AIFleet f : world.ownFleets) {
          //            if (f.task.ordinal() <= FleetTask.EXPLORE.ordinal()) {
          //                return;
          //            }
          //        }
  
          if (world.nextAttack != null && world.nextAttack.compareTo(world.now) < 0) {
              world.nextAttack = null;
  
              List<AIFleet> fleets = findFleetsFor(FleetTask.ATTACK, null);
              filterCandidates(fleets);
              if (!fleets.isEmpty()) {
                  Collections.sort(fleets, new Comparator<AIFleet>() {
                      @Override
                      public int compare(AIFleet o1, AIFleet o2) {
                          return java.lang.Double.compare(o1.statistics.firepower, o2.statistics.firepower);
                      }
                  });
  
                  int fcmax = fleets.size() / 2 + 1;
                  if (world.difficulty == Difficulty.EASY) {
                      fcmax = 1;
                  }
                  int fc = 0;
                  Iterator<AIFleet> fi = fleets.iterator();
                  while (fc < fcmax && fi.hasNext()) {
                      final AIFleet ownFleet = fi.next();
  
                      AIFleet targetFleet = null;
                      AIPlanet targetPlanet = null;
                      if (world.mayConquer && ownFleet.statistics.groundFirepower > 0) {
                          if (ModelUtils.randomBool()) {
                              targetFleet = selectTargetFleet();
                          }
                          if (targetFleet == null) {
                              targetPlanet = selectTargetPlanet();
                          }
                      } else {
                          targetFleet = selectTargetFleet();
                      }
                      if (targetFleet != null) {
                          final AIFleet ftargetFleet = targetFleet;
                          ownFleet.task = FleetTask.ATTACK;
                          add(new Action0() {
                              @Override
                              public void invoke() {
                                  if (ownFleet.task != FleetTask.SCRIPT) {
                                      controls.actionAttackFleet(ownFleet.fleet, ftargetFleet.fleet, false);
                                  }
                              }
                          });
                          fc++;
                      } else
  
                          if (targetPlanet != null) {
                              final AIPlanet ftargetPlanet = targetPlanet;
                              final Player fExpectedOwner = ftargetPlanet.owner;
                              ownFleet.task = FleetTask.ATTACK;
                              add(new Action0() {
                                  @Override
                                  public void invoke() {
                                      if (ownFleet.task != FleetTask.SCRIPT && fExpectedOwner == ftargetPlanet.planet.owner) {
                                          controls.actionAttackPlanet(ownFleet.fleet, ftargetPlanet.planet, AIAttackMode.CAPTURE);
                                      }
                                  }
                              });
                              fc++;
                          }
                  }
              }
              if (world.hasDiplomacyRoom) {
                  manageDiplomacy();
              }
          }
          computeNextAttack();
      }
      /**
       * Consider fleets who are at least on half strength of the full potential.
       * @param fleets the list of fleets
       */
      void filterCandidates(List<AIFleet> fleets) {
          for (int i = fleets.size() - 1; i >= 0; i--) {
              AIFleet fl = fleets.get(i);
              boolean notEquipped = false;
              if (fl.statistics.fighterCount >= world.fighterLimit
                      && fl.statistics.cruiserCount >= world.cruiserLimit
                      && fl.statistics.battleshipCount >= world.battleshipLimit
                      && fl.statistics.vehicleCount >= fl.statistics.vehicleMax) {
                  outer:
                  for (AIInventoryItem ii : fl.inventory) {
                      for (InventorySlot is : ii.slots) {
                          if (is.type == null) {
                              notEquipped = true;
                              break outer;
                          }
                      }
                  }
              } else {
                  notEquipped = true;
              }
              if (notEquipped) {
                  fleets.remove(i);
              }
          }
      }
      /**
       * Compute the time when the next attack will happen.
       */
      void computeNextAttack() {
          if (world.nextAttack == null) {
              if (world.mayConquer) {
                  long next = 0;
                  if (world.difficulty == Difficulty.HARD) {
                      next = 30_000;
                  } else {
                      next = (3 + ModelUtils.randomInt(4)) * 60L * 60 * 24 * 1000
                              + ModelUtils.randomInt(24) * 60L * 60 * 1000;
                  }
                  world.nextAttack = new Date(world.now.getTime() + next);
                  setNewAttack.invoke(world.nextAttack);
              }
          }
      }
      /**
       * Select the weakest fleet based on our knowledge.
       * @return the weakest fleet or null if no fleet available
       */
      AIFleet selectTargetFleet() {
          List<AIFleet> candidates = new ArrayList<>();
          for (AIFleet f : world.enemyFleets) {
              if (!(f.fleet.owner.ai instanceof AITrader) && !(f.fleet.owner.ai instanceof AIPirate)) {
  
                  DiplomaticRelation dr = world.relations.get(f.fleet.owner);
  
                  if (dr != null && dr.full && !dr.strongAlliance) {
                      if (dr.value < p.warThreshold && !hasActiveAlliance(dr.alliancesAgainst)) {
                          candidates.add(f);
                      }
                  }
              }
          }
          if (!candidates.isEmpty()) {
              final Point2D.Double center = world.center();
              return Collections.min(candidates, new Comparator<AIFleet>() {
                  @Override
                  public int compare(AIFleet o1, AIFleet o2) {
                      double v1 = fleetValue(o1);
                      double v2 = fleetValue(o2);
  
                      double d1 = Math.hypot(o1.x - center.x, o1.y - center.y);
                      double d2 = Math.hypot(o2.x - center.x, o2.y - center.y);
                      if (v1 < v2) {
                          return -1;
                      } else
                          if (v1 > v2) {
                              return 1;
                          } else
                              if (d1 < d2) {
                                  return -1;
                              } else
                                  if (d1 > d2) {
                                      return 1;
                                  }
                      return 0;
                  }
              });
          }
          return null;
      }
      /**
       * Select an enemy planet.
       * @return the planet or null if none found
       */
      AIPlanet selectTargetPlanet() {
          List<AIPlanet> candidates = new ArrayList<>();
          Set<Player> enemies = new HashSet<>();
          for (AIPlanet p : world.enemyPlanets) {
              if (p.owner != null) {
                  if (world.explorationInnerLimit != null && world.explorationInnerLimit.contains(p.planet.x, p.planet.y)) {
                      continue;
                  }
                  if (world.explorationOuterLimit != null && !world.explorationOuterLimit.contains(p.planet.x, p.planet.y)) {
                      continue;
                  }
                  DiplomaticRelation dr = world.relations.get(p.owner);
                  if (dr != null && dr.full && !dr.strongAlliance) {
                      if (dr.value < this.p.warThreshold && !hasActiveAlliance(dr.alliancesAgainst)) {
                          if (planetValue(p) > 0) {
                              candidates.add(p);
                              enemies.add(p.owner);
                          }
                      }
                  }
              }
          }
          if (!candidates.isEmpty()) {
              final Point2D.Double center = world.center();
  
              List<Player> enemiesList = new ArrayList<>(enemies);
              Collections.sort(enemiesList, new Comparator<Player>() {
                  @Override
                  public int compare(Player o1, Player o2) {
                      double r1 = getDiplomaticMultiplier(o1);
                      double r2 = getDiplomaticMultiplier(o2);
                      return java.lang.Double.compare(r1, r2);
                  }
  
              });
  
              Player targetPlayer = choseAtRandom(enemiesList);
  
              for (int i = candidates.size() - 1; i >= 0; i--) {
                  if (candidates.get(i).owner != targetPlayer) {
                      candidates.remove(i);
                  }
              }
  
              // then select one of his planets
              Collections.sort(candidates, new PlanetTargetValueComparator(center));
  
              return choseAtRandom(candidates);
          }
          return null;
      }
      /**
       * Chose an element from the list with the following probabilities:
       * 60% chance to select the first, 15% to select the second or
       * 25% chance to select one from the remaining elements at uniform distribution.
       * @param <T> the element type
       * @param list the list of elements, shouldn't be empty
       * @return the selected item
       */
      <T> T choseAtRandom(List<T> list) {
          double prob = ModelUtils.random();
          int idx = 0;
          if (prob < 0.6) {
              idx = 0;
          } else
          if (prob >= 0.6 && prob < 0.75) {
              idx = 1;
          } else {
              if (list.size() > 2) {
                  idx = 2 + ModelUtils.randomInt(list.size() - 2);
              } else {
                  idx = ModelUtils.randomInt(2);
              }
          }
  
          idx = Math.min(idx, list.size() - 1);
  
          return list.get(idx);
      }
      /**
       * Returns the diplomatic value multiplier, e.g., 100 - relation,
       * which is considered when selecting valuable targets.
       * @param owner the other player
       * @return the value
       */
      double getDiplomaticMultiplier(Player owner) {
          DiplomaticRelation dr = world.relations.get(owner);
          if (dr != null) {
              return dr.value;
          }
          return 50;
      }
      /**
       * Check if the sequence of common enemies has still active elements.
       * @param against the sequence of other players
       * @return true if any of those players still has planets
       */
      boolean hasActiveAlliance(Iterable<? extends String> against) {
          for (String p0 : against) {
              if (!world.planetsOf(p0).isEmpty()) {
                  return true;
              }
          }
          return false;
      }
      /**
       * Estimate fleet value.
       * @param f the target fleet
       * @return the value
       */
      public static double fleetValue(AIFleet f) {
          if (f.knowledge == FleetKnowledge.VISIBLE) {
              return 0;
          }
          double v = 0;
          if (f.knowledge == FleetKnowledge.COMPOSITION) {
              for (AIInventoryItem ii : f.inventory) {
                  if (ii.type.category == ResearchSubCategory.SPACESHIPS_BATTLESHIPS
                          || ii.type.category == ResearchSubCategory.SPACESHIPS_CRUISERS
                          || ii.type.category == ResearchSubCategory.SPACESHIPS_FIGHTERS) {
                      v += ii.type.productionCost * ((ii.count + 9 / 10) * 10);
                  }
              }
          } else {
              for (AIInventoryItem ii : f.inventory) {
                  v += ii.type.productionCost * ii.count;
              }
          }
          return v;
      }
      /**
       * Computes the planet value.
       * @param p the planet
       * @return the value
       */
      public static double planetValue(AIPlanet p) {
          if (p.knowledge == PlanetKnowledge.OWNER) {
              return 1;
          }
          double v = 1;
          if (p.knowledge.ordinal() >= PlanetKnowledge.STATIONS.ordinal()) {
              for (AIInventoryItem ii : p.inventory) {
                  if (ii.type.category == ResearchSubCategory.SPACESHIPS_STATIONS) {
                      v += ii.type.productionCost * ii.count;
                  }
              }
          }
          if (p.knowledge.ordinal() >= PlanetKnowledge.BUILDING.ordinal()) {
              for (AIBuilding b : p.buildings) {
                  if (b.type.kind.equals("Defensive")
  
                          || b.type.kind.equals("Gun")
                          || b.type.kind.equals("Shield")) {
                      v += b.type.cost;
                  }
              }
              v += p.statistics.vehicleMax * 500;
          }
          return v;
      }
      /**
  
       * Audio signal for new message.
       * @param p the target player
  
       */
      void signalMessage(Player p) {
          if (p == p.world.player) {
              if (ModelUtils.randomBool()) {
                  p.world.env.playSound(SoundTarget.COMPUTER, SoundType.NEW_MESSAGE_1, null);
              } else {
                  p.world.env.playSound(SoundTarget.COMPUTER, SoundType.NEW_MESSAGE_2, null);
              }
          }
      }
      /**
       * Make contact with other races and send offers.
       */
      void manageDiplomacy() {
          for (Map.Entry<Player, DiplomaticRelation> dr0 : world.relations.entrySet()) {
              final Player other = dr0.getKey();
              double rnd = ModelUtils.random();
              DiplomaticRelation dr = dr0.getValue();
              boolean mayContact = dr.lastContact == null
  
                      || (world.now.getTime() - dr.lastContact.getTime() >= (dr.wontTalk() ? 7L : 1L) * 24 * 60 * 60 * 1000);
  
              if (dr.full && !world.activeOffer.contains(other) && mayContact) {
  
                  final ApproachType at = ModelUtils.random(Arrays.asList(ApproachType.values()));
  
                  if (rnd < 0.1 && world.ownPlanets.size() < 2 && dr.value < p.warThreshold) {
                      add(new Action0() {
                          @Override
                          public void invoke() {
                              other.offers.put(p.id, new DiplomaticOffer(CallType.SURRENDER, at));
                              signalMessage(p);
                          }
                      });
                      break;
                  } else
                      if (rnd < 0.25 && dr.value >= p.warThreshold + 25) {
                          add(new Action0() {
                              @Override
                              public void invoke() {
                                  other.offers.put(p.id, new DiplomaticOffer(CallType.ALLIANCE, at));
                                  signalMessage(p);
                              }
                          });
                          break;
                      } else
                          if (rnd < 0.5 && dr.value < p.warThreshold && countPlanets(world.enemyPlanets, other) < 3) {
                              add(new Action0() {
                                  @Override
                                  public void invoke() {
                                      other.offers.put(p.id, new DiplomaticOffer(CallType.RESIGN, at));
                                      signalMessage(p);
                                  }
                              });
                              break;
                          } else
                              if (rnd < 0.25 && world.money >= 75000 && dr.value >= p.warThreshold + 5) {
                                  add(new Action0() {
                                      @Override
                                      public void invoke() {
                                          other.offers.put(p.id, new DiplomaticOffer(CallType.MONEY, at).value(2500L * (ModelUtils.randomInt(20) + 1)));
                                          signalMessage(p);
                                      }
                                  });
                                  break;
                              } else
                                  if (rnd < 0.5 && dr.value < p.warThreshold - 10) {
                                      add(new Action0() {
                                          @Override
                                          public void invoke() {
                                              other.offers.put(p.id, new DiplomaticOffer(CallType.WAR, at));
                                              signalMessage(p);
                                          }
                                      });
                                      break;
                                  } else
                                      if (rnd < 0.1 && dr.value >= p.warThreshold - 10 && dr.value < p.warThreshold) {
                                          add(new Action0() {
                                              @Override
                                              public void invoke() {
                                                  other.offers.put(p.id, new DiplomaticOffer(CallType.PEACE, at));
                                                  signalMessage(p);
                                              }
                                          });
                                          break;
                                      }
              }
          }
      }
      /**
       * Count the number of planets of the owner.
       * @param planets the known planet list
       * @param owner the owner
       * @return the count
       */
      int countPlanets(List<AIPlanet> planets, Player owner) {
          int result = 0;
          for (AIPlanet aip : planets) {
              if (aip.owner == owner) {
                  result++;
              }
          }
          return result;
      }
      /**
       * Handle the loss of a planet due to conquest.
       * @param planet the target planet
       * @param p the the AI player
       */
      public static void onPlanetLost(Planet planet, AI p) {
          switch (p.w.difficulty) {
          case NORMAL:
              p.nextAttack = new Date(p.w.time.getTimeInMillis() + 2L * 60 * 60 * 1000);
              break;
          case HARD:
              p.nextAttack = new Date(p.w.time.getTimeInMillis() + 30 * 60 * 1000);
              break;
          default:
          }
      }
      /**
       * Handle the conquest of a planet.
       * @param planet the target planet
       * @param p the the AI player
       */
      public static void onPlanetConquered(Planet planet, AI p) {
          PlanetStatistics ps = planet.getStatistics();
          int max = ps.vehicleMax - 1;
          List<Planet> ownPlanets = p.p.ownPlanets();
          ownPlanets.remove(planet);
          if (!ownPlanets.isEmpty()) {
              Collections.sort(ownPlanets, Planet.VALUE);
  
              for (Planet p0 : ownPlanets) {
                  if (max <= 0) {
                      break;
                  }
                  for (InventoryItem ii : p0.inventory.iterable()) {
                      if (ii.owner == p.p && ii.type.category == ResearchSubCategory.WEAPONS_TANKS) {
                          p0.undeployItem(ii.id, 1);
                          planet.deployItem(ii.type, p.p, 1);
                          max--;
                          break;
                      }
                  }
              }
          }
      }
  }
  