/**
 *  Copyright (C) 2002-2012   The FreeCol Team
 *
 *  This file is part of FreeCol.
 *
 *  FreeCol is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  FreeCol is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with FreeCol.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.sf.freecol.common.model;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.logging.Logger;

import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.UnitTypeChange.ChangeType;
import net.sf.freecol.common.util.Utils;


/**
 * This class implements the original Colonization combat model.
 * Note that the damage part of any CombatResult is ignored throughout.
 */
public class SimpleCombatModel extends CombatModel {

    private static final Logger logger = Logger.getLogger(SimpleCombatModel.class.getName());

    // The maximum attack power of a Colony's fortifications against a
    // naval unit.
    public static final int MAXIMUM_BOMBARD_POWER = 48;

    public static final String SMALL_MOVEMENT_PENALTY
        = "model.modifier.smallMovementPenalty";
    public static final String BIG_MOVEMENT_PENALTY
        = "model.modifier.bigMovementPenalty";
    public static final String ARTILLERY_IN_THE_OPEN
        = "model.modifier.artilleryInTheOpen";
    public static final String ATTACK_BONUS
        = "model.modifier.attackBonus";
    public static final String FORTIFIED
        = "model.modifier.fortified";
    public static final String ARTILLERY_AGAINST_RAID
        = "model.modifier.artilleryAgainstRaid";
    public static final String AMPHIBIOUS_ATTACK
        = "model.modifier.amphibiousAttack";
    public static final String BOMBARD_BONUS
        = "model.modifier.bombardBonus";
    public static final Modifier UNKNOWN_DEFENCE_MODIFIER
        = new Modifier("bogus", Modifier.UNKNOWN, Modifier.Type.ADDITIVE);

    public SimpleCombatModel() {}


    /**
     * Calculates the odds of success in combat.
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @return The combat odds.
     */
    public CombatOdds calculateCombatOdds(FreeColGameObject attacker,
                                          FreeColGameObject defender) {
        if (attacker == null || defender == null) {
            return new CombatOdds(CombatOdds.UNKNOWN_ODDS);
        }

        float attackPower = getOffencePower(attacker, defender);
        float defencePower = getDefencePower(attacker, defender);
        if (attackPower == 0.0f && defencePower == 0.0f) {
            return new CombatOdds(CombatOdds.UNKNOWN_ODDS);
        }
        float victory = attackPower / (attackPower + defencePower);
        return new CombatOdds(victory);
    }

    /**
     * Get the offensive power of a unit attacking another.
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @return The offensive power.
     */
    public float getOffencePower(FreeColGameObject attacker,
                                 FreeColGameObject defender) {
        float result = 0.0f;
        if (attacker == null) {
            throw new IllegalStateException("Null attacker");
        } else if (combatIsAttackMeasurement(attacker, defender)
            || combatIsAttack(attacker, defender)
            || combatIsSettlementAttack(attacker, defender)) {
            result = FeatureContainer.applyModifierSet(0,
                    attacker.getGame().getTurn(),
                    getOffensiveModifiers(attacker, defender));

        } else if (combatIsBombard(attacker, defender)) {
            Settlement attackerSettlement = (Settlement) attacker;
            if (attackerSettlement.hasAbility("model.ability.bombardShips")) {
                for (Unit unit : attackerSettlement.getTile().getUnitList()) {
                    if (unit.hasAbility(Ability.BOMBARD)) {
                        result += unit.getType().getOffence();
                    }
                }
            }
            if (result > MAXIMUM_BOMBARD_POWER) result = MAXIMUM_BOMBARD_POWER;
        } else {
            throw new IllegalArgumentException("Bogus combat");
        }
        return result;
    }

    /**
     * Get the defensive power wrt an attacker.
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @return The defensive power.
     */
    public float getDefencePower(FreeColGameObject attacker,
                                 FreeColGameObject defender) {
        float result;
        if (combatIsDefenceMeasurement(attacker, defender)
            || combatIsAttack(attacker, defender)
            || combatIsSettlementAttack(attacker, defender)) {
            result = FeatureContainer.applyModifierSet(0,
                    defender.getGame().getTurn(),
                    getDefensiveModifiers(attacker, defender));
        } else if (combatIsBombard(attacker, defender)) {
            result = ((Unit) defender).getType().getDefence();

        } else {
            throw new IllegalArgumentException("Bogus combat");
        }
        return result;
    }

    /**
     * Collect all the offensive modifiers that apply to an attack.
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @return All the applicable offensive modifiers.
     */
    public Set<Modifier> getOffensiveModifiers(FreeColGameObject attacker,
                                               FreeColGameObject defender) {
        Set<Modifier> result = new LinkedHashSet<Modifier>();
        if (attacker == null) {
            throw new IllegalStateException("Null attacker");
        } else if (combatIsAttackMeasurement(attacker, defender)
            || combatIsAttack(attacker, defender)
            || combatIsSettlementAttack(attacker, defender)) {
            Unit attackerUnit = (Unit) attacker;
            UnitType type = attackerUnit.getType();
            result.add(new Modifier(Modifier.OFFENCE,
                                    Specification.BASE_OFFENCE_SOURCE,
                                    type.getOffence(),
                                    Modifier.Type.ADDITIVE));
            result.addAll(attackerUnit
                          .getModifierSet(Modifier.OFFENCE));
            if (defender instanceof Ownable) {
                result.addAll(attackerUnit
                              .getModifierSet(Modifier.OFFENCE_AGAINST,
                                              (Ownable) defender));
            }
            if (attackerUnit.isNaval()) {
                addNavalOffensiveModifiers(attackerUnit, result);
            } else {
                addLandOffensiveModifiers(attacker, defender, result);
            }

        } else if (combatIsBombard(attacker, defender)) {
            result.add(new Modifier("model.modifier.bombardModifier",
                                    getOffencePower(attacker, defender),
                                    Modifier.Type.ADDITIVE));

        } else {
            throw new IllegalArgumentException("Bogus combat");
        }
        return result;
    }

    /**
     * Add all the offensive modifiers that apply to a naval attack.
     *
     * @param attacker The attacker.
     * @param result The set of modifiers to add to.
     */
    private void addNavalOffensiveModifiers(Unit attacker,
                                            Set<Modifier> result) {
        int goodsCount = attacker.getGoodsCount();
        if (goodsCount > 0) {
            // Penalty for every unit of cargo.
            // TODO: shouldn't this be -cargo/capacity?
            // TODO: magic number to spec
            result.add(new Modifier(Modifier.OFFENCE,
                                    Specification.CARGO_PENALTY_SOURCE,
                                    -12.5f * goodsCount,
                                    Modifier.Type.PERCENTAGE));
        }
    }

    /**
     * Add all the offensive modifiers that apply to a land attack.
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @param result The set of modifiers to add to.
     */
    private void addLandOffensiveModifiers(FreeColGameObject attacker,
                                           FreeColGameObject defender,
                                           Set<Modifier> result) {
        Unit attackerUnit = (Unit) attacker;
        Specification spec = attackerUnit.getSpecification();
        // Equipment bonuses
        TypeCountMap<EquipmentType> equip = attackerUnit.getEquipment();
        if (equip != null) {
            for (EquipmentType et : equip.keySet()) {
                result.addAll(et.getModifierSet(Modifier.OFFENCE));
            }
        }
        // Attack bonus
        result.addAll(spec.getModifiers(ATTACK_BONUS));
        // Movement penalty
        int movesLeft = attackerUnit.getMovesLeft();
        if (movesLeft == 1) {
            result.addAll(spec.getModifiers(BIG_MOVEMENT_PENALTY));
        } else if (movesLeft == 2) {
            result.addAll(spec.getModifiers(SMALL_MOVEMENT_PENALTY));
        }

        // Amphibious attack?
        if (combatIsAmphibious(attacker, defender)) {
            result.addAll(spec.getModifiers(AMPHIBIOUS_ATTACK));
        }

        if (combatIsAttackMeasurement(attacker, defender)) {
            ; // No defender information available
        } else if (combatIsSettlementAttack(attacker, defender)) {
            // Settlement present, REF bombardment bonus
            result.addAll(attackerUnit
                          .getModifierSet(BOMBARD_BONUS));
        } else if (combatIsAttack(attacker, defender)) {
            Unit defenderUnit = (Unit) defender;
            Tile tile = defenderUnit.getTile();
            if (tile != null) {
                if (tile.getSettlement() != null) {
                    result.addAll(attackerUnit
                                  .getModifierSet(BOMBARD_BONUS));
                } else {
                    // Ambush bonus in the open = defender's defence
                    // bonus, if defender is REF, or attacker is indian.
                    if (isAmbush(attacker, defender)) {
                        for (Modifier mod : tile.getType()
                                 .getModifierSet(Modifier.DEFENCE)) {
                            Modifier modifier = new Modifier(mod);
                            modifier.setId(Modifier.OFFENCE);
                            modifier.setSource(Specification.AMBUSH_BONUS_SOURCE);
                            result.add(modifier);
                        }
                    }
                    // Artillery in the open penalty, must be on a
                    // tile and not in a settlement.
                    if (attackerUnit.hasAbility(Ability.BOMBARD)
                        && attackerUnit.getLocation() instanceof Tile
                        && attackerUnit.getSettlement() == null) {
                        result.addAll(spec.getModifiers(ARTILLERY_IN_THE_OPEN));
                    }
                }
            }
        } else {
            throw new IllegalStateException("Bogus combat");
        }
    }

    /**
     * Collect all defensive modifiers when defending against an attack.
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @return All the applicable defensive modifiers.
     */
    public Set<Modifier> getDefensiveModifiers(FreeColGameObject attacker,
                                               FreeColGameObject defender) {
        Set<Modifier> result = new LinkedHashSet<Modifier>();
        if (combatIsDefenceMeasurement(attacker, defender)
            || combatIsAttack(attacker, defender)) {
            Unit defenderUnit = (Unit) defender;
            result.add(new Modifier(Modifier.DEFENCE,
                                    Specification.BASE_DEFENCE_SOURCE,
                                    defenderUnit.getType().getDefence(),
                                    Modifier.Type.ADDITIVE));
            result.addAll(defenderUnit.getType()
                          .getModifierSet(Modifier.DEFENCE));
            if (defenderUnit.isNaval()) {
                addNavalDefensiveModifiers(defender, result);
            } else {
                addLandDefensiveModifiers(attacker, defender, result);
            }

        } else if (combatIsSettlementAttack(attacker, defender)) {
            // Not allowed to see inside the settlement.  This only applies 
            // to the pre-combat dialog--- the actual attack is on the
            // unit chosen to defend.
            result.add(UNKNOWN_DEFENCE_MODIFIER);

        } else if (combatIsBombard(attacker, defender)) {
            Unit defenderUnit = (Unit) defender;
            result.add(new Modifier("model.modifier.defenceBonus",
                                    defenderUnit.getType().getDefence(),
                                    Modifier.Type.ADDITIVE));

        } else {
            throw new IllegalArgumentException("Bogus combat");
        }
        return result;
    }

    /**
     * Add all the defensive modifiers that apply to a naval attack.
     *
     * @param defender The defender.
     * @param result The set of modifiers to add to.
     */
    private void addNavalDefensiveModifiers(FreeColGameObject defender,
                                            Set<Modifier> result) {
        Unit defenderUnit = (Unit) defender;
        int goodsCount = defenderUnit.getVisibleGoodsCount();
        if (goodsCount > 0) {
            // Penalty for every unit of cargo.
            // TODO: should this be -cargo/capacity?
            result.add(new Modifier(Modifier.DEFENCE,
                                    Specification.CARGO_PENALTY_SOURCE,
                                    -12.5f * goodsCount,
                                    Modifier.Type.PERCENTAGE));
        }
    }

    /**
     * Add all the defensive modifiers that apply to a land attack.
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @param result The set of modifiers to add to.
     */
    private void addLandDefensiveModifiers(FreeColGameObject attacker,
                                           FreeColGameObject defender,
                                           Set<Modifier> result) {
        Unit defenderUnit = (Unit) defender;
        Specification spec = defender.getSpecification();
        // Auto-equip and equipment bonuses.
        TypeCountMap<EquipmentType> equip = defenderUnit.getEquipment();
        if (equip != null) {
            for (EquipmentType et : equip.keySet()) {
                result.addAll(et.getModifierSet(Modifier.DEFENCE));
            }
        }
        equip = defenderUnit.getAutomaticEquipment();
        if (equip != null) {
            for (EquipmentType et : equip.keySet()) {
                result.addAll(et.getModifierSet(Modifier.DEFENCE));
            }
        }
        // Fortify bonus
        if (defenderUnit.getState() == Unit.UnitState.FORTIFIED) {
            result.addAll(spec.getModifiers(FORTIFIED));
        }
        Tile tile = defenderUnit.getTile();
        if (tile != null) {
            if (tile.getSettlement() == null) { // In the open
                // Terrain defensive bonus.
                if (!isAmbush(attacker, defender)) {
                    result.addAll(tile.getType().getDefenceBonus());
                }
                // Artillery in the Open penalty
                if (defenderUnit.hasAbility(Ability.BOMBARD)
                    && defenderUnit.getState() != Unit.UnitState.FORTIFIED) {
                    result.addAll(spec.getModifiers(ARTILLERY_IN_THE_OPEN));
                }
            } else { // In settlement
                result.addAll(tile.getSettlement()
                              .getModifierSet(Modifier.DEFENCE));
                // Artillery defence bonus against an Indian raid
                if (defenderUnit.hasAbility(Ability.BOMBARD)
                    && attacker != null
                    && ((Unit)attacker).getOwner().isIndian()) {
                    result.addAll(spec.getModifiers(ARTILLERY_AGAINST_RAID));
                }
            }
        }
    }

    /**
     * Generates a result of a unit attacking.
     * Takes care to only call the pseudo-random source *once*.
     *
     * @param random A pseudo-random number source.
     * @param attacker The attacker.
     * @param defender The defender.
     * @return The results of the combat.
     */
    public List<CombatResult> generateAttackResult(Random random,
        FreeColGameObject attacker, FreeColGameObject defender) {
        ArrayList<CombatResult> crs = new ArrayList<CombatResult>();
        CombatOdds odds = calculateCombatOdds(attacker, defender);
        float r = random.nextFloat();
        boolean great = false; // Great win or loss?
        String action;

        if (combatIsAttack(attacker, defender)) {
            Unit attackerUnit = (Unit) attacker;
            Unit defenderUnit = (Unit) defender;
            action = "Attack";

            // For random float 0 <= r < 1.0:
            // Partition this range into wins < odds.win and losses above.
            // Within the 0 <= r < odds.win range, partition the first 10%
            // to be great wins and the rest to be ordinary wins.
            //   r < 0.1 * odds.win  => great win
            //   else r < odds.win   => win
            // Within the odds.win <= r < 1.0 range, partition the first
            // 20% to be evasions (if defender has the evadeAttack ability),
            // the next 70% to be ordinary losses, and the rest great losses.
            //   r < odds.win + 0.2 * (1.0 - odds.win) = 0.8 * odds.win + 0.2
            //     => evade
            //   else r < odds.win + (0.2 + 0.7) * (1.0 - odds.win)
            //     = 0.1 * odds.win + 0.9 => loss
            //   else => great loss
            // ...and beached ships always lose.
            if (r < odds.win || isBeached(defenderUnit)) {
                great = r < 0.1f * odds.win; // Great Win
                crs.add(CombatResult.WIN);
                resolveAttack(attackerUnit, defenderUnit, great,
                    // Rescale to 0 <= r < 1
                    r / (0.1f * odds.win), crs);
            } else if (r < 0.8f * odds.win + 0.2f
                    && defenderUnit.hasAbility("model.ability.evadeAttack")) {
                crs.add(CombatResult.NO_RESULT);
                crs.add(CombatResult.EVADE_ATTACK);
            } else {
                great = r >= 0.1f * odds.win + 0.9f; // Great Loss
                crs.add(CombatResult.LOSE);
                resolveAttack(defenderUnit, attackerUnit, great,
                    // Rescaling to 0 <= r < 1
                    // (rearrange: 0.8 * odds.win + 0.2 <= r < 1.0)
                    (1.25f * r - 0.25f - odds.win)/(1.0f - odds.win), crs);
            }

        } else if (combatIsBombard(attacker, defender)) {
            Unit defenderUnit = (Unit) defender;
            if (!defenderUnit.isNaval()) {
                // One day we might want:
                //   crs.add(CombatResult.SLAUGHTER_UNIT_BOMBARD);
                throw new IllegalStateException("Bombard of non-naval");
            }
            action = "Bombard";

            // The bombard succeeds.
            if (r <= odds.win) {
                crs.add(CombatResult.WIN);

                // Great wins occur at most in 1 in 3 of successful bombards,
                // Good defences reduce this proportion.
                float offencePower = getOffencePower(attacker, defender);
                float defencePower = getDefencePower(attacker, defender);
                float diff = Math.max(3f, defencePower * 2f - offencePower);
                great = r < odds.win / diff;

                // Sink the defender on great wins or lack of repair
                // location, otherwise just damage.
                if (great || defenderUnit.getRepairLocation() == null) {
                    crs.add(CombatResult.SINK_SHIP_BOMBARD);
                } else {
                    crs.add(CombatResult.DAMAGE_SHIP_BOMBARD);
                }

            // The bombard fails but this is not a win for the
            // defender, just an evasion, as it is not currently given
            // an opportunity to return fire.
            } else {
                crs.add(CombatResult.NO_RESULT);
                crs.add(CombatResult.EVADE_BOMBARD);
            }

        } else {
            throw new IllegalStateException("Bogus combat");
        }

        // Log the results so that we have a solid record of combat
        // determinations for debugging and investigation of user
        // `I just lost N combats' complaints.
        List<String> results = new ArrayList<String>();
        for (CombatResult cr : crs) results.add(cr.toString());
        logger.info(attacker.toString() + " " + action
                    + " " + defender.toString()
                    + ": victory=" + Float.toString(odds.win)
                    + " random(1.0)=" + Float.toString(r)
                    + " great=" + Boolean.toString(great)
                    + " => " + Utils.join(" ", results));
        return crs;
    }

    /**
     * Resolve all the consequences of a normal attack.
     *
     * @param winner The winning <code>Unit</code>.
     * @param loser The losing <code>Unit</code>.
     * @param great True if this is a great win/loss.
     * @param r A "residual" random value (for convert/burn mission).
     * @param crs A list of <code>CombatResult</code>s to add to.
     */
    private void resolveAttack(Unit winner, Unit loser, boolean great,
                               float r, List<CombatResult> crs) {
        Player loserPlayer = loser.getOwner();
        Tile tile = loser.getTile();
        Player winnerPlayer = winner.getOwner();
        boolean attackerWon = crs.get(0) == CombatResult.WIN;

        if (loser.isNaval()) {
            // Naval victors get to loot the defenders hold.  Sink the
            // loser on great win/loss, lack of repair location, or
            // beached.
            if (winner.isNaval() && winner.canCaptureGoods()
                && !loser.getGoodsList().isEmpty()) {
                crs.add(CombatResult.LOOT_SHIP);
            }
            if (great
                || loser.getRepairLocation() == null
                || isBeached(loser)) {
                crs.add(CombatResult.SINK_SHIP_ATTACK);
            } else {
                crs.add(CombatResult.DAMAGE_SHIP_ATTACK);
            }

        } else { // loser is land unit
            Settlement settlement = tile.getSettlement();
            EquipmentType autoEquip = null;
            EquipmentType equip = null;
            boolean loserWasUnarmed = !loser.isDefensiveUnit();

            // Autoequip the defender due to Revere?
            if (settlement instanceof Colony) {
                autoEquip = loser.getBestCombatEquipmentType(loser
                        .getAutomaticEquipment());
                if (autoEquip != null) {
                    crs.add(CombatResult.AUTOEQUIP_UNIT);
                    loserWasUnarmed = false;
                }
            }

            // Failed to defend a colony and was not armed.
            if (settlement instanceof Colony && loserWasUnarmed) {
                // A Colony falls to Europeans when the last defender
                // is unarmed.  Natives will pillage if possible but
                // otherwise proceed to kill colonists incrementally
                // until the colony falls for lack of survivors.
                // Ships in a falling colony will be damaged or sunk
                // if they have no repair location.
                Colony colony = (Colony) settlement;
                CombatResult colonyResult = (winnerPlayer.isEuropean())
                    ? CombatResult.CAPTURE_COLONY
                    : (!great && colony.canBePillaged(winner))
                    ? CombatResult.PILLAGE_COLONY
                    : (colony.getUnitCount() > 1
                        || loser.getLocation() == tile)
                    ? CombatResult.SLAUGHTER_UNIT
                    : CombatResult.DESTROY_COLONY;
                if (colonyResult == CombatResult.DESTROY_COLONY) {
                    crs.add(CombatResult.SLAUGHTER_UNIT);
                }
                if (colonyResult == CombatResult.CAPTURE_COLONY
                    || colonyResult == CombatResult.DESTROY_COLONY) {
                    CombatResult shipResult = null;
                    shipResult = (colony.getShipList().isEmpty()) ? null
                        : (colony.getShipList().get(0).getRepairLocation() == null)
                        ? CombatResult.SINK_COLONY_SHIPS
                        : CombatResult.DAMAGE_COLONY_SHIPS;
                    if (shipResult != null) crs.add(shipResult);
                }
                crs.add(colonyResult);

            // Failed to defend a native settlement.
            } else if (settlement instanceof IndianSettlement) {
                // Attacking and defeating the defender of a native
                // settlement with a mission may yield converts but
                // also may provoke the burning of all missions.
                // Native settlements fall when there are no units
                // present either in-settlement or on the settlement
                // tile.
                IndianSettlement is = (IndianSettlement) settlement;
                int lose = 1;
                if (attackerWon) {
                    if (r < winner.getConvertProbability()) {
                        if (is.getUnitCount() + tile.getUnitCount() > 1
                            && is.getMissionary(winnerPlayer) != null
                            && winner.getTile() != null
                            && winner.getTile().isLand()) {
                            crs.add(CombatResult.CAPTURE_CONVERT);
                            lose++;
                        }
                    } else if (r >= 1.0f - winner.getBurnProbability()) {
                        for (IndianSettlement s
                                 : loserPlayer.getIndianSettlements()) {
                            if (s.getMissionary(winnerPlayer) != null) {
                                crs.add(CombatResult.BURN_MISSIONS);
                                break;
                            }
                        }
                    }
                }
                crs.add(CombatResult.SLAUGHTER_UNIT);
                if (settlement.getUnitCount() + tile.getUnitCount() <= lose) {
                    crs.add(CombatResult.DESTROY_SETTLEMENT);
                }

            // Immediately slaughter units with this property.
            } else if (loser.hasAbility("model.ability.disposeOnCombatLoss")) {
                crs.add(CombatResult.SLAUGHTER_UNIT);

            // Disarm losing defensive units.
            } else if ((equip = loser.getBestCombatEquipmentType(
                        loser.getEquipment())) != null) {
                crs.add((loser.losingEquipmentKillsUnit(equip))
                        ? CombatResult.SLAUGHTER_UNIT
                        : (winner.canCaptureEquipment(equip, loser) != null)
                        ? CombatResult.CAPTURE_EQUIP
                        : CombatResult.LOSE_EQUIP);
                if (loser.losingEquipmentDemotesUnit(equip)) {
                    crs.add(CombatResult.DEMOTE_UNIT);
                }

            // Consume autoequipment.
            } else if (settlement instanceof Colony && autoEquip != null) {
                crs.add((loser.losingEquipmentKillsUnit(autoEquip))
                        ? CombatResult.SLAUGHTER_UNIT
                        : (winner.canCaptureEquipment(autoEquip, loser) != null)
                        ? CombatResult.CAPTURE_AUTOEQUIP
                        : CombatResult.LOSE_AUTOEQUIP);

            // Demote units with a demotion available.
            } else if (loser.getTypeChange(ChangeType.DEMOTION, loserPlayer)
                       != null) {
                crs.add(CombatResult.DEMOTE_UNIT);

            // Capture suitable units if the winner is capable.
            } else if (loser.hasAbility(Ability.CAN_BE_CAPTURED)
                       && winner.hasAbility(Ability.CAPTURE_UNITS)
                       && !combatIsAmphibious(winner, loser)) {
                crs.add(CombatResult.CAPTURE_UNIT);

            // Final catch all is just to slaughter.
            } else {
                crs.add(CombatResult.SLAUGHTER_UNIT);
            }
        }

        // Promote great winners or with automatic promotion, if possible.
        UnitTypeChange promotion = winner.getType()
            .getUnitTypeChange(ChangeType.PROMOTION, winnerPlayer);
        if (promotion != null
            && (winner.hasAbility("model.ability.automaticPromotion")
                || (great && (100 * (r - Math.floor(r)) <= promotion.getProbability(ChangeType.PROMOTION))))) {
            crs.add(CombatResult.PROMOTE_UNIT);
        }
    }

    /**
     * Could this attack be an ambush?
     *
     * @param attacker The attacker.
     * @param defender The defender.
     * @return True if the attack can be an ambush.
     */
    private boolean isAmbush(FreeColGameObject attacker,
                             FreeColGameObject defender) {
        return attacker.hasAbility("model.ability.ambushBonus")
            || defender.hasAbility("model.ability.ambushPenalty");
    }

    /**
     * Is a unit a beached ship?
     *
     * @param unit The <code>Unit</code> to test.
     * @return True if the unit is a beached ship.
     */
    private boolean isBeached(Unit unit) {
        return unit.isNaval() && unit.getTile().isLand()
            && unit.getSettlement() == null;
    }

}
