/* **********************************
 *   RuleChecker.java
 *     RuleChecker
 * **********************************/
package admin.utils;

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


/** Class for checking if a player transaction adheres to all game rules
 *    @author animesh, acdubre */
public class RuleChecker {
    String message = "";
    PlayerTransaction pTrans;
    Config config;
    
    protected RuleChecker(PlayerTransaction pT, Config c){ pTrans = pT; config = c; }
    
    void addMessage(String s){ message += s+"\n"; }
    public static Pair<Boolean,String> rulesFollowed(PlayerTransaction t, Store s, Accounts a, GameType gt, Config c) {
        return new RuleChecker(t,c).rulesFollowed(s,a,gt);
    }
    public static Pair<Boolean,String> overtimeRulesFollowed(PlayerTransaction t, Store s, Accounts a, GameType gt, Config c){
    	return new RuleChecker(t,c).overtimeRulesFollowed(s, a);
    }
    
    /** Checks if all rules are followed by a player transaction */
    public Pair<Boolean,String> rulesFollowed(Store store, Accounts accts, GameType gt) {
        boolean posAccount = isAccountPositive(accts);
        boolean buyOrReoffer = isBuyOrReofferRuleFollowed(store);
        boolean create = isCreateRuleFollowed(store, gt);
        boolean deliver = isDeliveryRuleFollowed(store);
        boolean finish = isFinishRuleFollowed(store);
        return new Pair<Boolean,String>(posAccount && buyOrReoffer && 
                create && deliver && finish, message);
    }
    
    /** Checks if all overtime rules are followed by a player transaction */
    public Pair<Boolean,String> overtimeRulesFollowed(Store store, Accounts accts)
    {
    	boolean posAccount = isAccountPositive(accts);
    	boolean deliver = isDeliveryRuleFollowed(store);
    	boolean finish = isFinishRuleFollowed(store);
    	return new Pair<Boolean,String>(posAccount && deliver && finish, message);
    }
    
    /** Checks if the player's account is positive after pTrans */
    protected boolean isAccountPositive(Accounts accts){
        double prevBalance = 0;
        double spentThisTurn = 0;
        double earnedThisTurn = 0;
        for(Pair<PlayerID, Double> acct : accts.accounts){
            if(acct.a.equals(pTrans.player.id))
                prevBalance = acct.b;
        }
        
        for(Transaction trans: pTrans.transactions){
            if(trans.ttype.getClass().equals(Buy.class)){
                // Can't trust the transaction... Need it from the Store
                spentThisTurn += trans.deriv.price.val;
            }
            if(trans.ttype.getClass().equals(Finish.class)){
                earnedThisTurn += trans.deriv.optfinished.inner().quality.val;
            }
        }
        if(prevBalance + earnedThisTurn - spentThisTurn < 0){
            addMessage("Negative account balance");
            return false;
        }
        return true;
    }
    
    /** Checks if buy or reoffer rules are followed */
    protected boolean isBuyOrReofferRuleFollowed(Store store){
        // Either there's nothing to buy/reoffer
    	if(emptyStoreBuyReoffer(store) && 
    			!store.stores.isEmpty() && 
    			!DerivativesFinder.findDerivativesForSaleByOthers(store.stores, pTrans.player.id).isEmpty()){
            // Or there must be a buy or reoffer transaction
            if(!((pTrans.transactions.contains(new TransactionComparatorByTType(new Buy())))
                    ||(pTrans.transactions.contains(new TransactionComparatorByTType(new Reoffer()))))){
                addMessage("No Buy/Reoffer from a non-empty store");
                return false;
            }else{
            	
                if(pTrans.transactions.contains(new TransactionComparatorByTType(new Buy()))){
                    return correctBuy(store);
                }else{
                    Config config = AdminDocumentHandler.getConfig();
                    return correctReoffer(store, config.MPD);                
                }
            }
        }
        return true;        
    }
    
    protected boolean correctReoffer(final Store store, final double MPD){
        List<Transaction> reofferTrans = pTrans.transactions.filter(new TransactionComparatorByTType(new Reoffer()));
        List<Derivative> actual = reofferTrans.map(new List.Map<Transaction, Derivative>(){
            public Derivative map(Transaction t){ return t.deriv; }
        });
        
        List<Derivative> forSale = DerivativesFinder.findDerivativesForSaleByOthers(store.stores, pTrans.player.id);
        List<Derivative> toReoffer = DerivativesFinder.findUniqueDerivatives(forSale);
        List<Derivative> expected = toReoffer.map(new List.Map<Derivative,Derivative>(){
            public Derivative map(Derivative d){
                double price = d.price.val - MPD; 
                return d.reoffer(pTrans.player.id, new Price(price > 0 ? price : 0.0));
            }});
        
        if(actual.length() < expected.length()){
            addMessage("Incorrect reoffering - too few Derivatives.");
            return false;
        }
        
        for(Derivative d : expected)
            if(!actual.contains(new CheaperDerivative(d))){
                addMessage("Incorrect reoffering - price too high");
                return false;
            }
        return true;
    }
    
    // if there is a buy transaction, then the derivative bought must be on sale and player should not buy from itself                
    protected boolean correctBuy(Store store){
        List<Derivative> dersForSale = DerivativesFinder.findDerivativesForSale(store.stores);
        List<Transaction> buyTs = pTrans.transactions.filter(new TransactionComparatorByTType(new Buy()));
        
        for(Transaction buyTrans:buyTs){
            if(!dersForSale.contains(new SameDerivative(buyTrans.deriv))){
                addMessage("Non-existant derivative bought");
                return false;
            }
            if(buyTrans.deriv.seller.equals(pTrans.player.id)){
                addMessage("Player bought from itself :: "+buyTrans.deriv.print());
                return false;
            }
        }
        return true;
    }
    
    protected boolean emptyStoreBuyReoffer(Store store){
        // if the store is empty, then there should not be any buy or reoffer    
        // What we really want is whether there are any derivatives -from others- to reoffer. - acdubre
        if(store.stores.isEmpty() || DerivativesFinder.findDerivativesForSaleByOthers(store.stores, pTrans.player.id).isEmpty()){
            for(Transaction trans: pTrans.transactions){
                if(trans.ttype.getClass().equals(Buy.class)||trans.ttype.getClass().equals(Reoffer.class)){
                    addMessage("Buy/Reoffer from an empty store");
                    return false;
                }
            }
        }
        return true;
    }
    
    /** Checks if create rules are followed */
    protected boolean isCreateRuleFollowed(Store store, GameType gt) {
        // there must be a create transaction
        if(!pTrans.transactions.contains(new TransactionComparatorByTType(new Create()))){
            addMessage("Player did not create a derivative");
            return false;
        }else{
            List<Transaction> createTrans = pTrans.transactions.filter(new TransactionComparatorByTType(new Create()));
            // ** Comment out to not enforce limited Create transactions 
            if(createTrans.length() > config.MaxCreates){
                addMessage("Created too many derivative");
                return false;
            }
            /**/
            
            for(Transaction cTrans:createTrans){
                List<Derivative> dersForSale = DerivativesFinder.findDerivativesForSale(store.stores);
                // created derivative must not be of existing types
                if(dersForSale.contains(new SameDerivativeByType(cTrans.deriv))){
                    addMessage("Created a derivative of an existing type");
                    return false;
                }
                // Created derivatives must have 0 <= price <= 1
                if(cTrans.deriv.price.val > 1 || cTrans.deriv.price.val < 0){
                    addMessage("Illegally priced derivative");
                    return false;
                }
                // Check additional rules based on GameType
                if(gt.compareTo(GameType.BASEBALL) == 0) { }
                else if(gt.compareTo(GameType.FASTPITCH2)==0) 
                {
                	if(cTrans.deriv.type.instances.length() > 2)
                	{
                		addMessage("Violated Fastpitch Level 2 Independent creation rule.");
                		return false;
                	}
                }
                else if(gt.compareTo(GameType.FASTPITCH) == 0) { }
                else if(gt.compareTo(GameType.SLOWPITCH) == 0) 
                {
                	return implicationChain(cTrans.deriv.type);
                }
                else if(gt.compareTo(GameType.TBALL) == 0) 
                {
                	if(cTrans.deriv.type.instances.length() > 1)
                	{
                		addMessage("Violated T-Ball creation rule.");
                		return false;
                	}
                }
            }
        }
        return true;
    }
    
    /** Is t and implication chain? */
    protected boolean implicationChain(Type t)
    {
    	TypeInstance prev = null;
    	List<TypeInstance> slti = t.instances.sort(new TypeInstanceComparator());
    	for(TypeInstance cur : slti){
    		if(prev != null){
    			if(!implies(prev, cur)){
    				addMessage("Violated Slowpitch creation rule.");
    				return false;
    			}
    		}
    		prev = cur;
    	}
    	return true;
    }
    
    /** Does a imply b? */
    protected static boolean implies(TypeInstance a, TypeInstance b){
    	return (a.r.v & b.r.v) == a.r.v;
    }
    
    /** Checks if delivery rules are followed */
    protected boolean isDeliveryRuleFollowed(Store store) {
        //see whether the expected and actual lists of derivatives that need RM are same
        List<Transaction> actualDeliveryTrans = pTrans.transactions.filter(new TransactionComparatorByTType(new Deliver()));
        List<Derivative> actualDeliveryDers = actualDeliveryTrans.map(new List.Map<Transaction, Derivative>(){
            public Derivative map(Transaction t){ return t.deriv; }});
        
        List<Derivative> expectedDeliveryDers = DerivativesFinder.findDersThatNeedRM(store.stores, pTrans.player);
        if(!actualDeliveryDers.same(expectedDeliveryDers, new DerivativeComp())){
            addMessage("Incorrect number of delivered Derivatives");
            return false;
        }else{
            for(Derivative d : actualDeliveryDers)
                if(!validRawMaterial(d))return false;
            return true;
        }
    }
    
    /**
     * Checks that:
     * 1) d's optional RawMaterial exists
     * 2) All Constraints are of the correct type
     * 3) All Constraints use unique variables
     */
    protected boolean validRawMaterial(Derivative d){
        if(!d.optraw.isSome()){
            addMessage("No RawMaterial found in delivered Derivative");
            return false;
        }else{
            for(Constraint c : d.optraw.inner().instance.cs){
                // If we find a Constraint with an invalid type
                if(!d.type.instances.contains(new TypeInstance(c.r))){
                    addMessage("RawMaterial with Type not specified in Derivative found");
                    return false;
                }
                List<Variable> temp = c.vs;
                
                if(temp.length() != 3){
                    addMessage("Constraint with too few Variables found");
                    return false;
                }else{
                    Variable v;
                    while(temp.length() > 0){
                        v = temp.top();
                        temp = temp.pop();
                        // If we remove the first element and the list of vars still contains that element
                        if(temp.contains(v)){
                            addMessage("Duplicate Variable found in Constraint: " + c.print() + "");
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }
    
    /** Checks if finishing rules are followed */
    protected boolean isFinishRuleFollowed(Store store){
        //see whether the expected and actual lists of derivatives that need RM are same
        List<Transaction> actualFinishTrans = pTrans.transactions.filter(new TransactionComparatorByTType(new Finish()));
        List<Derivative> actualFinishDers = List.create();
        for(Transaction trans: actualFinishTrans){
            actualFinishDers = actualFinishDers.push(trans.deriv);
        }
        List<Derivative> expectedFinishDers = DerivativesFinder.findDersThatNeedFinishing(store.stores, pTrans.player);
        if(!actualFinishDers.same(expectedFinishDers,new DerivativeComp())){
            addMessage("Incorrect number of finished derivatives");
            return false;
        }
        for(Derivative der : actualFinishDers){
            Assignment a = der.optfinished.inner().ip.assignment;
            RawMaterial rm = der.optraw.inner();
            if(!SufficientAssignment.good(rm, a)){
                addMessage("Insufficient assignment");
                return false;
            }
        }
        return true;
    }
}