
package spacewar;

class Ship extends SpaceObject {

    pointcut helmCommandsCut(Ship ship):
        target(ship) && ( call(void rotate(int))     ||
			  call(void thrust(boolean)) ||
			  call(void fire()) );


    /**
     * Energy and Damage are key values in the state of a ship.  Energy is
     * basically about fuel, and damage is about how bad a shape we are in.
     *
     * The energy related values are:
     *   MAX_ENERGY
     *   BULLET_ENERGY
     *   ACCELERATION_ENERGY_FACTOR
     *   energy
     *
     * The damage related values are:
     *   MAX_DAMAGE
     *   BULLET_DAMAGE
     *   COLLISION_DAMAGE_FACTOR
     *   damage
     *
     * Finally, REPAIR_RATE is the rate at which energy is consumed to fix
     * damage.
     *
     */
    private static final int    MAX_ENERGY = 100;
    private static final int    BULLET_ENERGY= 2;
    private static final double ACCELERATION_COST_FACTOR = 0.05;

    //XXX was private
    static final int    MAX_DAMAGE = 100;
    private static final int    BULLET_DAMAGE = 15;
    private static final double COLLISION_DAMAGE_FACTOR = 0.1;

    private static final double REPAIR_RATE = 0.08;


    private static final int    EXPLOSION_LENGTH = 10;

    static final int    BULLET_SPEED = 10;

    static final int    CLOCKWISE = 1;
    static final int    STOP = 0;
    static final int    COUNTERCLOCKWISE = (-1);

    static final double DEFAULT_ANGULAR_VELOCITY = 0.2;
    static final double DEFAULT_ACCELERATION = .4;

    static private final int SIZE = 30;     //Can't be changed for now!!!

    private double    energy;               // range: 0 to MAX_ENERGY
    private double    damage;               // range: 0 to MAX_DAMAGE
    private double    orientation;          // in degrees
    private double    angularVel;           // in ???
    private double    xAcc, yAcc, rAcc;     //
    private int       countdown;            // remaining explosion time

    private Pilot     pilot;

    Ship(Game theGame, double xPos, double yPos, double orientation) {
        super(theGame, xPos, yPos, 0, 0);
        xAcc = 0;
        yAcc = 0;
        this.orientation = orientation;
        angularVel = 0;

        energy = MAX_ENERGY;
        damage = 0;
        countdown = EXPLOSION_LENGTH;
    }


    int getSize()            { return SIZE; }

    double getEnergy()       { return energy; }
    double getDamage()       { return damage; }
    double getOrientation()  { return orientation; }
    double getRAcc()         { return rAcc; }

    Pilot getPilot()         { return pilot; }
    void  setPilot (Pilot p) { pilot = p; }

    float getEnergyLevel() {
        return (float)energy / (float)MAX_ENERGY;
    }
    float getDamageLevel() {
        return (float)damage / (float)MAX_DAMAGE;
    }

    // returns false if energy is out, otherwise decrements energy by amount
    // and returns true
    boolean expendEnergy(double amount) {
        if (amount <= energy) {
            energy -= amount;
            return true;
        }
        else
            return false;
    }

    // increments damage by amount and handles the destruction of a ship if
    // damage reaches MAX_DAMAGE.
    void inflictDamage(double amount) {
        if (amount < 0)     // shouldn't happen
            return;
        damage = Math.min(MAX_DAMAGE, damage + amount);
        if (damage == MAX_DAMAGE)
            setIsAlive(false);
    }

    void repairDamage(double amount) {
        if (amount < 0)     // shouldn't happen
            return;
        if (damage == 0)
            return;
        damage = Math.max(0, damage - amount);
    }

    public void clockTick() {
        if (! isAlive())  {
            //
            // If we aren't alive, but we are still in the registry, it means
            // we are exploding.  countdown counts the length of the explosion.
            //
            if (--countdown == 0)
                die();
        }
        else {
            if (angularVel != 0) {
                orientation += angularVel;
                xAcc = rAcc * Math.cos(orientation);
                yAcc = rAcc * Math.sin(orientation);
            }
            setXVel(getXVel() + xAcc);
            setYVel(getYVel() + yAcc);

            //expend energy
            if (!expendEnergy(rAcc * ACCELERATION_COST_FACTOR))
                rAcc = xAcc = yAcc = 0;

            // fix damage
            if (energy > 10 && damage > REPAIR_RATE) {
                expendEnergy(REPAIR_RATE);
                repairDamage(REPAIR_RATE);
            }
        }
        super.clockTick();
    }

    /**
     * First check to make sure we have enough energy to accelerate.  If
     * we do, then go ahead and do so.  Acceleration is in the direction
     * we are already facing (i.e. orientation).
     */
    void setAcceleration(double acc) {
        if (acc * ACCELERATION_COST_FACTOR <= energy) {
            rAcc = acc;
            xAcc = rAcc * Math.cos(orientation);
            yAcc = rAcc * Math.sin(orientation);
        }
    }

    void setAngularVelocity(double omega) {
        // changing direction of rotation takes energy
        if (!expendEnergy(Math.abs(omega - angularVel) / 2))
            return;
        //sets amount of degree rotation per clock tick, in radians;
        //clockwise is positive
        angularVel = omega;
    }

    void rotate(int direction) {
        setAngularVelocity(
          direction == CLOCKWISE        ? DEFAULT_ANGULAR_VELOCITY :
          direction == COUNTERCLOCKWISE ? -DEFAULT_ANGULAR_VELOCITY :
          0);
    }

    void thrust(boolean onOff) {
        setAcceleration(onOff ? DEFAULT_ACCELERATION : 0);
    }

    void fire() {
        // firing a shot takes energy
        if (!expendEnergy(BULLET_ENERGY))
            return;

        //create a bullet object so it doesn't hit the ship that's firing it
        double xV = getXVel() + BULLET_SPEED * (Math.cos(orientation));
        double yV = getYVel() + BULLET_SPEED * (Math.sin(orientation));

        // create the actual bullet
        new Bullet(
         getGame(),
         (getXPos() + ((getSize()/2 + 2) * (Math.cos(orientation))) + xV),
         (getYPos() + ((getSize()/2 + 2) * (Math.sin(orientation))) + yV),
         xV,
         yV);
    }


    void handleCollision(SpaceObject obj) {
        if (obj instanceof Ship) {
            // should never be called. ship - ship collisions are handled in
            // Ship.bounce(Ship shipA, Ship shipB)
        }
        else if (obj instanceof Bullet) {
            inflictDamage(BULLET_DAMAGE);
        }
        else if (obj instanceof EnergyPacket) {
            double packetEnergy = ((EnergyPacket)obj).getEnergy();
            energy = Math.max(0, Math.min(energy + packetEnergy, MAX_ENERGY));
        }
        else {
            System.err.println("collision with UFO!");
        }
    }

    static void bounce(Ship shipA, Ship shipB) {
        double  dx, dy, denominator,
            xAccA, yAccA, xAccB, yAccB, damage,
            xComp, yComp, dvx, dvy;

        dx = Math.abs(shipA.getXPos() - shipB.getXPos());
        dy = Math.abs(shipA.getYPos() - shipB.getYPos());
        denominator = Math.sqrt(dx * dx + dy * dy);
        xComp = dx / denominator;
        yComp = dy / denominator;
        xAccA = shipB.getXVel() * xComp + shipA.getXVel() * (1 - xComp) -
            shipA.getXVel();
        yAccA = shipB.getYVel() * yComp + shipA.getYVel() * (1 - yComp) -
            shipA.getYVel();
        xAccB = shipA.getXVel() * xComp + shipB.getXVel() * (1 - xComp) -
            shipB.getXVel();
        yAccB = shipA.getYVel() * yComp + shipB.getYVel() * (1 - yComp) -
            shipB.getYVel();
        shipA.accelerate(xAccA, yAccA);
        shipB.accelerate(xAccB, yAccB);
        dvx = shipA.getXVel() - shipB.getXVel();
        dvy = shipA.getYVel() - shipA.getYVel();
        damage = COLLISION_DAMAGE_FACTOR * (dvx * dvx + dvy * dvy);
        shipA.inflictDamage(damage);
        shipB.inflictDamage(damage);

        // !!!
        // !!! poopers!  this does a local time warp.  this has to be a
        // !!! violation of the clockTick protocol
        // !!!
        while (Game.isCollision(shipA, shipB)) {
            shipA.clockTick();
            shipB.clockTick();
        }
    }
}
