/* **********************************
 *   Administrator.java
 *     Administrator
 * **********************************/
package admin;

import config.AdminConfig;
import edu.neu.ccs.demeterf.demfgen.lib.List;
import edu.neu.ccs.demeterf.demfgen.lib.List.Pred;
import gen.*;
import admin.utils.*;
import admin.utils.Game.GameType;

import java.io.*;

/** Administrator of the game
 *   @authors animesh, mkoscumb, and Alex Dubreuil
 */
public class Administrator{
    private Players players = AdminDocumentHandler.getPlayers();
    private Config config = AdminDocumentHandler.getConfig();
    private File playerDir = new File(AdminConfig.BLACKBOARD_PATH + AdminConfig.PLAYERS_PATH);
    private Store store = AdminDocumentHandler.getStore();
    private Accounts accounts = AdminDocumentHandler.getAccounts();
    Output out;
    Output results;
    String maxStack;
    int roundNum;
    /** Create a new Administrator with the given Output */
    Administrator(Output o, String pStack) { this(o, o, pStack); }
    Administrator(Output o, Output r, String pStack) { out = o; results = r; roundNum = 1; maxStack = pStack; }
    
    /** Connection to the outside world... */
    public static void main(String[] args){
        final AdminCmdLineParser cmd = new AdminCmdLineParser(args);
        Output o = (!cmd.outputFile.equals(""))?
                Output.htmlOutput(cmd.outputFile):Output.htmlOutput(); 
        AdminDocumentHandler.resetFilesToDefault();
        if (!cmd.resultsFile.equals(""))
        		new Administrator(o, Output.htmlOutput(cmd.resultsFile), cmd.maxStack).main(cmd.gt);
        else
        		new Administrator(o, cmd.maxStack).main(cmd.gt);
        
    }
    
    long curTurnFin = System.currentTimeMillis();
    /** Main method for Administrator */
    private void main(final GameType g){
        long gameStartTime = curTurnFin;
        
        out.header();
        if(results != out) { results.header(); }
        while(roundNum <= config.Rounds + 2 && players.length() > 1){
            if(overtime(roundNum)){ 
            	out.newOTRound(roundNum, config.Rounds); 
            	System.err.println(" * Starting Overtime Round #"+(roundNum-config.Rounds)+" of 2.");
            }else{ 
            	out.newRound(roundNum, config.Rounds); 
            	System.err.println(" * Starting Round #"+(roundNum)+" of "+config.Rounds);            	
            }
            
            Round round = new Round(roundNum, players.players.map(new List.Map<Player, PlayerTransaction>(){
                public PlayerTransaction map(Player player){
                    out.time("Admin overhead", curTurnFin);
                    out.line("");
                    
                    if(passTurn(player, roundNum)){
                        curTurnFin = System.currentTimeMillis();
                        PlayerTransaction pTrans = AdminDocumentHandler.getPlayerTrans(player);
                        
                        if(pTrans != null){
                        	Pair<Boolean, String> rules;
                            pTrans = Util.checkDerivatives(pTrans,store);
                        	pTrans = Util.computeQualities(pTrans);
                            pTrans = Util.truncateRMs(pTrans, config.MaxRawMaterialLen);
                            
                        	if(overtime(roundNum)){
	                        	List<Transaction> transs = pTrans.transactions.filter(new Pred<Transaction>(){
									public boolean huh(Transaction arg0) {
										return arg0.ttype instanceof Deliver || arg0.ttype instanceof Finish;
									}
	                            });
	                        	pTrans = new PlayerTransaction(pTrans.player, transs);
	                            rules = RuleChecker.overtimeRulesFollowed(pTrans,store,accounts,g,config);
                        	}else { rules = RuleChecker.rulesFollowed(pTrans,store,accounts,g,config); }
                            if(rules.a){
                                //long start = System.currentTimeMillis();
                                updateStore(pTrans);
                                //out.time("Store Update", start);
                                
                                //start = System.currentTimeMillis();
                                updateAccounts(pTrans);
                                //out.time("Accounts Update", start);
                                return pTrans;
                            }
                            
                            out.violation(rules.b, player);
                        }else
                            out.violation("No Transaction", player);    
                    }else
                        out.violation("Time Violation or missing jar", player);
                    
                    takeViolationAction(players, player);
                    
                    // Remove the player from players for next Round
                    players = players.remove(player.id);
                    return new PlayerTransaction(player,List.<Transaction>create());
                }}));
            
            //long start = System.currentTimeMillis();
            AdminDocumentHandler.updateHistory(round);
            //out.time("History Update", start);
            
            roundNum++;
        }
        System.err.println("Game finished, calculating results.");
        out.time("Admin overhead", curTurnFin);
        out.time("Total Game time", gameStartTime);
        declareWinner();
        out.footer();
    }
    
    /** Is it overtime? */
    protected boolean overtime(int round) { return round > config.Rounds; }
    
    /** Declares the player with the largest account the winner. */
    private void declareWinner(){    
   		results.finishTable(accounts, players, AdminDocumentHandler.getPlayers());
   		if(results != out) { results.footer(); }
    }
    /** Takes appropriate actions in case one of the players violates rules */
    private void takeViolationAction(Players players, Player player){
        if(store.contains(player.id)){
            // for each derivative in the <boughtstore> of the player 
            //   deduct price value from the account of the seller
            //   add the derivative back to the <forSale> store of the seller
            for(Derivative der: store.getStore(player).bought){
                accounts = accounts.update(Util.pair(der.seller, -der.price.val));
                store = store.replace(Util.spair(der.seller,store.getStore(der.seller)
                        .pushSale(new Derivative(der.name, der.seller, der.price, der.type))));    
            }


            // for each derivative that is created by the player AND in the <boughtStore> of Players
            //   add price value to the account of the buyer
            //   remove derivative from the bought store of the buyer
            for(Player otherPlayer: players.players){
                Pair<PlayerID, PlayerStore> otherPIDAndStore = store.getStorePair(otherPlayer.id);
                List<Derivative> otherPlayerBoughtStore = otherPIDAndStore.b.bought;
                for(Derivative der: otherPlayerBoughtStore){
                    if(der.seller.equals(player.id)){
                        accounts = accounts.update(Util.pair(otherPIDAndStore.a, der.price.val));
                        otherPlayerBoughtStore = otherPlayerBoughtStore.remove(der);
                    }
                }
                otherPIDAndStore.b.bought = otherPlayerBoughtStore;
                store = store.replace(otherPIDAndStore);
            }  
        }
        // remove the player store from store
        AdminDocumentHandler.commitStore(store.remove(player.id));
        // remove the player account from accounts
        AdminDocumentHandler.commitAccounts(accounts.remove(player.id));
    }

    /** Updates the store with the player transaction */
    private void updateStore(PlayerTransaction trans) {
        store = StoreUpdater.updateStore(store, trans);
        AdminDocumentHandler.commitStore(store);        
    }
    /** Updates the accounts with the player transaction */
    private void updateAccounts(PlayerTransaction pTrans) {
        accounts = AccountUpdater.updateAccounts(accounts, pTrans);
        AdminDocumentHandler.commitAccounts(accounts);        
    }
    
    
    static Runtime runtime = Runtime.getRuntime();
    /** Passes the turn to player */
    private boolean passTurn(Player player, int roundNum) {
        out.println("");
        out.line("** Player " + player.id.id + ": Team " + player.name + "'s turn");
        
        String fName = AdminConfig.BLACKBOARD_PATH + AdminConfig.PLAYERS_PATH + "/" + player.name;
        String jcmdline = "java -Xss" + maxStack + " -jar " + player.name + ".jar " + player.id.id + " " + roundNum;
        String ccmdline = fName+ ".exe " + player.id.id + " " + player.name;
        String cmdline = jcmdline;
        
        // Determine if the player is C# or Java
        if(!new File(fName + ".jar").exists()){ 
            if(!new File(fName + ".exe").exists())
                return false;
            cmdline = ccmdline; 
        }
        
        Process p;
        long sTime = 0, eTime = 0;
        try{
            sTime = System.currentTimeMillis();
            out.playerStart();
            p = runtime.exec(cmdline, null, playerDir);
            StreamWatcher outWatch = new StreamWatcher(p.getInputStream(), out);
            StreamWatcher errWatch = new StreamWatcher(p.getErrorStream(), out);
            ProcessWatcher pWatch = new ProcessWatcher(p);
            
            pWatch.waitForTimeLimit(config.Time);
            eTime = System.currentTimeMillis();
            Thread.sleep(2000);
            System.out.flush();
            System.err.flush();
            outWatch.kill();
            errWatch.kill();
            out.playerEnd();
            out.time("Player " + player.id.id + ": Team " + player.name + " finished", sTime);
            out.println(out.vspace()+out.vspace());
            if ((eTime - sTime) > config.Time){
                p.destroy();
                return false;
            }
        // Catch for p = r.exec(cmdline);
        }catch(IOException e) { e.printStackTrace(); 
        // Catch for pWatch.waitForTimeLimit(config.Time);
        }catch (InterruptedException e) { e.printStackTrace(); }
        return true;
    }
}