/*
 * MegaMek - Copyright (C) 2003, 2004 Ben Mazur (bmazur@sev.org)
 * 
 *  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.
 */

package megamek.common.xml;

import gd.xml.tiny.ParsedXML;
import megamek.common.AmmoType;
import megamek.common.BattleArmor;
import megamek.common.BipedMech;
import megamek.common.Coords;
import megamek.common.CriticalSlot;
import megamek.common.Entity;
import megamek.common.GunEmplacement;
import megamek.common.IArmorState;
import megamek.common.IGame;
import megamek.common.Infantry;
import megamek.common.InfernoTracker;
import megamek.common.Mech;
import megamek.common.Mounted;
import megamek.common.Pilot;
import megamek.common.Player;
import megamek.common.Protomech;
import megamek.common.QuadMech;
import megamek.common.Tank;
import megamek.common.TechConstants;

import java.io.IOException;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;

/**
 * Objects of this class can encode a <code>Entity</code> object as XML
 * into an output writer and decode one from a parsed XML node.  It is used
 * when saving games into a version- neutral format.
 *
 * @author      James Damour <suvarov454@users.sourceforge.net>
 */
public class EntityEncoder {

    /**
     * Encode a <code>Entity</code> object to an output writer.
     *
     * @param   entity - the <code>Entity</code> to be encoded.
     *          This value must not be <code>null</code>.
     * @param   out - the <code>Writer</code> that will receive the XML.
     *          This value must not be <code>null</code>.
     * @throws  IllegalArgumentException if the entity is
     *          <code>null</code>.
     * @throws  IOException if there's any error on write.
     */
    public static void encode( Entity entity, Writer out )
        throws IOException
    {
        Enumeration iter; // used when marching through a list of sub-elements
        Coords coords;
        int turns;
        String substr;

        // First, validate our input.
        if ( null == entity ) {
            throw new IllegalArgumentException( "The entity is null." );
        }
        if ( null == out ) {
            throw new IllegalArgumentException( "The writer is null." );
        }

        // Make sure any transported entities are written first.
        iter = entity.getLoadedUnits().elements();
        while( iter.hasMoreElements() ) {
            EntityEncoder.encode( (Entity) iter.nextElement(), out );
        }

        // Start writing this entity to the file.
        out.write( "<entity chassis=\"" );
        out.write( entity.getChassis() );
        out.write( "\" model=\"" );
        out.write( entity.getModel() );
        out.write( "\" type=\"" );
        out.write( entity.getMovementModeAsString() );
        out.write( "\" typeVal=\"" );
        out.write( String.valueOf(entity.getMovementMode()) );
        out.write( "\" techBase=\"" );
        out.write ( entity.getTechLevel() + ":" + TechConstants.getLevelName(entity.getTechLevel()) );
        out.write( "\" year=\"" );
        out.write( String.valueOf(entity.getYear()) );
        out.write( "\" mass=\"" );
        out.write( String.valueOf(entity.getWeight()) );
        out.write( "\" walkMp=\"" );
        out.write( String.valueOf(entity.getOriginalWalkMP()) );
        out.write( "\" jumpMp=\"" );
        out.write( String.valueOf(entity.getOriginalJumpMP()) );
        out.write( "\">" );

        // Add the crew this entity.
        final Pilot crew = entity.getCrew();
        out.write( "<pilot name=\"" );
        out.write( crew.getName() );
        out.write( "\" gunnery=\"" );
        out.write( String.valueOf(crew.getGunnery()) );
        out.write( "\" piloting=\"" );
        out.write( String.valueOf(crew.getPiloting()) );
        if ( crew.isDead() || crew.getHits() > 5 ) {
            out.write( "\" hits=\"Dead" );
        }
        else if ( crew.getHits() > 0 ) {
            out.write( "\" hits=\"" );
            out.write( String.valueOf(crew.getHits()) );
        }
        if ( crew.countAdvantages() > 0 ) {
            out.write( "\" advantages=\"" );
            out.write( String.valueOf(crew.getAdvantageList(" ")) );
        }
        out.write( "\"/>" );

        // Write the game-specific data.
        out.write( "<entityData gameId=\"" );
        out.write( String.valueOf(entity.getId()) );
        out.write( "\" externalId=\"" );
        out.write( String.valueOf(entity.getExternalId()) );
        out.write( "\" ownerId=\"" );
        out.write( String.valueOf(entity.getOwnerId()) );
        out.write( "\" facing=\"" );
        out.write( String.valueOf(entity.getFacing()) );
        out.write( "\" secondaryFacing=\"" );
        out.write( String.valueOf(entity.getSecondaryFacing()) );
        out.write( "\" walkMpCurrent=\"" );
        out.write( String.valueOf(entity.getWalkMP()) );
        out.write( "\" isOmni=\"" );
        out.write( entity.isOmni() ? "true" : "false" );
        out.write( "\" jumpMpCurrent=\"" );
        out.write( String.valueOf(entity.getJumpMP()) );
        out.write( "\" C3MasterId=\"" );
        out.write( String.valueOf(entity.getC3MasterId()) );
        out.write( "\" transportId=\"" );
        out.write( String.valueOf(entity.getTransportId()) );
        out.write( "\" swarmTargetId=\"" );
        out.write( String.valueOf(entity.getSwarmTargetId()) );
        out.write( "\" swarmAttackerId=\"" );
        out.write( String.valueOf(entity.getSwarmAttackerId()) );
        out.write( "\" removalCondition=\"" );
        out.write( String.valueOf(entity.getRemovalCondition()) );
        out.write( "\" deployRound=\"" );
        out.write( String.valueOf(entity.getDeployRound()) );

        out.write( "\" isShutDown=\"" );
        out.write( entity.isShutDown() ? "true" : "false" );
        out.write( "\" isDoomed=\"" );
        out.write( entity.isDoomed() ? "true" : "false" );
        out.write( "\" isDestroyed=\"" );
        out.write( entity.isDestroyed() ? "true" : "false" );
        out.write( "\" isDone=\"" );
        out.write( entity.isDone() ? "true" : "false" );
        out.write( "\" isProne=\"" );
        out.write( entity.isProne() ? "true" : "false" );
        out.write( "\" isFindingClub=\"" );
        out.write( entity.isFindingClub() ? "true" : "false" );
        out.write( "\" isArmsFlipped=\"" );
        out.write( entity.getArmsFlipped() ? "true" : "false" );
        out.write( "\" isUnjammingRAC=\"" );
        out.write( entity.isUnjammingRAC() ? "true" : "false" );
        out.write( "\" isSpotting=\"" );
        out.write( entity.isSpotting() ? "true" : "false" );
        out.write( "\" isClearingMinefield=\"" );
        out.write( entity.isClearingMinefield() ? "true" : "false" );
        out.write( "\" isSalvage=\"" );
        out.write( entity.isSalvage() ? "true" : "false" );
        out.write( "\" isDeployed=\"" );
        out.write( entity.isDeployed() ? "true" : "false" );
        out.write( "\" isUnloadedThisTurn=\"" );
        out.write( entity.isUnloadedThisTurn() ? "true" : "false" );

        out.write( "\" heat=\"" );
        out.write( String.valueOf(entity.heat) );
        out.write( "\" heatBuildup=\"" );
        out.write( String.valueOf(entity.heatBuildup) );
        out.write( "\" delta_distance=\"" );
        out.write( String.valueOf(entity.delta_distance) );
        out.write( "\" mpUsed=\"" );
        out.write( String.valueOf(entity.mpUsed) );
        out.write( "\" moved=\"" );
        out.write( String.valueOf(entity.moved) );
        out.write( "\" damageThisPhase=\"" );
        out.write( String.valueOf(entity.damageThisPhase) );
        out.write( "\" engineHitsThisRound=\"" );
        out.write( String.valueOf(entity.engineHitsThisRound) );
        out.write( "\" rolledForEngineExplosion=\"" );
        out.write( entity.rolledForEngineExplosion ? "true" : "false" );
        out.write( "\" dodging=\"" );
        out.write( entity.dodging ? "true" : "false" );
        out.write( "\" >" );

        // Now save the entity's coordinates.
        coords = entity.getPosition();
        if ( null != coords ) CoordsEncoder.encode( coords, out );

        // Is the entity performing a displacement attack?
        if ( entity.hasDisplacementAttack() ) {
            EntityActionEncoder.encode( entity.getDisplacementAttack(), out );
        }

        // Add the narc pods attached to this entity (if any are needed).
        substr = getNarcString( entity );
        if ( null != substr ) out.write( substr );

        // Encode the infernos burning on this entity.
        if ( entity.infernos.isStillBurning() ) {
            // Encode the infernos on this entity.
            out.write( "<inferno>" );
            turns = entity.infernos.getArrowIVTurnsLeftToBurn();
            // This value may be zero.
            if ( turns > 0 ) {
                out.write( "<arrowiv turns=\"" );
                out.write( String.valueOf(turns) );
                out.write( "\" />" );
            }
            // -(Arrow IV turns - All Turns) = Standard Turns.
            turns -= entity.infernos.getTurnsLeftToBurn();
            turns = -turns;
            if ( turns > 0 ) {
                out.write( "<standard turns=\"" );
                out.write( String.valueOf(turns) );
                out.write( "\" />" );
            }
            out.write( "</inferno>" );
        }

        // Do we have any transporters?
        String transporters = Entity.encodeTransporters( entity );
        if ( null != transporters && 0 == transporters.length() ) {
            out.write( "<transporters value=\"" );
            out.write( transporters );
            out.write( "\" />" );
        }

        // Record the IDs of all transported units (if any).
        iter = entity.getLoadedUnits().elements();
        if ( iter.hasMoreElements() ) {
            out.write( "<loadedUnits>" );
            while ( iter.hasMoreElements() ) {
                Entity loaded = (Entity) iter.nextElement();
                out.write( "<entityRef gameId=\"" );
                out.write( String.valueOf(loaded.getId()) );
                out.write( "\" />" );
            }
            out.write( "</loadedUnits>" );
        }

        // Handle sub-classes of Entity.
        out.write( "<class name=\"" );
        if ( entity instanceof BipedMech ) {
            out.write( "BipedMech\">" );
            BipedMechEncoder.encode( entity, out );
        }
        else if ( entity instanceof QuadMech ) {
            out.write( "QuadMech\">" );
            QuadMechEncoder.encode( entity, out );
        }
        else if ( entity instanceof Tank ) {
            out.write( "Tank\">" );
            TankEncoder.encode( entity, out );
        }
        else if ( entity instanceof BattleArmor ) {
            out.write( "BattleArmor\">" );
            BattleArmorEncoder.encode( entity, out );
        }
        else if ( entity instanceof Infantry ) {
            out.write( "Infantry\">" );
            InfantryEncoder.encode( entity, out );
        }
        else if ( entity instanceof Protomech ) {
            out.write( "Protomech\">" );
            ProtomechEncoder.encode( entity, out );
        }
        else if ( entity instanceof GunEmplacement ) {
            out.write( "GunEmplacement\">" );
            GunEmplacementEncoder.encode( entity, out );
        }
        else {
            throw new IllegalStateException
                ( "Unexpected entity type " + entity.getClass().getName() );
        }
        out.write( "</class>" );
        
        // Finish the game-specific data.
        out.write( "</entityData>" );

        // Encode this unit's equipment.
        Iterator iter2 = entity.getEquipment().iterator();
        if ( iter2.hasNext() ) {
            out.write( "<entityEquipment>" );
            int index = 0;
            while ( iter2.hasNext() ) {
                substr = EntityEncoder.formatEquipment
                    ( index, (Mounted) iter2.next(), entity );
                if ( null != substr ) out.write( substr );
                index++;
            }
            out.write( "</entityEquipment>" );
        }

        // Add the locations of this entity (if any are needed).
        substr = getLocString( entity );
        if ( null != substr ) out.write( substr );

        // Finish the XML stream for this entity.
        out.write( "</entity>" );
    }

    /**
     * Produce a string describing all NARC pods on the entity.
     *
     * @param   entity - the <code>Entity</code> being examined.
     *          This value may be <code>null</code>.
     * @return  a <code>String</code> describing the equipment.
     *          This value may be <code>null</code>.
     */
    private static String getNarcString( Entity entity ) {
        // null in, null out
        if ( null == entity ) return null;

        // Show all teams that have NARCed the entity.
        StringBuffer output = new StringBuffer();
        boolean narced = false;
        for ( int team = Player.TEAM_NONE; team < Player.MAX_TEAMS; team++ ) {
            if ( entity.isNarcedBy( team ) ) {

                // Is this the first narc on the entity?
                if ( !narced ) {
                    output.append( "<narcs>" );
                    narced = true;
                }

                // Add this team to the NARC pods.
                output.append( "<narc type=\"Standard\" team=\"" );
                output.append( String.valueOf( team ) );
                output.append( "\" />" );
            }
        }

        // If the entity wasn't narced, return a null.
        if ( !narced ) return null;

        // Finish off this section, and return the string.
        output.append( "</narcs>" );
        return output.toString();
    }

    /**
     * Produce a string describing a piece of equipment.
     *
     * @param   index - the <code>int</code> index of this equipment
     *          on the given entity.
     * @param   mount - the <code>Mounted</code> object of the equipment.
     *          This value should not be <code>null</code>.
     * @param   entity - the <code>Entity</code> that has this mount.
     * @return  a <code>String</code> describing the equipment.
     *          This value will be <code>null</code> if a <code>null</code>
     *          was passed.
     */
    private static String formatEquipment( int index, Mounted mount,
                                           Entity entity ) {
        StringBuffer output = new StringBuffer();

        // null in, null out.
        if ( null == mount ) return null;

        // Format this piece of equipment.
        output.append( "<equipment index=\"" );
        output.append( String.valueOf(index) );
        output.append( "\" type=\"" );
        output.append( mount.getType().getInternalName() );
        output.append( "\" location=\"" );
        output.append( String.valueOf(mount.getLocation()) );
        output.append( "\" isRear=\"" );
        output.append( mount.isRearMounted() ? "true" : "false" );
        if ( mount.getType() instanceof AmmoType ) {
            output.append( "\" shots=\"" );
            output.append( String.valueOf(mount.getShotsLeft()) );
        }
        output.append( "\" curMode=\"" );
        output.append( mount.curMode().getName() );
        output.append( "\" pendingMode=\"" );
        output.append( mount.pendingMode().getName() );
        output.append( "\" linkedRef=\"" );
        if ( null == mount.getLinked() ) {
            output.append( "N/A" );
        } else {
            output.append( String.valueOf( entity.getEquipmentNum
                                           (mount.getLinked()) ) );
        }
        output.append( "\" foundCrits=\"" );
        output.append( String.valueOf(mount.getFoundCrits()) );

        output.append( "\" isUsedThisRound=\"" );
        output.append( mount.isUsedThisRound() ? "true" : "false" );
        output.append( "\" isBreached=\"" );
        output.append( mount.isBreached() ? "true" : "false" );
        output.append( "\" isHit=\"" );
        output.append( mount.isHit() ? "true" : "false" );
        output.append( "\" isDestroyed=\"" );
        output.append( mount.isDestroyed() ? "true" : "false" );
        output.append( "\" isMissing=\"" );
        output.append( mount.isMissing() ? "true" : "false" );
        output.append( "\" isJammed=\"" );
        output.append( mount.isJammed() ? "true" : "false" );
        output.append( "\" isPendingDump=\"" );
        output.append( mount.isPendingDump() ? "true" : "false" );
        output.append( "\" isDumping=\"" );
        output.append( mount.isDumping() ? "true" : "false" );
        output.append( "\" isSplit=\"" );
        output.append( mount.isSplit() ? "true" : "false" );
        output.append( "\" isFired=\"" );
        output.append( mount.isFired() ? "true" : "false" );
        output.append( "\"/>" );

        // Return a String.
        return output.toString();
    }

    /**
     * Produce a string describing the equipment in a critical slot.
     *
     * @param   slot - the <code>CriticalSlot</code> being encoded.
     * @param   mount - the <code>Mounted</code> object of the equipment.
     *          This value should be <code>null</code> for a slot with
     *          system equipment.
     * @return  a <code>String</code> describing the slot.
     */
    private static String formatSlot( CriticalSlot slot, Mounted mount ) {
        StringBuffer output = new StringBuffer();

        // Don't forget... slots start at index 1.
        output.append( "<slot index=\"" );
        output.append( String.valueOf(slot.getIndex()+1) );
        output.append( "\" type=\"" );
        if ( mount == null ) {
            output.append( "System" );
        } else {
            output.append( mount.getType().getInternalName() );
            if ( mount.isRearMounted() ) {
                output.append( "\" isRear=\"true" );
            }
            if ( mount.getType() instanceof AmmoType ) {
                output.append( "\" shots=\"" );
                output.append( String.valueOf
                                (mount.getShotsLeft()) );
            }
        }
        output.append( "\" isHit=\"" );
        output.append( slot.isHit() ? "true" : "false" );
        output.append( "\" isDestroyed=\"" );
        output.append( slot.isDestroyed() ? "true" : "false" );
        output.append( "\" isMissing=\"" );
        output.append( slot.isMissing() ? "true" : "false" );
        output.append( "\" isBreached=\"" );
        output.append( slot.isBreached() ? "true" : "false" );
        output.append( "\" isHittable=\"" );
        output.append( slot.isEverHittable() ? "true" : "false" );
        output.append( "\"/>" );

        // Return a String.
        return output.toString();
    }

    /**
     * Helper function that generates a string identifying the state of
     * the locations for an entity.
     *
     * @param   entity - the <code>Entity</code> whose location state is needed
     */
    private static String getLocString( Entity entity ) {
        boolean isMech = entity instanceof Mech;
        StringBuffer output = new StringBuffer();

        // Walk through the locations for the entity,
        // and only record damage and ammo.
        for ( int loc = 0; loc < entity.locations(); loc++ ) {

            // Add this location to the output string.
            output.append( "<location index=\"" );
            output.append( String.valueOf(loc) );
            output.append( "\"> " );

            // Record values of armor and internal structure,
            // unless the section never has armor.
            if ( entity.getOInternal(loc) != IArmorState.ARMOR_NA ) {
                output.append( "<armor points=\"" );
                output.append( String.valueOf(entity.getArmor(loc)) );
                output.append( "\"/>" );
                output.append( "<armor points=\"" );
                output.append( String.valueOf(entity.getInternal(loc)) );
                output.append( "\" type=\"Internal\"/>" );
                if ( entity.hasRearArmor(loc) ) {
                    output.append( "<armor points=\"" );
                    output.append(String.valueOf(entity.getArmor(loc, true)));
                    output.append( "\" type=\"Rear\"/>" );
                }
            }

            // Walk through the slots in this location.
            for ( int loop = 0; loop < entity.getNumberOfCriticals(loc);
                  loop++ ) {

                // Get this slot.
                CriticalSlot slot = entity.getCritical( loc, loop );

                // Did we get a slot?
                if ( null == slot ) {

                    // Nope.  Record missing actuators on Biped Mechs.
                    if ( isMech && !entity.entityIsQuad() &&
                         ( loc == Mech.LOC_RARM || loc == Mech.LOC_LARM ) &&
                         ( loop == 2 || loop == 3 ) ) {
                        output.append( "<slot index=\"" );
                        output.append( String.valueOf(loop+1) );
                        output.append( "\" type=\"Empty\"/>" );
                    }

                } else {

                    // Yup.  If the equipment isn't a system, get it.
                    Mounted mount = null;
                    if ( CriticalSlot.TYPE_EQUIPMENT == slot.getType() ) {
                        mount = entity.getEquipment( slot.getIndex() );
                    }

                    // Format the slot.
                    output.append( formatSlot( slot, mount ) );

                } // End have-slot

            } // Check the next slot in this location

            // Tanks don't have slots, and Protomechs only have
            // system slots, so we have to handle their ammo specially.
            if ( entity instanceof Tank ||
                 entity instanceof Protomech ) {
                for (Mounted mount : entity.getAmmo()) {

                    // Is this ammo in the current location?
                    if ( mount.getLocation() == loc ) {
                        output.append( "<slot index=\"N/A\" type=\"" );
                        output.append( mount.getType().getInternalName() );
                        output.append( "\" shots=\"" );
                        output.append( String.valueOf(mount.getShotsLeft()) );
                        output.append( "\"/>" );
                    }

                } // Check the next ammo.

            } // End is-tank-or-proto

            // Finish off the location
            output.append( "</location>" );

        } // Handle the next location

        // Convert the output into a String and return it.
        return output.toString();

    } // End private static String getLocString( Entity )

    /**
     * Helper function to decode the pilot (crew) of an <code>Entity</code>
     * object from the passed node.
     *
     * @param   node - the <code>ParsedXML</code> node for this object.
     *          This value must not be <code>null</code>.
     * @param   entity - the <code>Entity</code> the decoded object belongs to.
     * @throws  IllegalArgumentException if the node is
     *          <code>null</code>.
     * @throws  IllegalStateException if the node does not
     *          contain a valid <code>Entity</code>.
     */
    private static void decodePilot( ParsedXML node, Entity entity ) {
        // TODO : implement me
    }

    /**
     * Helper function to decode the equipment of an <code>Entity</code>
     * object from the passed node.
     *
     * @param   node - the <code>ParsedXML</code> node for this object.
     *          This value must not be <code>null</code>.
     * @param   entity - the <code>Entity</code> the decoded object belongs to.
     * @throws  IllegalArgumentException if the node is
     *          <code>null</code>.
     * @throws  IllegalStateException if the node does not
     *          contain a valid <code>Entity</code>.
     */
    private static void decodeEntityEquipment( ParsedXML node, Entity entity ) {
        // TODO : implement me
    }

    /**
     * Helper function to decode a location of an <code>Entity</code>
     * object from the passed node.
     *
     * @param   node - the <code>ParsedXML</code> node for this object.
     *          This value must not be <code>null</code>.
     * @param   entity - the <code>Entity</code> the decoded object belongs to.
     * @throws  IllegalArgumentException if the node is
     *          <code>null</code>.
     * @throws  IllegalStateException if the node does not
     *          contain a valid <code>Entity</code>.
     */
    private static void decodeLocation( ParsedXML node, Entity entity ) {
        // TODO : implement me
    }

    /**
     * Helper function to decode the inferno rounds on an <code>Entity</code>
     * object from the passed node.
     *
     * @param   node - the <code>ParsedXML</code> node for this object.
     *          This value must not be <code>null</code>.
     * @param   entity - the <code>Entity</code> the decoded object belongs to.
     * @throws  IllegalArgumentException if the node is
     *          <code>null</code>.
     * @throws  IllegalStateException if the node does not
     *          contain a valid <code>Entity</code>.
     */
    private static void decodeInferno( ParsedXML node, Entity entity ) {
        String attrStr = null;
        int attrVal = 0;

        // Did we get a null node?
        if ( null == node ) {
            throw new IllegalArgumentException( "The inferno is null." );
        }

        // Make sure that the node is for a EntityData object.
        if ( !node.getName().equals( "inferno" ) ) {
            throw new IllegalStateException( "Not passed a inferno node." );
        }

        // Try to find the inferno detail nodes.
        Enumeration details = node.elements();
        while ( details.hasMoreElements() ) {
            ParsedXML detail = (ParsedXML) 
                details.nextElement();

            // Have we found the Arrow IV inferno detail?
            if ( detail.getName().equals("arrowiv") ) {

                // Get the burn turns attribute.
                attrStr = detail.getAttribute( "turns" );
                if ( null == attrStr ) {
                    throw new IllegalStateException
                        ( "Couldn't decode the burn turns for an Arrow IV inferno round." );
                }

                // Try to pull the value from the string
                try {
                    attrVal = Integer.parseInt( attrStr );
                }
                catch ( NumberFormatException exp ) {
                    throw new IllegalStateException
                        ( "Couldn't get an integer from " + attrStr );
                }

                // Add the number of Arrow IV burn turns.
                entity.infernos.add( InfernoTracker.INFERNO_IV_TURN,
                                     attrVal );

            } // End found-arrowiv-detail

            // Have we found the standard inferno entry?
            else if ( detail.getName().equals("standard") ) {

                // Get the burn turns attribute.
                attrStr = detail.getAttribute( "turns" );
                if ( null == attrStr ) {
                    throw new IllegalStateException
                        ( "Couldn't decode the burn turns for a standard inferno round." );
                }

                // Try to pull the value from the string
                try {
                    attrVal = Integer.parseInt( attrStr );
                }
                catch ( NumberFormatException exp ) {
                    throw new IllegalStateException
                        ( "Couldn't get an integer from " + attrStr );
                }

                // Add the number of standard burn turns.
                entity.infernos.add( InfernoTracker.STANDARD_TURN,
                                     attrVal );

            } // End found-standard-detail

        } // Handle the next detail node.

    }

    /**
     * Helper function to decode a <code>Entity</code> object from the
     * passed node.
     *
     * @param   node - the <code>ParsedXML</code> node for this object.
     *          This value must not be <code>null</code>.
     * @param   game - the <code>IGame</code> the decoded object belongs to.
     * @return  the <code>Entity</code> object based on the node.
     * @throws  IllegalArgumentException if the node is
     *          <code>null</code>.
     * @throws  IllegalStateException if the node does not
     *          contain a valid <code>Entity</code>.
     */
    private static Entity decodeEntityData( ParsedXML node, IGame game ) {
        String attrStr = null;
        int attrVal = 0;
        boolean attrTrue = false;
        Entity entity = null;
        Coords coords = null;
        ParsedXML actionNode = null;
        ParsedXML narcNode = null;
        ParsedXML infernoNode = null;
        ParsedXML loadedUnitsNode = null;

        // Did we get a null node?
        if ( null == node ) {
            throw new IllegalArgumentException( "The entityData is null." );
        }

        // Make sure that the node is for a EntityData object.
        if ( !node.getName().equals( "entityData" ) ) {
            throw new IllegalStateException( "Not passed a entityData node." );
        }

        // TODO : perform version checking.

        // Walk the entityData node's children.
        Enumeration children = node.elements();
        while ( children.hasMoreElements() ) {
            ParsedXML child = (ParsedXML) children.nextElement();
            String childName = child.getName();

            // Handle null child names.
            if ( null == childName ) {

                // No-op.
            }

            // Did we find the coords node?
            else if ( childName.equals("coords") ) {

                // We can decode the coords immediately.
                coords = CoordsEncoder.decode( child, game );

            } // End found-"coords"-child

            // Did we find the action node?
            // TODO : rename me
            else if ( childName.equals("action") ) {

                // Save the action node for later decoding.
                actionNode = child;

            } // End found-"action"-child

            // Did we find the narcs node?
            else if ( childName.equals("narcs") ) {

                // Save the narc node for later decoding.
                narcNode = child;

            } // End found-"narc"-child

            // Did we find the inferno node?
            else if ( childName.equals("inferno") ) {

                // Save the inferno node for later decoding.
                infernoNode = child;

            } // End found-"inferno"-child

            // Did we find the loadedUnits node?
            else if ( childName.equals("loadedUnits") ) {

                // Save the loadedUnits node for later decoding.
                loadedUnitsNode = child;

            } // End found-"loadedUnits"-child

            // Did we find the class node?
            else if ( childName.equals("class") ) {

                // Create the appropriate sub-class of Entity.
                attrStr = child.getAttribute( "name" );
                if ( null == attrStr ) {
                    throw new IllegalStateException
                        ( "Couldn't decode the name of a class node." );
                }
                else if ( attrStr.equals("BipedMech") ) {
                    entity = BipedMechEncoder.decode( child, game );
                }
                else if ( attrStr.equals("QuadMech") ) {
                    entity = QuadMechEncoder.decode( child, game );
                }
                else if ( attrStr.equals("Tank") ) {
                    entity = TankEncoder.decode( child, game );
                }
                else if ( attrStr.equals("BattleArmor") ) {
                    entity = BattleArmorEncoder.decode( child, game );
                }
                else if ( attrStr.equals("Infantry") ) {
                    entity = InfantryEncoder.decode( child, game );
                }
                else if ( attrStr.equals("Protomech") ) {
                    entity = ProtomechEncoder.decode( child, game );
                }
                else if ( attrStr.equals("GunEmplacement") ) {
                    entity = GunEmplacementEncoder.decode( child, game );
                }
                else {
                    throw new IllegalStateException
                        ( "Unexpected name for a class node: " + attrStr );
                }

            } // End found-"class"-child

        } // Look at the next child.

        // Did we find the entity yet?
        if ( null == entity ) {
            throw new IllegalStateException
                ( "Couldn't locate the class for an entityData node." );
        }

        // Decode the inferno node.
        EntityEncoder.decodeInferno( infernoNode, entity );

        // TODO : a whole lot more decoding needed.

        return entity;
    }

    /**
     * Decode a <code>Entity</code> object from the passed node.
     *
     * @param   node - the <code>ParsedXML</code> node for this object.
     *          This value must not be <code>null</code>.
     * @param   game - the <code>IGame</code> the decoded object belongs to.
     * @return  the <code>Entity</code> object based on the node.
     * @throws  IllegalArgumentException if the node is
     *          <code>null</code>.
     * @throws  IllegalStateException if the node does not
     *          contain a valid <code>Entity</code>.
     */
    public static Entity decode( ParsedXML node, IGame game ) {
        String attrStr = null;
        int attrVal = 0;
        Entity entity = null;
        Coords coords = null;
        Vector locations = new Vector();
        ParsedXML pilotNode = null;
        ParsedXML equipNode = null;
        Enumeration children = null;
        ParsedXML child = null;
        String childName;

        // Did we get a null node?
        if ( null == node ) {
            throw new IllegalArgumentException( "The entity is null." );
        }

        // Make sure that the node is for a Entity object.
        if ( !node.getName().equals( "entity" ) ) {
            throw new IllegalStateException( "Not passed a entity node." );
        }

        // TODO : perform version checking.

        // Walk the entity node's children, finding bits for later parsing..
        children = node.elements();
        while ( children.hasMoreElements() ) {
            child = (ParsedXML) children.nextElement();
            childName = child.getName();

            // Handle null child childNames.
            if ( null == childName ) {

                // No-op.
            }

            // Did we find the pilot node?
            else if ( childName.equals( "pilot" ) ) {

                // Save the entity's pilot for later decoding.
                pilotNode = child;

            } // End found-"entityEquipment"-node

            // Did we find the entityData node?
            else if ( childName.equals( "entityData" ) ) {

                // Did we find the entity already?
                if ( null != entity ) {
                    throw new IllegalStateException
                        ( "Found two entityData nodes for an Entity node." );
                }

                // Decode the entity data.
                entity = EntityEncoder.decodeEntityData( child, game );

            } // End found-"entityData"-node

            // Did we find the entityEquipment node?
            else if ( childName.equals( "entityEquipment" ) ) {

                // Save the entity equipment for later decoding.
                equipNode = child;

            } // End found-"entityEquipment"-node

            // Did we find the location node?
            else if ( childName.equals( "location" ) ) {

                // Save this location for later decoding.
                locations.addElement( child );

            } // End found-"location"-node

        } // Look at the next child.

        // Did we find the needed elements?
        if ( null == entity ) {
            throw new IllegalStateException
                ( "Couldn't locate the entityData for an Entity node." );
        }
        else if ( null == pilotNode ) {
            throw new IllegalStateException
                ( "Couldn't locate the pilot for an Entity node." );
        }
        else if ( null == equipNode ) {
            throw new IllegalStateException
                ( "Couldn't locate the entityEquipment for an Entity node." );
        }
        else if ( locations.size() != entity.locations() ) {
            StringBuffer msgBuf = new StringBuffer();
            msgBuf.append( "Found " )
                .append( locations.size() )
                .append( " locations for an Entity node. " )
                .append( "Was expecting to find " )
                .append( entity.locations() )
                .append( "." );
            throw new IllegalStateException( msgBuf.toString() );
        }

        // Decode the entity node's chassis.
        attrStr = node.getAttribute( "chassis" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the chassis from an Entity node." );
        }
        entity.setChassis( attrStr );

        // Decode the entity node's model.
        attrStr = node.getAttribute( "model" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the model from an Entity node." );
        }
        entity.setModel( attrStr );

        // Decode the entity node's movement type.
        attrStr = node.getAttribute( "typeVal" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the typeVal from an Entity node." );
        }

        // Try to pull the value from the string
        try {
            attrVal = Integer.parseInt( attrStr );
        }
        catch ( NumberFormatException exp ) {
            throw new IllegalStateException
                ( "Couldn't get an integer from " + attrStr );
        }
        entity.setMovementMode( attrVal );

        // Decode the entity node's year.
        attrStr = node.getAttribute( "year" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the year from an Entity node." );
        }

        // Try to pull the value from the string
        try {
            attrVal = Integer.parseInt( attrStr );
        }
        catch ( NumberFormatException exp ) {
            throw new IllegalStateException
                ( "Couldn't get an integer from " + attrStr );
        }
        entity.setYear( attrVal );

        // Decode the entity node's techBase.
        attrStr = node.getAttribute( "techBase" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the techBase from an Entity node." );
        }

        // Try to pull the value from the string
        try {
            attrVal = Integer.parseInt( attrStr.substring(0,1) );
        }
        catch ( NumberFormatException exp ) {
            throw new IllegalStateException
                ( "Couldn't get an integer from " + attrStr.substring(0,1) );
        }
        entity.setTechLevel( attrVal );

        // Decode the entity node's mass.
        attrStr = node.getAttribute( "mass" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the mass from an Entity node." );
        }

        // Try to pull the value from the string
        try {
            attrVal = Integer.parseInt( attrStr );
        }
        catch ( NumberFormatException exp ) {
            throw new IllegalStateException
                ( "Couldn't get an integer from " + attrStr );
        }
        entity.setWeight( attrVal );

        // Decode the entity node's walkMp.
        attrStr = node.getAttribute( "walkMp" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the walkMp from an Entity node." );
        }

        // Try to pull the value from the string
        try {
            attrVal = Integer.parseInt( attrStr );
        }
        catch ( NumberFormatException exp ) {
            throw new IllegalStateException
                ( "Couldn't get an integer from " + attrStr );
        }
        entity.setOriginalWalkMP( attrVal );

        // Decode the entity node's jumpMp.
        attrStr = node.getAttribute( "jumpMp" );
        if ( null == attrStr ) {
            throw new IllegalStateException
                ( "Couldn't decode the jumpMp from an Entity node." );
        }

        // Try to pull the value from the string
        try {
            attrVal = Integer.parseInt( attrStr );
        }
        catch ( NumberFormatException exp ) {
            throw new IllegalStateException
                ( "Couldn't get an integer from " + attrStr );
        }
        entity.setOriginalJumpMP( attrVal );

        // Try to pull the value from the string
        try {
            attrVal = Integer.parseInt( attrStr );
        }
        catch ( NumberFormatException exp ) {
            throw new IllegalStateException
                ( "Couldn't get an integer from " + attrStr );
        }

        // Decode the entity's pilot.
        EntityEncoder.decodePilot( pilotNode, entity );

        // Decode the entity's equipment.
        EntityEncoder.decodeEntityEquipment( equipNode, entity );

        // Decode the entity's locations.
        children = locations.elements();
        while ( children.hasMoreElements() ) {
            child = (ParsedXML) children.nextElement();
            EntityEncoder.decodeLocation( child, entity );
        }

        return entity;
    }

}

