
import org.jdom.Element;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import java.io.IOException;
import java.io.File;

import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Vector;

public class TICombat {
    
    public static final String TAG_BATTLE="battle";
    public static final String TAG_UNIT="unit";
    public static final String TAG_NAME="name";
    public static final String TAG_TOHIT="tohit";
    public static final String TAG_ROLLS="rolls";
    public static final String TAG_WOUNDS="wounds";
    public static final String TAG_MOVE="move";
    public static final String TAG_SAVE="save";
    public static final String TAG_DESTROYER="destroyer";
    public static final String TAG_FIGHTER="fighter";
    public static final String TAG_COUNT="count";
    public static final String TAG_PB_SHOTS="pbshots";
    public static final String TAG_PB_HIT="pbhit";
    public static final String NAME_GT="ground";
    public static final String NAME_ST="shock";
    public static final String NAME_FIGHTER="fighter";
    public static final String NAME_DESTROYER="destroyer";
    public static final String NAME_CRUISER="cruiser";
    public static final String NAME_CARRIER="carrier";
    public static final String NAME_DREADNAUGHT="dreadnaught";
    public static final String NAME_WARSUN="warsun";
    public static final String NAME_PDS="pds";

    public static final int DESTROYER_SHOTS=2;

    public static void main (String args[]) throws IOException, JDOMException {
        if (args.length < 1){
            out("usage: java TICombat <configFile>");
            return;
        }
        long seed = 0;
        if (args.length > 1) {
            seed = Long.parseLong(args[1]);
        }

        TICombat battle = new TICombat(args[0], seed);
        battle.fight();

        

    }

    protected List m_side1, m_side2;
    protected Random m_random;
    protected boolean m_debug = false;
    protected int m_numFights = 1;

////////////////////////////////////////////////////////////////////////////////
// initialization
////////////////////////////////////////////////////////////////////////////////
    public TICombat(String configFile, long seed) throws IOException, JDOMException {
        Document doc = new SAXBuilder().build(new File(configFile));
        Element root = doc.getRootElement();
        if (!TAG_BATTLE.equals(root.getName())) {
            throw new IOException ("root tag != '"+TAG_BATTLE+"'");
        }
        String curAttr = root.getAttributeValue("debug");
        if (curAttr != null) {
            m_debug = Boolean.valueOf(curAttr).booleanValue();
        } else {
            out ("cannot find 'debug' tag.");
        }
        curAttr = root.getAttributeValue("num");
        if (curAttr != null) {
            m_numFights = Integer.parseInt(curAttr.trim());
        } else {
            out ("cannot find 'num' tag.");
        }

        m_side1 = extractUnits(root.getChild("side1"));
        m_side2 = extractUnits(root.getChild("side2"));

        if (seed < 1) {
            seed = System.currentTimeMillis();
        }
        m_random = new Random(seed);
        out ("random seed: "+seed);
        out ("side 1:");
        printSide(m_side1, false);
        out ("side 2:");
        printSide(m_side2, false);
        

    }
    public List extractUnits(Element side) {
        List children = side.getChildren(TAG_UNIT);
        Iterator iter = children.iterator();

        List units = new Vector(children.size());

        int count;
        CombatUnit curUnit;
        while (iter.hasNext()) {
            Element cur = (Element) iter.next();
            String countStr = cur.getChildText(TAG_COUNT);
            curUnit = getUnit(cur);
            if (countStr != null) {
                count = Integer.parseInt(countStr);
                for (int i=0; i<count; i++) {
                    units.add(curUnit.clone());
                }
            } else {
                units.add(curUnit);
                count = 1;
            }
            debug("created "+count+" "+curUnit.getName()+"(s)");

        }
        return units;
    }

////////////////////////////////////////////////////////////////////////////////
// combat fighting
////////////////////////////////////////////////////////////////////////////////
    public void fight() {

        int numCurBattle = 0;
        int round = 1;
        int side1Wins = 0;
        int side2Wins = 0;
        int side1Left = 0;
        int side2Left = 0;
        int curLeft;
        while (numCurBattle < m_numFights) {
            resetSides();
            debug ("========= pre-battle ================");
            applyPreBattleWounds();
            // debug("main combat starting");
            while (isSideAlive(m_side1) && isSideAlive(m_side2)) {
                debug ("========= round "+round + " ================");
    
                int numWounds1 = getNonWarsunWounds(m_side1);
                int wsunWounds1 = getWarsunWounds(m_side1);
                debug("side 1 hits: "+numWounds1+" warsun: "+wsunWounds1);
    
                int numWounds2 = getNonWarsunWounds(m_side2);
                int wsunWounds2 = getWarsunWounds(m_side2);
                debug("side 2 hits: "+numWounds2+" warsun: "+wsunWounds2);
    
                applyWounds(m_random, numWounds1, m_side2);
                applyWounds(m_random, numWounds2, m_side1);

                applyWounds(m_random, wsunWounds1, m_side2, false);
                applyWounds(m_random, wsunWounds2, m_side1, false);
    
                round++;
            }
            printBattleResults();
            numCurBattle++;
            curLeft = getShipsLeft(m_side1);
            side1Left += curLeft;
            if (curLeft > 0) {
                side1Wins++;
            }

            curLeft = getShipsLeft(m_side2);
            side2Left += curLeft;

            if (curLeft > 0) {
                side2Wins++;
            }
        }
        out("side 1 wins "+side1Wins+"/"+m_numFights+", avg left: "+
            round (((double) side1Left / (double) side1Wins)));
        out("side 2 wins "+side2Wins+"/"+m_numFights+", avg left: "+
            round (((double) side2Left / (double) side2Wins)));
        out("no one wins "+(m_numFights-side1Wins-side2Wins)+"/"+m_numFights);
        
    }

    public double round(double d) {
        return (double) (Math.round(d * 1000)) / 1000.0;
    }
    public void printBattleResults() {
        debug("=============");
        debug("Side 1:");
        printSide(m_side1);
        debug("Side 2:");
        printSide(m_side2);
    }
    public void printSide(List side) {
        printSide(side, true);
    }

    public void printSide(List side, boolean debugOnly) {
        Iterator iter = side.iterator();
        CombatUnit unit;
        while (iter.hasNext()) {
            unit = (CombatUnit) iter.next();
            if (unit.isAlive()) { 
                if (debugOnly) {
                    debug (unit.getName()+", wounds left: "+unit.getWoundsLeft());
                } else {
                    out (unit.getName());
                }
            }
        }
    }
    public int getShipsLeft(List side) {
        Iterator iter = side.iterator();
        int numLeft=0;
        while (iter.hasNext()) {
            CombatUnit unit = (CombatUnit) iter.next();
            if (unit.isAlive() && !NAME_PDS.equals(unit.getName())) {
                numLeft++;
            }
        }
        return numLeft;

    }

    public boolean isSideAlive(List side) {
        Iterator iter = side.iterator();
        while (iter.hasNext()) {
            CombatUnit unit = (CombatUnit) iter.next();
            if (unit.isAlive() && !NAME_PDS.equals(unit.getName())) {
                return true;
            }
        }
        return false;

    }

    public void applyPreBattleWounds() {
        int dwounds1 = getDestroyerWounds(m_side1);
        int dwounds2 = getDestroyerWounds(m_side2);
        int excessWounds = applyDestroyerWounds(dwounds1, m_side2);

        // debug ("side 1 excess destroyer wounds: "+excessWounds);

        excessWounds = applyDestroyerWounds(dwounds2, m_side1);
        // debug ("side 2 excess destroyer wounds: "+excessWounds);

        // debug ("pds wounds");
        applyPdsWounds();

    }

    public void applyPdsWounds() {
        int pdsWounds1 = getWounds(NAME_PDS, m_side1);
        int pdsWounds2 = getWounds(NAME_PDS, m_side2);
        applyWounds(m_random, pdsWounds1, m_side2);
        applyWounds(m_random, pdsWounds2, m_side1);
        killAll(NAME_PDS, m_side1);
        killAll(NAME_PDS, m_side2);
    }

    public void killAll(String unitName, List side) {
        Iterator iter = side.iterator();
        CombatUnit curUnit;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            if (unitName.equals(curUnit.getName())) {
                curUnit.kill();
            }
        }
    }


    public int getWounds(String unitName, List side) {
        Iterator iter = side.iterator();
        CombatUnit curUnit;
        int totalWounds = 0;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            if (unitName.equals(curUnit.getName())) {
                totalWounds += curUnit.generateHits(m_random);
            }
        }
        return totalWounds;
    }

    public int getWounds(List side) {
        Iterator iter = side.iterator();
        CombatUnit curUnit;
        int totalWounds = 0;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            totalWounds += curUnit.generateHits(m_random);
        }
        return totalWounds;
    }

    public int getNonWarsunWounds(List side) {
        Iterator iter = side.iterator();
        CombatUnit curUnit;
        int totalWounds = 0;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            if (!NAME_WARSUN.equals(curUnit.getName())) {
                totalWounds += curUnit.generateHits(m_random);
            }
        }
        return totalWounds;
    }

    public int getWarsunWounds(List side) {
        return getWounds(NAME_WARSUN, side);
    }


    public void applyWounds(Random random, int wounds, List side) {
        applyWounds(random, wounds, side, true);
    }

    public void applyWounds(Random random, int wounds, List side, boolean allowSave) {
        wounds = tryDamageShips(random, wounds, side, allowSave);
        if (wounds > 0) {
            wounds = tryHit(random, NAME_GT, wounds, side, allowSave);
        }
        if (wounds > 0) {
            wounds = tryHit(random, NAME_ST, wounds, side, allowSave);
        }
        if (wounds > 0) {
            wounds = tryHit(random, NAME_FIGHTER, wounds, side, allowSave);
        }
        if (wounds > 0) {
            wounds = tryHit(random, NAME_DESTROYER, wounds, side, allowSave);
        }
        if (wounds > 0) {
            wounds = tryHit(random, NAME_CARRIER, wounds, side, allowSave);
        }
        if (wounds > 0) {
            wounds = tryHit(random, NAME_CRUISER, wounds, side, allowSave);
        }
        if (wounds > 0) {
            wounds = tryHit(random, NAME_DREADNAUGHT, wounds, side, allowSave);
        }
        if (wounds > 0) {
            wounds = tryHit(random, NAME_WARSUN, wounds, side, allowSave);
        }
    }

    /**
     * @return the amount of wounds left over after damaging the ships.
     */
    public int tryDamageShips(Random rand, int wounds, List side, boolean allowSave) {
        if (wounds < 1) return wounds;

        Iterator iter = side.iterator();
        CombatUnit curUnit;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            wounds = curUnit.applyWounds(rand, wounds, true, allowSave);

        }
        return wounds;
    }

    public int tryHit(Random rand, String name, int wounds, List side, boolean allowSave) {
        Iterator iter = side.iterator();
        CombatUnit curUnit;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            if (name.equals(curUnit.getName())
                && curUnit.isAlive()
            ) {
                wounds = curUnit.applyWounds(rand, wounds, false, allowSave);
            }
        }
        return wounds;
    }

    public int getDestroyerWounds (List side) {
        Iterator iter = side.iterator();
        CombatUnit curUnit;
        int numHits = 0;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            if (NAME_DESTROYER.equals(curUnit.getName())) {
                numHits += curUnit.generatePbHits(m_random);
            }
        }
        return numHits;
    }

    public int applyDestroyerWounds(int wounds, List side) {
        Iterator iter = side.iterator();
        CombatUnit curUnit;
        while (iter.hasNext()) {
            curUnit = (CombatUnit) iter.next();
            if (NAME_FIGHTER.equals(curUnit.getName()) && curUnit.isAlive()) {
                wounds = curUnit.applyWounds(m_random, wounds);
            }
        }
        return wounds;
    }

    public void resetSides() {
        Iterator iter = m_side1.iterator();
        while (iter.hasNext()) {
            CombatUnit cur = (CombatUnit) iter.next();
            cur.resetWoundsTaken();
        }
        iter = m_side2.iterator();
        while (iter.hasNext()) {
            CombatUnit cur = (CombatUnit) iter.next();
            cur.resetWoundsTaken();
        }
    }

    public CombatUnit getUnit(Element unit) {
        return new CombatUnit(unit);
    }


    public static void out(String s) {
        System.out.println(s);
    }
    public void debug(String s) {
        if (m_debug) {
            out(s);
        }
    }

////////////////////////////////////////////////////////////////////////////////
// CombatUnit class
////////////////////////////////////////////////////////////////////////////////
    public class CombatUnit implements Cloneable {
        protected String m_name = null;
        protected int m_move = 1;
        protected int m_tohit = 11;
        protected int m_wounds = 1;
        protected int m_rolls = 1;
        protected int m_save = 11;
        protected int m_pbShots = 0;
        protected int m_pbHit = 11;

        protected int m_woundsTaken = 0;

        public CombatUnit (Element xml) {
            String curVal = xml.getChildText(TAG_NAME);
            if (curVal != null) {
                m_name=curVal.toLowerCase();
                if (
                    m_name.equals(NAME_GT) ||
                    m_name.equals(NAME_ST) ||
                    m_name.equals(NAME_FIGHTER) ||
                    m_name.equals(NAME_DESTROYER) ||
                    m_name.equals(NAME_CARRIER) ||
                    m_name.equals(NAME_CRUISER) ||
                    m_name.equals(NAME_DREADNAUGHT) ||
                    m_name.equals(NAME_WARSUN)
                ) {
                    // name is good
                } else {
                    out ("ERROR: unit name '"+m_name+"' not recognized.");
                }
            }

            curVal = xml.getChildText(TAG_TOHIT);
            if (curVal != null) m_tohit=Integer.parseInt(curVal);

            curVal = xml.getChildText(TAG_ROLLS);
            if (curVal != null) m_rolls=Integer.parseInt(curVal);

            curVal = xml.getChildText(TAG_WOUNDS);
            if (curVal != null) m_wounds=Integer.parseInt(curVal);

            curVal = xml.getChildText(TAG_MOVE);
            if (curVal != null) m_move=Integer.parseInt(curVal);

            curVal = xml.getChildText(TAG_SAVE);
            if (curVal != null) m_save=Integer.parseInt(curVal);

            curVal = xml.getChildText(TAG_PB_SHOTS);
            if (curVal != null) m_pbShots=Integer.parseInt(curVal);

            curVal = xml.getChildText(TAG_PB_HIT);
            if (curVal != null) {
                m_pbHit=Integer.parseInt(curVal);
            } else {
                m_pbHit=m_tohit;
            }

            //if (xml.getChild(TAG_DESTROYER) != null) destroyer=true;
            //if (xml.getChild(TAG_FIGHTER) != null) fighter=true;

            debug("created a "+getName()+" with "+m_wounds+" wounds, "+
                m_tohit+" to hit, "+m_rolls+" rolls");

        }
        public Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException ex) {
                return null;
            }
        }

        public String getName() { return m_name; }
        public int getMove() { return m_move; }
        public int getToHit() { return m_tohit; }
        public int getWounds() { return m_wounds; }
        public int getRolls() { return m_rolls; }
        public int getSave() { return m_save; }
        public int getPbShots() { return m_pbShots; }
        public int getPbHit() { return m_pbHit; }
        // public boolean getDestroyer() { return destroyer; }
        // public boolean getFighter() { return fighter; }

        public void addWoundsTaken(int w) {m_woundsTaken +=w;}

        /**
         * returns the number of hits 'left' after applying the hits
         * to this unit.
         * @param noKill if true, only damage the ship.  
         */
        public int applyWounds(Random rand, int numHits) {
            return applyWounds (rand, numHits, false);
        }

        public int applyWounds(Random rand, int numHits, boolean noKill) {
            return applyWounds(rand, numHits, noKill, true);
        }
        public int applyWounds(Random rand, int numHits, boolean noKill, 
            boolean allowSave
        ) {

            int woundsLeft;
            if (noKill) {
                woundsLeft = m_wounds - m_woundsTaken - 1;
            } else {
                woundsLeft = m_wounds - m_woundsTaken;
            }
            if (woundsLeft <= 1 && noKill) return numHits;
            
            int hitsCaused = 0;
            while (numHits-- > 0) {
                if (allowSave) {
                    if (!trySave(rand)) {
                        hitsCaused++;
                    }
                } else {
                    hitsCaused++;
                }

                if (hitsCaused >= woundsLeft) break;
            }
            addWoundsTaken(hitsCaused);
            if (m_wounds <= m_woundsTaken) {
                debug(getName()+" destroyed.");

            } else if (hitsCaused > 0) {
                debug(getName()+" damaged.");

            }

            return numHits;

        }
        public boolean trySave(Random r) {
            
            int curRoll = r.nextInt(10)+1;
            if (curRoll >= getSave()) {
                debug (getName()+" saved with a roll of "+curRoll);
                return true;
            }
            return false;
        }


        public void setWoundsTaken(int w) { m_woundsTaken = w; }

        public int getWoundsTaken() { return m_woundsTaken; }
        public int getWoundsLeft() { return m_wounds-m_woundsTaken; }

        public void resetWoundsTaken() { m_woundsTaken = 0; }

        public boolean isAlive() { return m_woundsTaken < m_wounds; }

        public int generateHits(Random random) {
            return generateHits(random, m_rolls);
        }
        public int generateHits(Random random, int numRolls) {
            return generateHits(random, numRolls, false);
        }

        public int generateHits(Random random, int numRolls, boolean usePbHit) {
            if (!isAlive()) { 
                return 0;
            }

            int hits = 0;
            int curRoll;
            for (int i=0; i<numRolls; i++) {
                curRoll = random.nextInt(10)+1;
                debug (getName() +" rolled "+curRoll);
                if (usePbHit) {
                    if (curRoll >= m_pbHit) {
                        hits++;
                    }
                } else {
                    if (curRoll >= m_tohit) {
                        hits++;
                    }
                }
            }
            return hits;
        }

        public int generatePbHits(Random rand) {
            return generateHits(rand, m_pbShots, true);
        }

        public void kill() {
            setWoundsTaken(m_wounds);
        }


    }

    public static boolean isEqual(String one, String two) {
        if ((one != null && two != null && one.equals(two)) ||
            (one == null && two == null)
        ) {
            return true;
        } else {
            return false;
        }
    }
}
