/*
 * Copyright (C) 2004 Jennifer Wortman, Eugene Nudelman, Kevin Leyton-Brown, Yoav Shoham.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package edu.stanford.multiagent.gamer;

import java.util.*;
import java.io.*;

/**
 * Abstract class that describes the basic game entity.
 */

public abstract class Game extends ParameterizedObject
{

    // ------------------------------------------------
    // Things related to game data

    private int nPlayers;
    private int[] nActions;
    private String description;
    private String gameName;

    // For normalization
    private boolean minAndMaxSet = false;
    private double oldMax;
    private double oldMin;

    protected static final double DEFAULT_LOW = -100;
    protected static final double DEFAULT_HIGH = 100;

    /**
     * Returns Game Description (Name, Params, etc)
     */
    public String getDescription()
    {
	return description;
    }

    /**
     * Sets the description string for the game which will be
     * output as part of the help string.
     *
     * @param description a description of the game
     */
    protected void setDescription(String description)
    {
	this.description=description;
    }


    /**
     * Returns just the name of the game.  Should not have
     * any line breaks.
     */
    public String getName()
    {
	return gameName;
    }

    /**
     * Sets the name of the game.
     *
     * @param name the name of the class of games generated by
     *             a given game class
     */
    protected void setName(String name)
    {
	this.gameName = name;
    }

    
    /**
     * Returns the number of players in this game
     */
    public int getNumPlayers()
    {
	return nPlayers;
    }

    /**
     * Sets the number of players in the game.
     *
     * @param nPlayers the number of players
     */
    protected void setNumPlayers(int nPlayers)
    {
	this.nPlayers=nPlayers;
    }

    /**
     * Returns the number of actions of the given player
     * or a vector of Integers with the numbers of actions.
     * 
     * @param player the index of the player whose actions should
     *               be returned
     */
    public int getNumActions(int player)
    {
	return nActions[player];
    }

    /**
     * Returns an array containing the number of actions that each
     * player has.
     */
    public int[] getNumActions()
    {
	return nActions;
    }


    /**
     * Sets same number of actions for each player.  The number
     * of players in the game must have been set before calling this.
     *
     * @param nActions the number of actions each player has
     */
    protected void setNumActions(int nActions)
	throws Exception
    {
	int players = getNumPlayers();
	int val[] = new int[players];
	
	for(int i = 0; i < players; i++)
	    val[i] = nActions;

	setNumActions(val);
    }


    /**
     * Sets the number of actions for each player when each player
     * may have a different number of actions.
     *
     * @param nActions an array containing the number of actions each
     *                 player has
     * @throws Exception
     */
    protected void setNumActions(int[] nActions) 
	throws Exception
    {
	if(nActions.length!=nPlayers)
	    Global.handleError("Action profile length <> nPlayers!");
	this.nActions = nActions;
    }

    /**
     * Writes output in the format specific to this game.
     *
     * @param out a PrintWriter to which output can be written
     * @throws UnsupportedOperationException if the given game subclass
     *         does not support proprietary output
     */
    public void writeGame(PrintWriter out) throws UnsupportedOperationException
    {
	throw new UnsupportedOperationException("This game does not support proprietary output!");
    }


    /**
     * The consctructor.  Must be called by all subclasses.
     *
     * @throws Exception
     */
    protected Game()
	throws Exception
    {
	super();

	this.description="";
	this.gameName = "";
    }


    /**
     * Initializes using preset parameter values
     *
     * @throws Exception
     */
    public void initialize()
	throws Exception
    {
	super.initialize();
    }

    
    /**
     * Returns the help screen
     */
    public String getHelp()
    {
	StringBuffer buff=new StringBuffer();
	buff.append(Global.wrap(getGameHelp(), 70));

	buff.append("\n\nGame Parameters:\n");
	buff.append(Global.wrap(parameters.getPrintableInfo(), 70));


	return buff.toString();
    }


    /**
     * Adds range help information to games which use the range
     * DEFAULT_LOW through DEFAULT_HIGH.
     */
    public String getRangeHelp() 
    {
	return "\n\nBy default, all payoffs will fall in the range " +
	    "[" + (int) DEFAULT_LOW + ", " + (int) DEFAULT_HIGH + 
	    "].  This range can be altered by setting the " +
	    "normalization options or using integer payoffs.";
    }
	


    /**
     * The generate interface.  Calls the game specific doGenerate
     * function.
     */

    public void generate() throws Exception
    {
	StringBuffer buff = new StringBuffer();
	buff.append("Game Parameter Values:\n");
	buff.append("Random seed:\t" + Global.randSeed + "\n");
	buff.append("Cmd Line:\t" + GameOutput.arrayToString(Global.gArgs, " ") + "\n");
	buff.append("Players:\t" + nPlayers + "\n");
	buff.append("Actions:\t");
	for(int i=0; i<nPlayers; i++)
	    buff.append(nActions[i] + (i==nPlayers - 1 ? "\n" : " "));

	buff.append(getParamDescription("\n"));
	setDescription(getDescription() + buff.toString());

	minAndMaxSet = false;

	doGenerate();
    }


    // ----------------------------------------------------------------

    // The following four functions are used for parsing the parameters
    // for number of players and numbers of actions under different
    // cicumstances.


    /**
     * Parses only the parameter for the number of actions,
     * allowing either the same number for all players, or
     * different numbers for each.
     *
     * @throws Exception
     */
    protected void parseActions()
	throws Exception
    {
	int val[] = Global.parseIntArray((Vector)getParameter(Game.actions.name));
	
	if(val.length==1)
	    {
		int[] a=new int[getNumPlayers()];
		for(int i=0; i<a.length; i++)
		    a[i]=val[0];
		val=a;
	    }
	else if (val.length!=getNumPlayers())
	    Global.handleError("Must have one #actions for every player!");

	setNumActions(val);
    }



    /**
     * Parses action/player parameters and sets them.
     *
     * @throws Exception
     */
    protected void parsePlayersActions() 
	throws Exception
    {
	if(!parameters.isParamSet(Game.players.name))
	    Global.handleError("Required parameter missing: players");
	if(!parameters.isParamSet(Game.actions.name))
	    Global.handleError("Required parameter missing: actions");

	setNumPlayers((int)getLongParameter(Game.players.name));
	
	parseActions();
    }


    /**
     * Parses only the parameter for number of actions.  Requires
     * that all players have the same number of actions, in other
     * words, number of actions is set by a single integer.
     * 
     * @throws Exception if it is not the case that all players have the
     *                   same number of actions
     */
    protected void parseSameNumberActions()
	throws Exception
    {
	int val[] = Global.parseIntArray((Vector)getParameter(Game.actions.name));
	
	if(val.length==1) {
		int[] a=new int[getNumPlayers()];
		for(int i=0; i<a.length; i++)
		    a[i]=val[0];
		val=a;
	}
	else { 
	    if (val.length != getNumPlayers())
		Global.handleError("Must have one #actions for every player!");
	    int value = val[0];
	    for (int i = 1; i < getNumPlayers(); i++) {
		if (val[i] != value)
		    Global.handleError("All players must have equal number of actions.");
	    }
	}

	setNumActions(val);
    }



    /**
     * Parses action/player parameters and sets them.  Requires that all
     * players have the same number of actions.
     *
     * @throws Exception if it is not the case that all players have the 
     * same number of actions
     */
    protected void parsePlayersSameNumberActions() 
	throws Exception
    {
	if(!parameters.isParamSet(Game.players.name)) {
	    System.err.println("ERROR: Required parameter missing: players");
	    System.err.println(Global.getHelp());
	    System.err.println(getHelp());
	    System.exit(1);
	}
	if(!parameters.isParamSet(Game.symActions.name)) {
	    System.err.println("ERROR: Required parameter missing: actions");
	    System.err.println(Global.getHelp());
	    System.err.println(getHelp());
	    System.exit(1);
	}

	setNumPlayers((int)getLongParameter(Game.players.name));

	parseSameNumberActions();
    }


    /**
     * Collects payoffs into a vector. Should be used with caution.
     * Can be overwritten in subclasses if it is more efficient
     * to get all payoffs for each outcome at once.
     *
     * @param outcome an array containing the action choices of 
     *        all players at the given outcome
     */
    public Vector getPayoff(int[] outcome)
    {
	int players = getNumPlayers();
	Vector payoffVector = new Vector(players);

	for (int i = 0; i < players; i++) {
	    Double payoff = new Double(getPayoff(outcome, i));
	    payoffVector.add(i, payoff);
	}

	return payoffVector;
    }



    /**
     * Collects all payoffs (one at a time) and figures out the
     * min and max.  These will later be used for normalization.
     * This is very inefficient since it goes through every payoff
     * in the game.  Should be overwritten in subclasses when
     * normalization can be handled more efficiently.
     * <p>
     * Note that only base double max and min values are calculated
     * here, not the converted integer max and min.
     */
    public void setNormMinAndMax()
    {
	int players = getNumPlayers();

	Outcome o = new Outcome(getNumPlayers(), getNumActions());
	o.reset();

	// Initialize the min and max to the first payoff given.
	oldMax = getPayoff(o.getOutcome(), 0);
	oldMin = getPayoff(o.getOutcome(), 0);
	
	for(o.reset(); o.hasMoreOutcomes(); o.nextOutcome()) {
	    for (int i = 0; i < getNumPlayers(); i++) {
		double nextPay = getPayoff(o.getOutcome(), i);
		if (nextPay > oldMax) {
		    oldMax = nextPay;
		} else if (nextPay < oldMin) {
		    oldMin = nextPay;
		}
	    }
	}

	minAndMaxSet = true;
    }



    /**
     * Formats and outputs the payoff according to set flags.
     *
     * @param outcome an array containing all players' action
     *        choices at the given outcome
     * @param player tha player whose payoff should be returned
     * @throws Exception 
     */
    public String getOutputPayoff(int[] outcome, int player)
       throws Exception
    {
      return getOutputPayoff(getPayoff(outcome, player));
    }


    /**
     * Formats and outputs a payoff value according to set flags.
     *
     * @param payoff the value of the payoff to be formatted
     * @throws Exception
     */
    public String getOutputPayoff(double payoff)
	throws Exception
    {
	boolean intFlag = Global.params.getBooleanParameter(intPayoffs.name);
	boolean normFlag = Global.params.getBooleanParameter(pNormalize.name);

	double newMin, newMax;

	// If we are using normalization, call the default function
	// which will set up Global parameters oldMin and oldMax 
	// which hold the unnormalized min and max.  If these can be
	// set more efficiently in another way by some class then
	// setNormMinAndMax() can be overwritten.
	if (normFlag) {

	    if (!minAndMaxSet) 
		setNormMinAndMax();

	    newMin = Global.params.getDoubleParameter(pMinPayoff.name);
	    newMax = Global.params.getDoubleParameter(pMaxPayoff.name);
	 
	    if (oldMax - oldMin == 0.0) {
		Global.handleError("Tried to normalize with " +
				   "oldMax = oldMin");
	    }

	    payoff = ((payoff - oldMin) * ((newMax - newMin) /
		      (oldMax - oldMin))) + newMin;
	}

	if(intFlag)
	    {
		// Note that if the payoff has already been normalized,
		// it is the normalized value that will be further
		// converted to integer form.  (It would not make sense
		// to do this in reverse order as the values would no
		// longer be integers.  (Here they are no longer in the
		// normalized range, but it makes MORE sense.))

		long mult = Global.params.getLongParameter("int_mult");

		if(Math.abs(payoff)* mult >= Long.MAX_VALUE / mult)
		    Global.handleError("Payoffs are  too big for int output!");
		long p=Math.round(mult*payoff);
		return String.valueOf(p);
	    }
	else
	    {
		return String.valueOf(payoff);
	    }
    }



    /**
     * Formats and outputs the payoff after normalization according
     * to set flags.
     *
     * @param outcome an array containing all players' action
     *        choices at the given outcome
     * @param player tha player whose payoff should be returned
     */
    public double getNormPayoff(int[] outcome, int player)
    {
	boolean intFlag = Global.params.getBooleanParameter(intPayoffs.name);
	boolean normFlag = Global.params.getBooleanParameter(pNormalize.name);

	double newMin, newMax;

	double payoff = getPayoff(outcome, player);

	// If we are using normalization, call the default function
	// which will set up Global parameters oldMin and oldMax 
	// which hold the unnormalized min and max.  If these can be
	// set more efficiently in another way by some class then
	// setNormMinAndMax() can be overwritten.
	if (normFlag) {

	    if (!minAndMaxSet) 
		setNormMinAndMax();

	    newMin = Global.params.getDoubleParameter(pMinPayoff.name);
	    newMax = Global.params.getDoubleParameter(pMaxPayoff.name);
	 
	    if (oldMax - oldMin == 0.0) {
		Global.handleError("Tried to normalize with " +
				   "oldMax = oldMin");
	    }

	    payoff = ((payoff - oldMin) * (newMax - newMin) /
		      (oldMax - oldMin)) + newMin;
	}

	
	return payoff;
    }



    /**
     * Formats automatically to normalization between 0
     * and 1 with double payoffs, regardless of parameters.
     *
     * @param outcome an array containing all players' action
     *        choices at the given outcome
     * @param player tha player whose payoff should be returned
     */
    public double getAutoNormPayoff(int[] outcome, int player)
    {
	double newMin, newMax;
	double payoff = getPayoff(outcome, player);

	if (!minAndMaxSet) 
	    setNormMinAndMax();
	
	newMin = -1.0;
	newMax = 1.0;

	// deal with this case later
	if (oldMax - oldMin != 0.0) {
	    payoff = ((payoff - oldMin) * (newMax - newMin) /
		      (oldMax - oldMin)) + newMin;
	}
	
	return payoff;
    }




    // ---------------------------------------------------------------


    // --------------------------------------------
    // -- Abstract things to be done in subclasses
    // --------------------------------------------

    /**
     * Returns a payoff for a given player.
     *
     * @param outcome array of integers representing which actions
     *                have been chosen by each player
     * @param player the player whose payoff to return
     */
    public abstract double getPayoff(int[] outcome, int player);


    /**
     * Return help screen information about the given game class.
     */
    protected  abstract String getGameHelp();


    //---------------------------------------------
    // -- Static stuff
    //---------------------------------------------
    
    protected static Parameters.ParamInfo players;
    protected static Parameters.ParamInfo actions;
    protected static Parameters.ParamInfo symActions;
    protected static Parameters.ParamInfo intPayoffs;
    protected static Parameters.ParamInfo intMult;
    protected static Parameters.ParamInfo pMinPayoff;
    protected static Parameters.ParamInfo pMaxPayoff;
    protected static Parameters.ParamInfo pNormalize;

    static {
	players = new Parameters.ParamInfo("players", Parameters.ParamInfo.LONG_PARAM, new Long(2), new Long(100), "the number of players. Must be >= 2 and <= 100.", true,null);

	actions = new Parameters.ParamInfo("actions", Parameters.ParamInfo.VECTOR_PARAM, new Long(1), new Long(500), "number of actions.  Can be a single number, or a list of numbers - one for each player.  Must be >= 1 and may be as large as 500, but note that using large numbers of actions will result in exponentially large games.  For most games, the number of actions should be much smaller.", true, null);

	// Alternate option for actions parameter, displays message
	// that all players must have the same number of actions
	symActions = new Parameters.ParamInfo("actions", Parameters.ParamInfo.VECTOR_PARAM, new Long(1), new Long(500), "number of actions.  All players must have the same number of actions in this game.  Must be >= 1 and may be as large as 500, but note that using large numbers of actions will result in exponentially large games.  For most games, the number of actions should be much smaller.", true, null);

	// Options for  output payoff handling
	intPayoffs = new Parameters.ParamInfo("int_payoffs", Parameters.ParamInfo.BOOLEAN_PARAM, null, null, "generate integral payoffs.", false, Boolean.FALSE);
	intMult = new Parameters.ParamInfo("int_mult", Parameters.ParamInfo.LONG_PARAM, new Long(1), new Long(1000000000000L), "a multiplier used before converting payoffs to integers. Defaults to 10000.",false, new Long(10000));
	
	pNormalize = new Parameters.ParamInfo("normalize", Parameters.ParamInfo.BOOLEAN_PARAM, null, null, "use normalization. Note that normalization can result in some error in the last digit of the decimal payoff.", false, Boolean.FALSE);

	pMinPayoff = new Parameters.ParamInfo("min_payoff", Parameters.ParamInfo.DOUBLE_PARAM, new Double(-Double.MAX_VALUE), new Double(Double.MAX_VALUE), "minimum payoff in matrix, set if normalization is desired.", false);

	pMaxPayoff = new Parameters.ParamInfo("max_payoff", Parameters.ParamInfo.DOUBLE_PARAM, new Double(-Double.MAX_VALUE), new Double(Double.MAX_VALUE), "maximum payoff in matrix, set if normalization is desired.", false);

    }


}



