/*
 * 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.Location;
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.EquipmentSlot;
import hu.openig.model.ExplorationMap;
import hu.openig.model.Fleet;
import hu.openig.model.FleetTask;
import hu.openig.model.ModelUtils;
import hu.openig.model.Planet;
import hu.openig.model.ResearchSubCategory;
import hu.openig.model.ResearchType;
import hu.openig.utils.U;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
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 ExplorationPlanner extends Planner {
    /** The exploration map. */
    final ExplorationMap exploration;
    /** Set the last satellite deploy date. */
    final Action1<Date> setLastSatelliteDeploy;
    /**
     * 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 setLastSatelliteDeploy set the last satellite deploy date
     */
    public ExplorationPlanner(AIWorld world, AIControls controls, ExplorationMap exploration,
            Action1<Date> setLastSatelliteDeploy) {
        super(world, controls);
        this.exploration = exploration;
        this.setLastSatelliteDeploy = setLastSatelliteDeploy;
    }
    @Override
    public void plan() {
        deploySatellites();
        // find a fleet which has at least a decent radar range
        // and is among the fastest available
        if (!exploration.allowedMap(world.explorationInnerLimit, world.explorationOuterLimit).isEmpty()) {
            // check our current exploration fleets are idle
            List<AIFleet> fs = findFleetsWithTask(FleetTask.EXPLORE, null);
            for (AIFleet f : fs) {
                if (!f.isMoving()) {
                    // move it
                    if (setExplorationTarget(f)) {
                        return;
                    }
                }
            }
            // try recruiting a radar fleet
            AIFleet bestFleet = null;
            for (AIFleet f : findFleetsFor(FleetTask.EXPLORE, null)) {
                if (f.radar >= w.params().fleetRadarUnitSize()) {
                    if (bestFleet == null

                            || (bestFleet.statistics.speed < f.statistics.speed && bestFleet.radar == f.radar)
                            || bestFleet.radar < f.radar) {
                        bestFleet = f;
                    }
                }
            }
            if (bestFleet != null) {
                if (setExplorationTarget(bestFleet)) {
                    return;
                }
            } else {
                // at least find a fleet and move it around
                for (AIFleet f : findFleetsFor(FleetTask.EXPLORE, null)) {
                    if (setExplorationTarget(f)) {
                        return;
                    }
                }
            }
            if (checkExplorerLevel(world.ownFleets)) {
                planDiscoveryFleet();
            }
        } else {
            // yield more when exploration is finished
            if (ModelUtils.random() < 0.10) {
                // exploration complete, set explorers to idle
                for (final AIFleet f : findFleetsWithTask(FleetTask.EXPLORE, null)) {
                    f.task = FleetTask.IDLE;
                    add(new Action0() {
                        @Override
                        public void invoke() {
                            f.fleet.task = FleetTask.IDLE;
                        }
                    });
                }
                for (final AIFleet bf : findFleetsWithTask(FleetTask.PATROL, null)) {
                    if (!bf.isMoving()) {
                        if (world.ownPlanets.size() > 0) {
                            setPatrolTarget(bf);
                        }
                    }
                }
                // if we explored everything, let's patrol
                for (final AIFleet bf : findFleetsFor(FleetTask.PATROL, null)) {
                    if (world.ownPlanets.size() > 0) {
                        setPatrolTarget(bf);
                    }
                }
            }
        }

    }
    /**
     * Produce/Deploy satellites.
     * @return action taken?
     */
    public boolean deploySatellites() {
        if (world.lastSatelliteDeploy != null) {
            if (world.now.getTime() - world.lastSatelliteDeploy.getTime() < 24L * 60 * 60 * 1000) {
                return true;
            }
        }
        List<AIPlanet> survey = new ArrayList<>(world.unknownPlanets);
        survey.addAll(world.enemyPlanets);

        final Point2D.Double center = world.center();

        Collections.sort(survey, new Comparator<AIPlanet>() {
            @Override
            public int compare(AIPlanet o1, AIPlanet o2) {
                if (o1.hasSatelliteOfAI && !o2.hasSatelliteOfAI) {
                    return 1;
                } else
                if (!o1.hasSatelliteOfAI && o2.hasSatelliteOfAI) {
                    return -1;
                }
                double d1 = Math.hypot(o1.planet.x - center.x, o1.planet.y - center.y);
                double d2 = Math.hypot(o2.planet.x - center.x, o2.planet.y - center.y);
                return d1 < d2 ? -1 : (d1 > d2 ? 1 : 0);
            }
        });

        // traverse all known planet and deploy satellites
        outer:
        for (final AIPlanet planet : survey) {
            AIInventoryItem currentSatellite = null;
            for (AIInventoryItem ii : planet.inventory) {
                if (ii.owner == p && ii.type.has(ResearchType.PARAMETER_DETECTOR)

                        && ii.type.category == ResearchSubCategory.SPACESHIPS_SATELLITES) {
                    currentSatellite = ii;
                }
            }
            // find the most advanced satellite
            ResearchType sat = null;
            for (Map.Entry<ResearchType, Integer> e : world.inventory.entrySet()) {
                ResearchType rt = e.getKey();
                int radar = rt.getInt(ResearchType.PARAMETER_DETECTOR, 0);
                if (e.getValue() > 0

                        && rt.category == ResearchSubCategory.SPACESHIPS_SATELLITES
                        && radar > 0) {
                    if (sat == null || sat.getInt(ResearchType.PARAMETER_DETECTOR) < radar) {
                        sat = rt;
                    }
                }
            }
            if (sat != null) {
                // if we couldn't find a better satellite
                if (currentSatellite != null

                        && currentSatellite.type.getInt(ResearchType.PARAMETER_DETECTOR) >= sat.getInt(ResearchType.PARAMETER_DETECTOR)) {
                    continue outer;
                }
                final ResearchType sat0 = sat;
                final Planet planet0 = planet.planet;
                add(new Action0() {
                    @Override
                    public void invoke() {
                        controls.actionDeploySatellite(planet0, sat0);
                    }
                });
                world.lastSatelliteDeploy = world.now;
                setLastSatelliteDeploy.invoke(world.now);
                return true;
            }
            // find the best available detector
            for (ResearchType rt : world.availableResearch) {
                if (rt.has(ResearchType.PARAMETER_DETECTOR)) {
                    if (sat == null || sat.getInt(ResearchType.PARAMETER_DETECTOR) < rt.getInt(ResearchType.PARAMETER_DETECTOR)) {
                        sat = rt;
                    }
                }
            }
            if (sat != null) {
                if (hasFactory(sat)) {
                    placeProductionOrder(sat, 1, false);
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Find the best radar and check if we could build
     * a fleet with even better radar.
     * @param fleets the list of current explorers
     * @return true if better radar available
     */
    boolean checkExplorerLevel(List<AIFleet> fleets) {
        int maxRadar = 0;
        for (AIFleet f : fleets) {
            int rl = f.radarLevel();
            if (rl > maxRadar) {
                maxRadar = rl;
            }
        }
        for (ResearchType rt : world.availableResearch) {
            if (rt.has("radar")

                    && rt.category == ResearchSubCategory.EQUIPMENT_RADARS) {
                if (rt.getInt("radar") > maxRadar) {
                    return true;
                }
            }
            if (fitForExploration(rt)) {
                // check fixed slot ships
                for (EquipmentSlot es : rt.slots.values()) {
                    if (es.fixed) {
                        for (ResearchType rt1 : es.items) {
                            if (rt1.has("radar") && rt1.getInt("radar") > maxRadar) {
                                return true;
                            }
                        }
                    }
                }
            }
        }
        return false;
    }
    /**
     * Set a patrol target for the given fleet.
     * @param bf the fleet
     */
    void setPatrolTarget(final AIFleet bf) {
        AIPlanet p = ModelUtils.random(world.ownPlanets);
        final int x = p.planet.x;
        final int y = p.planet.y;
        bf.task = FleetTask.PATROL;
        add(new Action0() {
            @Override
            public void invoke() {
                if (bf.fleet.task != FleetTask.SCRIPT) {
                    controls.actionMoveFleet(bf.fleet, x, y);
                    bf.fleet.task = FleetTask.PATROL;
                }
            }
        });
    }
    /**
     * Set a new exploration target and task for the given fleet.
     * @param bf the fleet
     * @return true if action taken
     */
    boolean setExplorationTarget(final AIFleet bf) {
        final Point2D.Double center = world.center();
        final Point2D.Double fa = new Point2D.Double(bf.x, bf.y);
        Set<Location> allowed = exploration.allowedMap(world.explorationInnerLimit, world.explorationOuterLimit);
        Comparator<Location> distance = new Comparator<Location>() {
            @Override
            public int compare(Location o1, Location o2) {
                Point2D.Double ol1 = exploration.toMapCenter(o1);
                Point2D.Double ol2 = exploration.toMapCenter(o2);

                double distanceToCenter1 = center.distance(ol1);
                double distanceToCenter2 = center.distance(ol2);
                double distanceToFleet1 = fa.distance(ol1);
                double distanceToFleet2 = fa.distance(ol2);

                /*
                double diff = Math.abs(distanceToFleet1 - distanceToFleet2);
                if (diff < 0.01) {
                    return Double.compare(distanceToCenter1, distanceToCenter2);
                }
                return Double.compare(distanceToCenter1, distanceToCenter2);
                */
                return Double.compare(distanceToCenter1 + distanceToFleet1, distanceToCenter2 + distanceToFleet2);
            }
        };
        Location loc = null;
        final int rl = bf.radarLevel();
        // move to center or edge of the cell?
        double bias = 0.5;
        if (rl > 0 && rl % 2 == 0) {
            bias = 0;

            // remove top segments to avoid leaving the map
            Iterator<Location> it = allowed.iterator();
            while (it.hasNext()) {
                Location loc0 = it.next();
                if (loc0.y == 0 || loc0.x == 0) {
                    it.remove();
                }
            }
            if (allowed.isEmpty()) {
                return false;
            }
        }
        final double fbias = bias;
        if (rl > 0/* && w.random.get().nextDouble() < 0.99*/) {
            outer:
            for (Location loc0 : U.sort(allowed, distance)) {
                for (AIFleet f : world.ownFleets) {
                    if (f.targetPoint != null && f.task == FleetTask.EXPLORE) {
                        double dc = Math.hypot(f.targetPoint.x - (loc0.x + fbias) * exploration.cellWidth,

                                f.targetPoint.y - (loc0.y + fbias) * exploration.cellHeight);
                        if (dc < 1) {
                            continue outer;
                        }
                    }
                }
                // noone targeted this
                loc = loc0;
                break;
            }
            if (loc == null) {
                loc = Collections.min(allowed, distance);
            }
        } else {
            List<Location> ls = U.sort(allowed, distance);
            if (ls.size() > 30) {
                loc = ModelUtils.random(ls.subList(0, 30));
            } else {
                loc = ModelUtils.random(ls);
            }
        }
        final Location floc = loc;
        bf.task = FleetTask.EXPLORE;
        add(new Action0() {
            @Override
            public void invoke() {
                if (bf.fleet.task != FleetTask.SCRIPT) {
                    controls.actionMoveFleet(bf.fleet, (floc.x + fbias) * exploration.cellWidth, (floc.y + fbias) * exploration.cellHeight);
                    bf.fleet.task = FleetTask.EXPLORE;
                }
            }
        });
        return true;
    }
    /**
     * Plan for discovery fleet creation.
     * @return if action taken
     */
    boolean planDiscoveryFleet() {
        if (checkEquipment()) {
            return true;
        }
        if (checkMilitarySpaceport(false)) {
            return true;
        }
        return checkDeploy();
    }
    /**
     * Find the best available radar and ship in inventory, and deploy it.
     * @return true if action taken
     */
    boolean checkDeploy() {
        List<AIPlanet> mss = new ArrayList<>();
        for (final AIPlanet planet : world.ownPlanets) {
            if (planet.statistics.hasMilitarySpaceport) {
                mss.add(planet);
            }
        }
        if (mss.isEmpty()) {
            return false;
        }
        final AIPlanet deploy = ModelUtils.random(mss);
        ResearchType what = findBestFixed();
        if (what == null) {
            what = findBestNormal();
        }
        if (what != null) {
            final ResearchType fwhat = what;
            world.addInventoryCount(fwhat, -1);
            add(new Action0() {
                @Override
                public void invoke() {
                    if (deploy.planet.owner.inventoryCount(fwhat) > 0) {
                        Fleet f = controls.actionCreateFleet(format("explorer_fleet", p.shortName), deploy.planet);
                        f.deployItem(fwhat, p, 1);
                        f.upgradeAll();
                    }
                }
            });
            return true;
        }
        return false;
    }
    /**
     * Find the best regular ship which can be equipped with the best radar.
     * @return the ship choosen
     */
    ResearchType findBestNormal() {
        ResearchType best = null;
        int bestRadar = 0;
        for (Map.Entry<ResearchType, Integer> e : world.inventory.entrySet()) {
            if (e.getValue() == 0) {
                continue;
            }
            ResearchType rt = e.getKey();
            if (!fitForExploration(rt)) {
                continue;
            }
            for (EquipmentSlot es : rt.slots.values()) {
                for (ResearchType rt0 : es.items) {
                    if (rt0.has("radar") && world.inventoryCount(rt0) > 0) {
                        if (bestRadar < rt0.getInt("radar")) {
                            bestRadar = rt0.getInt("radar");
                            best = rt;
                        }
                    }
                }
            }
        }
        return best;
    }
    /**
     * Find the best available, fixed radar ship from the inventory.
     * @return the best found
     */
    ResearchType findBestFixed() {
        // check if we have a ship which has a fixed radar in inventory
        ResearchType bestFixed = null;
        int bestFixedRadar = 0;
        outer0:
        for (Map.Entry<ResearchType, Integer> e : world.inventory.entrySet()) {
            ResearchType rt = e.getKey();
            if (fitForExploration(rt) && e.getValue() > 0) {
                for (EquipmentSlot es : rt.slots.values()) {
                    if (es.fixed) {
                        for (ResearchType rt0 : es.items) {
                            if (rt0.has("radar")) {
                                if (bestFixed == null || bestFixedRadar < rt0.getInt("radar")) {
                                    bestFixed = rt;
                                    bestFixedRadar = rt0.getInt("radar");
                                }
                                continue outer0;
                            }
                        }
                    }
                }
            }
        }
        return bestFixed;
    }
    /**
     * Find the best available, fixed radar ship from the technology tree.
     * @return the best found
     */
    ResearchType findBestFixedTech() {
        // check if we have a ship which has a fixed radar in inventory
        ResearchType bestFixed = null;
        int bestFixedRadar = 0;
        outer0:
        for (ResearchType rt : world.availableResearch) {
            if (fitForExploration(rt)) {
                for (EquipmentSlot es : rt.slots.values()) {
                    if (es.fixed) {
                        for (ResearchType rt0 : es.items) {
                            if (rt0.has("radar")) {
                                if (bestFixed == null || bestFixedRadar < rt0.getInt("radar")) {
                                    bestFixed = rt;
                                    bestFixedRadar = rt0.getInt("radar");
                                }
                                continue outer0;
                            }
                        }
                    }
                }
            }
        }
        return bestFixed;
    }
    /**
     * Check if the given technology should be considered for exploration ship construction.
     * @param rt the technology
     * @return true if applicable
     */
    boolean fitForExploration(ResearchType rt) {
        if (rt.category == ResearchSubCategory.SPACESHIPS_CRUISERS) {
            return true;
        }
        if (world.isAvailable("OrbitalFactory") != null) {
            return rt.category == ResearchSubCategory.SPACESHIPS_BATTLESHIPS;
        }
        return false;
    }
    /**
     * Find the best available ship capable of carrying a radar.
     * @return the best technology
     */
    ResearchType findBestTech() {
        ResearchType best = null;
        int bestFixedRadar = 0;
        outer1:
        for (ResearchType rt : world.availableResearch) {
            if (fitForExploration(rt)) {
                for (EquipmentSlot es : rt.slots.values()) {
                    if (es.fixed) {
                        for (ResearchType rt0 : es.items) {
                            if (rt0.has("radar") && world.isAvailable(rt0)) {
                                if (best == null || bestFixedRadar < rt0.getInt("radar")) {
                                    best = rt;
                                    bestFixedRadar = rt0.getInt("radar");
                                }
                                continue outer1;
                            }
                        }
                    }
                }
            }
        }
        return best;
    }
    /**
     * Check if we have radar in inventory.
     * @return if action taken
     */
    boolean checkEquipment() {
        ResearchType best = findBestFixedTech();
        if (best != null) {
            if (world.inventoryCount(best) > 0) {
                return false;
            }
        } else {
            best = findBestTech();
            if (best != null) {
                if (world.inventoryCount(best) > 0) {
                    return false;
                }
            }
        }
        if (best != null) {
            if (best.has("needsOrbitalFactory")) {
                // check the existence of orbital factory
                if (checkOrbitalFactory()) {
                    return true;
                }
            }
            placeProductionOrder(best, 1, false);
            return true;
        }
        // list available radars
        List<ResearchType> radars = new ArrayList<>();
        for (ResearchType rt : world.availableResearch) {
            if (rt.has("radar") && rt.category == ResearchSubCategory.EQUIPMENT_RADARS) {
                radars.add(rt);
            }
        }
        // list ships capable of carrying radar
        List<ResearchType> ships = new ArrayList<>();
        outer4:
        for (ResearchType rt1 : world.availableResearch) {
            if (fitForExploration(rt1)) {
                for (EquipmentSlot es : rt1.slots.values()) {
                    if (!es.fixed) {
                        for (ResearchType rt2 : es.items) {
                            if (rt2.has("radar") && rt2.category == ResearchSubCategory.EQUIPMENT_RADARS) {
                                ships.add(rt1);
                                continue outer4;
                            }
                        }
                    }
                }
            }
        }
        ResearchType bestRadar = null;

        // find the best radar/ship combination
        for (ResearchType radar : radars) {
            for (ResearchType ship : ships) {
                EquipmentSlot es = ship.supports(radar);
                if (es != null && !es.fixed) {
                    if (bestRadar == null || bestRadar.getInt("radar") < radar.getInt("radar")) {
                        bestRadar = radar;
                    }
                }
            }
        }

        if (bestRadar != null) {
            if (world.inventoryCount(bestRadar) > 0) {
                return checkShip(bestRadar);
            }
            placeProductionOrder(bestRadar, 5, false);
            return true;
        }
        return false;
    }
    /**
     * Check if we have a ship we could deploy a radar to.
     * @param radar the radar to apply
     * @return true if action taken
     */
    boolean checkShip(ResearchType radar) {
        ResearchType bestShip = null;
        for (Map.Entry<ResearchType, Integer> e : world.inventory.entrySet()) {
            if (e.getValue() == 0) {
                continue;
            }
            ResearchType rt = e.getKey();
            if (!fitForExploration(rt)) {
                continue;
            }
            EquipmentSlot es = rt.supports(radar);
            if (es != null && !es.fixed) {
                // find the cheapest
                if (bestShip == null || bestShip.productionCost > rt.productionCost) {
                    bestShip = rt;
                }
            }
        }
        // there is one such
        if (bestShip != null) {
            return false;
        }
        // find the cheapest and produce it
        for (ResearchType rt : world.availableResearch) {
            if (!fitForExploration(rt)) {
                continue;
            }
            // check if supports the given radar
            for (EquipmentSlot es : rt.slots.values()) {
                if (es.items.contains(radar)) {
                    if (bestShip == null || bestShip.productionCost > rt.productionCost) {
                        bestShip = rt;
                        break;
                    }
                }
            }
        }
        if (bestShip != null) {
            if (bestShip.has("needsOrbitalFactory")) {
                // check the existence of orbital factory
                if (checkOrbitalFactory()) {
                    return true;
                }
            }
            // avoid over excessive use of colony ship as explorer
            if (!"ColonyShip".equals(bestShip.id) && ModelUtils.random() < 0.01) {
                placeProductionOrder(bestShip, 1, false);
            }
        }
        return true;
    }
    /**
     * Check if the given research has the necessary factory.
     * @param rt the research
     * @return true if has the necessary factory
     */
    boolean hasFactory(ResearchType rt) {
        switch (rt.factory) {
        case "spaceship":
            return world.global.activeProduction.spaceship > 0;
        case "equipment":
            return world.global.activeProduction.equipment > 0;
        case "weapon":
            return world.global.activeProduction.weapons > 0;
        case "building":
            return !world.ownPlanets.isEmpty();
        default:
            return false;
        }
    }
}
