//
// $Id: Unit.m,v 1.9 1997/10/31 04:52:11 nygard Exp $
//

//
//  This file is a part of Empire, a game of exploration and conquest.
//  Copyright (C) 1996  Steve Nygard
//
//  This program 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.
//
//  This program 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 this program; if not, write to the Free Software
//  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
//  You may contact the author by:
//     e-mail:  nygard@telusplanet.net
//

#import "Empire.h"

RCSID ("$Id: Unit.m,v 1.9 1997/10/31 04:52:11 nygard Exp $");

#import "Unit.h"
#import "Map.h"
#import "City.h"
#import "EmpireImageVendor.h"
#import "EmPlayer.h"
#import "Orders.subproj/Orders.h"
#import "GameManager.h" // For berserk

//----------------------------------------------------------------------
// hit points - damage sustained before unit is destroyed.
// speed - number of moves per turn the unit can take.  An attack counts
//         as a move.
// fuel size - fighters have a limited fuel capacity and must be
//             refueled in cities or carriers.
// cargo capacity - number of units this type can hold, or -1 if it
//                  can't carry other units.  Note: There are further
//                  restrictions on what types can be carried.
// damage per hit - damage to enemy for each hit landed.
// moves on land - can this unit move on (or over) land?
// moves on water - can this unit move on (or over) water?
// attack factor - adjusts the probability or a successful attack based
//                 on the unit type.
// defense factor - adjusts the probability or a successful defense
//                  based on the unit type.
// defense factor - adjusts the probability or a successful defense
//                  against bombardment, based on the unit type.
//----------------------------------------------------------------------

typedef struct
{
    int hitPoints;
    int speed; // per turn
    int fuel_size;
    int cargo_capacity;
    int damage_per_hit;
    BOOL moves_on_land;
    BOOL moves_on_water;
    float attack_factor;
    float defense_factor;
    float defense_factor_against_bombardment;
} UnitTypeAttributes;

static UnitTypeAttributes unitTypeAttributes[UNIT_TYPE_COUNT] =
{
    {  0, 0, -1, 0, 0, NO,  NO ,   0,   0,   0 }, // u_unknown
    {  1, 1, -1, 0, 1, YES, NO ,   1,   1, 0.5 }, // u_army
    {  1, 5, 20, 0, 1, YES, YES,   1,   1,   1 }, // u_fighter
    {  3, 2, -1, 6, 1, NO,  YES, 0.5, 0.5, 0.5 }, // u_transport
    {  2, 2, -1, 0, 3, NO,  YES,   1, 0.5, 0.5 }, // u_submarine
    {  3, 3, -1, 0, 1, NO,  YES,   1,   1,   1 }, // u_destroyer
    {  8, 2, -1, 0, 2, NO,  YES,   1,   1,   1 }, // u_cruiser
    {  8, 2, -1, 8, 1, NO,  YES, 0.5,   1,   1 }, // u_carrier
    { 12, 2, -1, 0, 3, NO,  YES,   1,   1,   1 }, // u_battleship
      
    {  1, 5, -1, 0, 1, YES, YES,   0,   0,   0 }, // u_hovercraft
};

//----------------------------------------------------------------------
// This simply keeps track of the number of units of each type produced,
// so that we can give them unique names to each unit. (i.e. Army #7).
//
// Ultimately, there should be a more intelligent name assignment.  For
// example, use lists of names from a text file for ships, and rules
// for armies and fighters.  (i.e. 57th Army, 72nd Fighter).
//----------------------------------------------------------------------

static int _typeCount[UNIT_TYPE_COUNT];

//======================================================================
// Most of the attributes of a unit can be parameterized, so there is
// no need for separate subclasses for each type of unit.
//======================================================================

#define Unit_VERSION 1

@implementation Unit

+ (void) initialize
{
    if (self == [Unit class])
    {
        [self setVersion:Unit_VERSION];
    }
}

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

- initWithUnitType:(UnitType)aUnitType inCity:(City *)producingCity
{
    [super init];

    NSAssert (aUnitType >= u_army && aUnitType <= u_hovercraft, @"Invalid unit type.");

    unitType = aUnitType;

    // We don't "own" the owner, so it is not retained.
    owner = [producingCity owner];

    unitName = [[NSString stringWithFormat:@"%@ #%d", EMUnitTypeName (unitType), ++_typeCount[unitType]] retain];
    unitLocation = [producingCity cityLocation];

    currentHitPoints = unitTypeAttributes[unitType].hitPoints;
    currentRange = unitTypeAttributes[unitType].speed;
    fuel = unitTypeAttributes[unitType].fuel_size;

    order = nil;

    // Hmm.  With more than one game manager, how is the Berserk On/Off menu item handled?
    if ([[owner gameManager] isBerserk] == YES)
    {
        order = [[OExplore alloc] initForUnit:self explore:[owner map]];
    }

    isDestroyed = NO;
    isDoneTurn = NO;

    if (unitTypeAttributes[unitType].cargo_capacity > 0)
        cargo = [[NSMutableArray array] retain];
    else
        cargo = nil;

    onBoard = nil;

    withinCity = producingCity;
    [producingCity unitDidEnter:self];

    return self;
}

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

- (void) dealloc
{
    // Need to notify anything that might be keeping a reference...

    if (withinCity != nil)
        [withinCity unitDidExit:self];

    if (onBoard != nil)
        [onBoard unitDidExit:self];

    SNRelease (unitName);
    SNRelease (order);
    SNRelease (cargo);

    [super dealloc];
}

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

- (NSString *) description
{
    return [NSString stringWithFormat:@"<%@>", [self statusLine]];
}

//======================================================================
// General static values
//======================================================================

- (UnitType) unitType
{
    return unitType;
}

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

- (EmPlayer *) owner
{
    return owner;
}

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

- (Player) playerNumber
{
    return [owner playerNumber];
}

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

- (NSString *) unitName
{
    return unitName;
}

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

- (BOOL) isAShip
{
    return unitType >= u_transport && unitType <= u_battleship;
}

//======================================================================
// Combat information
//======================================================================

- (int) currentHitPoints
{
    return currentHitPoints;
}

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

- (int) maxHitPoints
{
    return unitTypeAttributes[unitType].hitPoints;
}

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

- (int) damagePerHit
{
    return unitTypeAttributes[unitType].damage_per_hit;
}

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

- (float) attackFactor
{
    return unitTypeAttributes[unitType].attack_factor;
}

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

- (float) defenseFactor:(BOOL)bombardment
{
    if (bombardment == YES)
        return unitTypeAttributes[unitType].defense_factor;

    return unitTypeAttributes[unitType].defense_factor_against_bombardment;
}

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

- (BOOL) canBombard
{
    return (unitType == u_cruiser) || (unitType == u_battleship);
}

//======================================================================
// Movement information
//======================================================================

- (int) currentRange
{
    return currentRange;
}

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

- (int) remainingFuel
{
    return fuel;
}

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

- (BOOL) canMoveOnTerrain:(Terrain)terrain
{
    BOOL canMove = YES;

    switch (terrain)
    {
      case t_land:
          canMove = unitTypeAttributes[unitType].moves_on_land;
          break;
    
      case t_water:
          canMove = unitTypeAttributes[unitType].moves_on_water;
          break;
    
      case t_city:
          canMove = NO;
          break;

      default:
          break;
    }

    return canMove;
}

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

- (MovementType) movementType
{
    int movement = 0;

    if (unitTypeAttributes[unitType].moves_on_land == YES)
        movement |= LAND_UNIT;
    if (unitTypeAttributes[unitType].moves_on_water == YES)
        movement |= WATER_UNIT;

    return movement;
}

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

- (EMMapLocation) unitLocation
{
    return unitLocation;
}

//----------------------------------------------------------------------
// Set the new location of a unit.  This takes a look at the map to
// determine if it's entering a city or boarding a ship.
//----------------------------------------------------------------------

- (void) setLocation:(EMMapLocation)newLocation
{
    MapToken mapToken = [[owner map] tokenAtLocation:newLocation];
    Terrain terrain;
    Icon icon;
    UnitType targetUnitType;

    unitLocation = newLocation;

    // We need to update the cargo locations before we set the city.
    if (cargo != nil)
    {
        NSEnumerator *unitEnumerator = [cargo objectEnumerator];
        Unit *unit;

        while (unit = [unitEnumerator nextObject])
        {
            [unit moved];
            [unit takeLocationFromTransport];
        }
    }
    
    // Assuming that the player is us.
    EMMapTokenComponents (mapToken, &terrain, NULL, &icon, NULL);
    targetUnitType = EMConvertIconToUnitType (icon);
       
    if (terrain == t_city)
    {
        [self setCity:[owner cityAtLocation:newLocation]];
    }
    else
        [self setCity:nil];

    if ((unitType == u_army && targetUnitType == u_transport)
        || (unitType == u_fighter && targetUnitType == u_carrier) )
    {
        // What happens if it doesn't fit?  Or is it just assumed to fit by this point?
        [[owner unitWithType:targetUnitType atLocation:newLocation] loadUnit:self];
        //[self setOnBoardShip:[owner unitWithType:targetUnitType atLocation:newLocation]];
    }
    else
    {
        [self setOnBoardShip:nil];
    }
}

//----------------------------------------------------------------------
// If a ship moves, it notifies all of its cargo so they can update
// their location to match that of the ship.
//----------------------------------------------------------------------

- (void) takeLocationFromTransport
{
    if (onBoard != nil)
        unitLocation = [onBoard unitLocation];
}

//======================================================================
// Display
//======================================================================

- (Icon) icon
{
    static const Icon baseIconsForUnitTypes[] = {i_none, i_army, i_fighter, i_unloaded_transport, i_submarine,
                                                 i_destroyer, i_cruiser, i_unloaded_carrier, i_battleship, i_hovercraft};

    Icon icon = baseIconsForUnitTypes[unitType];

    if (unitType == u_army && [self isSentried] == YES)
    {
        icon = i_sentry;
    }
    else if (unitType == u_transport && cargo != nil && [cargo count] > 0)
    {
        icon = i_loaded_transport;
    }
    else if (unitType == u_carrier && cargo != nil && [cargo count] > 0)
    {
        icon = i_loaded_carrier;
    }

    return icon;
}

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

- (NSImage *) iconImage
{
    NSImage *aCity, **onLand, **onWater, **onCity;
    EmpireImageVendor *vendor = [EmpireImageVendor instance];
    NSImage *image = nil;

    [vendor player:[owner playerNumber]:&aCity:&onLand:&onWater:&onCity:NULL];

    switch ([self icon])
    {
      case i_army:
          image = onLand[0];
          break;
    
      case i_sentry:
          image = onLand[1];
          break;
    
      case i_fighter:
          image = onLand[2];
          break;
    
      case i_unloaded_transport:
          image = onWater[1];
          break;
    
      case i_loaded_transport:
          image = onWater[2];
          break;
    
      case i_submarine:
          image = onWater[3];
          break;
    
      case i_destroyer:
          image = onWater[4];
          break;
    
      case i_cruiser:
          image = onWater[5];
          break;
    
      case i_unloaded_carrier:
          image = onWater[6];
          break;
    
      case i_loaded_carrier:
          image = onWater[7];
          break;
    
      case i_battleship:
          image = onWater[8];
          break;
    
      case i_hovercraft:
          image = onWater[9];
          break;
    
      default:
          image = nil;
    }

    return image;
}

//======================================================================
// Order support
//======================================================================

- (void) setOrder:(Order *)newOrder
{
    BOOL s1, s2;
  
    s1 = s2 = NO;

    if (order != nil)
        s1 = [order isSentried];

    if (newOrder != nil)
        s2 = [newOrder isSentried];

    SNRelease (order);
    order = [newOrder retain];

    if ((s1 == YES && s2 == NO) || (s1 == NO && s2 == YES))
    {
        [owner unitIconHasChanged:self];
        [withinCity unitIconHasChanged:self];
    }
}

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

- (Order *) currentOrder
{
    return order;
}

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

- (BOOL) isSentried
{
    return (order != nil) && [order isSentried] == YES;
}

//----------------------------------------------------------------------
// Damaged ships have a reduced cargo capacity.
//----------------------------------------------------------------------

- (int) remainingCargoCapacity
{
    if (cargo == nil)
    {
        return 0;
    }
    else if (unitType == u_transport)
    {
        return 2 * currentHitPoints - [cargo count];
    }
    else if (unitType == u_carrier)
    {
        return currentHitPoints - [cargo count];
    }

    return unitTypeAttributes[unitType].cargo_capacity - [cargo count];
}

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

- (BOOL) isFull
{
    return [self remainingCargoCapacity] == 0;
}

//======================================================================
// Turn handling
//======================================================================

//----------------------------------------------------------------------
// Only ships can repair (other units only have 1 hit point)
//----------------------------------------------------------------------

- (void) repairDamage
{
    if (currentHitPoints < unitTypeAttributes[unitType].hitPoints)
        currentHitPoints++;
}

//----------------------------------------------------------------------
// Only fighters need to refuel...  But only in city/carrier.
//----------------------------------------------------------------------

- (void) refuel
{
    if (unitTypeAttributes[unitType].fuel_size > 0 && (withinCity != nil || onBoard != nil))
        fuel = unitTypeAttributes[unitType].fuel_size;
}

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

- (void) resetRange
{
    if (currentHitPoints <= unitTypeAttributes[unitType].hitPoints / 2)
        currentRange = unitTypeAttributes[unitType].speed / 2;
    else
        currentRange = unitTypeAttributes[unitType].speed;

    isDoneTurn = NO;
}

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

- (void) skipMove
{
    isDoneTurn = YES;
}

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

- (void) useSkippedFuel
{
    fuel -= currentRange;
    currentRange = 0;
}

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

- (void) moved
{
    if (currentRange > 0)
        currentRange--;

    if (fuel > 0)
        fuel--;
  
    if (currentRange == 0)
        isDoneTurn = YES;

    if (order != nil)
        [order unblocked];
}

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

- (BOOL) isFinishedMoving
{
    return isDoneTurn;
}

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

- (void) setUnitFinishedMoving:(BOOL)flag
{
    if (currentRange > 0 && flag == NO)
        isDoneTurn = NO;
    else
        isDoneTurn = YES;
}

//----------------------------------------------------------------------
// If this unit has orders, it will try to execute them.
// Returns YES if it had orders and was able to move, NO otherwise;
//----------------------------------------------------------------------

- (BOOL) tryToMove
{
    BOOL hasMoved = NO;

    if (order != nil)
    {
        Direction dir;

        [order aboutToMove];
        if ([self isFinishedMoving] == NO)
        {
            int count = 9;

            dir = [order nextMove];
            while (dir != d_none && hasMoved == NO && count > 0)
            {
                count--;
                hasMoved = [owner tryToMoveSelectedUnitInDirection:dir];
                if (hasMoved == NO)
                {
                    BOOL tryAgain = [order wasBlocked];
                        
                    if (tryAgain == YES)
                        dir = [order nextMove];
                    else
                        break;
                }
                else
                {
                    [order unblocked];
                }
            }
        }
        else
        {
            isDoneTurn = YES;
            hasMoved = YES;
        }

        if ([order isOrderComplete] == YES)
            [self setOrder:nil];
    }

    if (isDoneTurn == YES)
        hasMoved = YES;

    return hasMoved;
}

//======================================================================
// Human UI support
//======================================================================

- (NSString *) statusLine
{
    NSString *orderText;
    int count;
  
    if (order == nil)
        orderText = @"None";
    else
        orderText = [order orderText];

    count = (cargo == nil) ? 0 : [cargo count];

    if (unitType == u_fighter)
    {
        return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@, Hits: %d/%d, %d/%d%@, Orders: %@",
                         unitLocation.row, unitLocation.column, EMUnitTypeName (unitType), unitName, currentHitPoints,
                         [self maxHitPoints], currentRange, fuel, (isDoneTurn == YES) ? @"*" : @"", orderText];
    }
    else if (unitType == u_transport && count > 0)
    {
        return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@ w/%d %@, Hits: %d/%d, %d, Orders: %@",
                         unitLocation.row, unitLocation.column,
                         EMUnitTypeName (unitType), unitName, count, (count == 1) ? @"army" : @"armies",
                         currentHitPoints, [self maxHitPoints], currentRange, orderText];
    }
    else if (unitType == u_carrier && count > 0)
    {
        return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@ w/%d fighter%@, Hits: %d/%d, %d, Orders: %@",
                         unitLocation.row, unitLocation.column,
                         EMUnitTypeName (unitType), unitName, [cargo count], (count == 1) ? @"" : @"s",
                         currentHitPoints, [self maxHitPoints], currentRange, orderText];
    }

    return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@, Hits: %d/%d, %d, Orders: %@",
                     unitLocation.row, unitLocation.column, EMUnitTypeName (unitType), unitName, currentHitPoints,
                     [self maxHitPoints], currentRange, orderText];
}

//======================================================================
// Commands
//======================================================================

//----------------------------------------------------------------------
// Need to set orders to nil, make the ship wait, and then add any
// unloaded cargo that can move to the units awaing movement list, being
// careful not to add duplicates...
//----------------------------------------------------------------------

- (void) unloadShip
{
    if (cargo != nil)
    {
        int count = [cargo count];
        int l;
        Unit *unit;

        for (l = 0; l < count; l++)
        {
            unit = [cargo objectAtIndex:l];
            [unit setOrder:nil];
            [owner activateThisUnit:unit];
        }
    }
}

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

- (void) destroyUnit:(BOOL)disbanded
{
    Unit *unit;
    
    currentHitPoints = 0;
    isDestroyed = YES;
    // Disbanded to be YES when disbanding an army to create city garrison.
    [self setCity:nil];
    [self setOnBoardShip:nil];

    if (cargo != nil)
    {
        unit = [cargo lastObject];
        while (unit != nil)
        {
            [owner destroyUnit:unit wasDisbanded:NO];
            unit = [cargo lastObject];
        }
    }
}

//----------------------------------------------------------------------
// Returns YES is the unit was loaded aboard this unit, NO otherwise.
//----------------------------------------------------------------------

- (BOOL) loadUnit:(Unit *)aUnit
{
    UnitType cargo_ut = [aUnit unitType];
    Order *old_order;

    if ([self isFull] == NO
        && ( (unitType == u_transport && cargo_ut == u_army)
             || (unitType == u_carrier && cargo_ut == u_fighter) ))
    {
        [aUnit setOnBoardShip:self];

        old_order = [aUnit currentOrder];
        if (old_order == nil || [old_order isOrderComplete] == YES)
        {
            [aUnit setOrder:[[OSentry alloc] initForUnit:aUnit]];
        }

        return YES;
    }

    return NO;
}

//======================================================================
// Combat Support
//======================================================================

- (BOOL) hitForDamage:(int)damage
{
    BOOL wasDestroyed = NO;

    currentHitPoints -= damage;
    if (currentHitPoints <= 0)
    {
        [[self retain] autorelease];
        [owner destroyUnit:self wasDisbanded:NO];
        wasDestroyed = YES;
    }
    else if (cargo != nil)  // Make sure cargo still fits!
    {
        int count = [cargo count];
        int over = 0;
        int l;

        if (unitType == u_transport)
            over = count - 2 * currentHitPoints;
        else if (unitType == u_carrier)
            over = count - currentHitPoints;

        for (l = 0; l < over; l++)
            [owner destroyUnit:[cargo lastObject] wasDisbanded:NO];
    }

    return wasDestroyed;
}

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

- (CombatProfile) combatProfileAgainstBombardment:(BOOL)bombarding
{
    CombatProfile profile;
    int efficiency;

    efficiency = [owner combatEfficiency];

    profile.location = unitLocation;
    profile.player = [owner playerNumber];
    profile.attacking_efficiency = efficiency * [self attackFactor];
    profile.defending_efficiency = efficiency * [self defenseFactor:bombarding];
    profile.damage_per_hit = [self damagePerHit];
    profile.current_hit_points = currentHitPoints;
    profile.adjacent_players = 0; // filled in by someone else
    profile.is_a_city = NO;

    return profile;
}

//======================================================================
// Other
//======================================================================

- (BOOL) isOnBoardShip
{
    return onBoard != nil;
}

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

- (BOOL) isInCity
{
    return withinCity != nil;
}

//----------------------------------------------------------------------
// The current management of releasing destroyed units has to be
// looked at a bit more closely and handled more carefully.
//----------------------------------------------------------------------

- (BOOL) hasBeenDestroyed
{
    return NO;
}

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

- (City *) city
{
    return withinCity;
}

//----------------------------------------------------------------------
// A unit may be within a city, or (possibly) on board a ship, but it
// can't be on both.
//
// When a unit enters a city, all the cargo has it's city set, and therefore
// leaves its transport.
//----------------------------------------------------------------------

- (void) setCity:(City *)aCity
{
    if (withinCity != nil)
    {
        [withinCity unitDidExit:self];
        withinCity = nil;
    }

    if (aCity != nil && onBoard != nil)
    {
        [onBoard unitDidExit:self];
        onBoard = nil;
    }

    if (aCity != nil && cargo != nil)
    {
        Unit *unit = [cargo lastObject];

        while (unit != nil)
        {
            [unit setCity:aCity];
            // Carge removed when the new city is set.
            //[cargo removeLastObject];
            unit = [cargo lastObject];
        }
    }

    withinCity = aCity;

    if (withinCity != nil)
        [withinCity unitDidEnter:self];
}

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

- (Unit *) shipOnBoard
{
    return onBoard;
}

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

- (void) setOnBoardShip:(Unit *)ship
{
    if (onBoard != nil)
    {
        [onBoard unitDidExit:self];
        onBoard = nil;
    }

    if (ship != nil && withinCity != nil)
    {
        [withinCity unitDidExit:self];
        withinCity = nil;
    }

    onBoard = ship;

    if (ship != nil)
        [ship unitDidEnter:self];
}

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

- (void) unitDidExit:(Unit *)aUnit
{
    [cargo removeObject:aUnit];

    if (cargo != nil && [cargo count] == 0)
        [owner unitIconHasChanged:self];
}

//----------------------------------------------------------------------
// Assumed that the movement method has already checked that the unit
// will fit.
//----------------------------------------------------------------------

- (void) unitDidEnter:(Unit *)aUnit
{
    BOOL wasEmpty = [cargo count] == 0;

    [cargo addObject:aUnit];
    
    if (wasEmpty == YES)
        [owner unitIconHasChanged:self];

    [aUnit setOrder:[[OSentry alloc] initForUnit:aUnit]];

    if (order != nil)
    {
        [order unitDidEnter:aUnit];
    }
}

@end
