// This player reads the training examples collected by the ExampleCollector
// agent and uses them to create FOUR decision trees: the decision made
// by each tree is:
//
//     Is it a good idea to move in the direction X?
//
// where X is one of {N, E, W, S}, eg, the four main compass directions.

// When more than one decision tree says its direction is a good one,
// you can use any method you wish for picking which suggestion to follow.
// (In AI this is called "conflict resolution.")  A sample solution
// appears in the code below.
//
// The code below also provides a solution for the case when NONE of the
// directions are recommended; you can leave that as is or extend it
// in any manner that you feel is more intelligent.


// In this solution, the only features considered are:
//
//     What type of object is being sensed in direction D?
//
// where D ranges from 0 to Sensors.NUMBER_OF_SENSORS.

// YOUR TASK IS TO EXTEND THIS CODE (in a class named using your
// login rather than mine) SO THAT IT CONSIDERS FEATURES INVOLVING
// DISTANCES TO THE SENSED OBJECTS.  Since distances are continuous-valued
// rather than discrete, you'll have to devise a mechanism for creating
// discrete (eg, use thresholds, such as distance < 5 or divide
// into intervals, such as: distance < 5, 5 <= distance < 10, and
// distance >= 0 - be sure to use DISJOINT but abutting intervals
// so that each distance reading falls into one and only one interval.
// Finally, rather than using fixed numbers such as 5 and 10,
// you should use multiples of Utils.getObjectRadius()
//   - radii are measured center-to-center, so touching objects
//     are 2 * Utils.getObjectRadius() apart, while an agent just
//     barely touches the wall when it is Utils.getObjectRadius()
//     away from it.
// NOTE: in the HW writeup, I mistakenly said use "Utils.getPlayerStepSize()"
//       Using either is fine (they return the same value currently,
//       and we wont change that fact during grading).

// (ID3/C4.5 has a mechanism for internally choosing its thresholds,
// but we wont do that in order to reduce complexity.  But do be
// aware that our 'brute force' method of selecting the thresholds
// manually is not the start of the art.)


// Some setup code is in here that should help you get started.
// You should read through all of it to see where you need to add 
// code for handling the distance-related features.

// IT IS ALSO FINE FOR YOU TO COMPLETELY DESIGN YOUR DECISION-TREE
// LEARNER FROM 'SCRATCH,' NOT USING ANY OF THE PROVIDED CODE.
// Eg., you need not have a one-to-one match between features and
// sensor directions; instead you could, say, have features that
// summarized information across several sensors.
// Just be sure you match the constructor method so that the TAs can 
// create instances of your agent.  Also, be sure your code prints out 
// your N (north) tree to depth 3 and reports its size (number of
// interior and leaf nodes).  And you'll still need to use
// some sort of distance-related features in your solution.

// FINALLY, DON'T FORGET TO REPLACE Shavlik WITH YOUR LOGIN NAME
// IF YOU USE THIS FILE AS YOUR STARTING POINT. 

import java.util.*; // Need this for Vector's.
import AgentWorld.*;

public final class ShavlikID3player extends Player
{ private Sensors sensors, prevSensors;
  private double  reward;
  private boolean debugging = false;
  private DecisionTree N_decisionTree, E_decisionTree, W_decisionTree, S_decisionTree;
  private int N_positiveExamplesCount, N_negativeExamplesCount,
              E_positiveExamplesCount, E_negativeExamplesCount,
              W_positiveExamplesCount, W_negativeExamplesCount, 
              S_positiveExamplesCount, S_negativeExamplesCount;

  // As a VERY CRUDE way of addressing the 'overfitting' problem,
  // we can limit the depth of our decision trees.
  // Use Integer.MAX_VALUE to have NO limit.
  private final int maxTreeDepth = Integer.MAX_VALUE;

  // The constructor method.
  public ShavlikID3player(AgentWindow agentWindow, boolean showSensors, String examplesFile) 
  {
    super(agentWindow); 
    setShowSensors(showSensors); // Display this player's sensors?

    // Upon creation of our decision-tree player, it reads the
    // training examples and then induces some decision trees.

    // First, collect the training examples for the four main compass directions.
    // ReadTrainingExamples() collects all of the examples from the given
    // file that say the give direction is good or bad.  Examples are
    // instances of the LabeledExample class, which is built into the
    // AgentWorld package - the code for the LabeledExample class is made
    // available so that you can see its accessor methods, BUT DO NOT
    // INCLUDE IT IN THE SET OF YOUR JAVA FILES FOR HW2, since it
    // is already included.)

    // You're free to use more directions: the valid ones
    // are: N, NE, E, SE, S, SW, W, NE, and @ ('stand still').
    // These correspond to the cells in the 3x3 grid used
    // during the collection of training examples.
    Vector N_examplesVector = readTrainingExamples(examplesFile, "N"),
           E_examplesVector = readTrainingExamples(examplesFile, "E"),
           W_examplesVector = readTrainingExamples(examplesFile, "W"),
           S_examplesVector = readTrainingExamples(examplesFile, "S");

    // The following methods illustrate how one can compute statistics about examples,
    // plus they provides some relevant info here.
    N_positiveExamplesCount = countPositiveExamples(N_examplesVector);
    N_negativeExamplesCount = countNegativeExamples(N_examplesVector);
    E_positiveExamplesCount = countPositiveExamples(E_examplesVector);
    E_negativeExamplesCount = countNegativeExamples(E_examplesVector);
    W_positiveExamplesCount = countPositiveExamples(W_examplesVector);
    W_negativeExamplesCount = countNegativeExamples(W_examplesVector);
    S_positiveExamplesCount = countPositiveExamples(S_examplesVector);
    S_negativeExamplesCount = countNegativeExamples(S_examplesVector);

    System.out.println("N: " + N_positiveExamplesCount + " pos and " 
		       + N_negativeExamplesCount + " neg training examples");
    System.out.println("E: " + E_positiveExamplesCount + " pos and " 
		       + E_negativeExamplesCount + " neg training examples");
    System.out.println("W: " + W_positiveExamplesCount + " pos and " 
		       + W_negativeExamplesCount + " neg training examples");
    System.out.println("S: " + S_positiveExamplesCount + " pos and " 
		       + S_negativeExamplesCount + " neg training examples");

    // Next, use these training examples to construct a decision tree for each direction.

    // We'll play it safe and our default will always be that a move is bad.
    // Given our overall design, this alteration of the standard ID3 algo makes sense.
    N_decisionTree = createDecisionTree(N_examplesVector,
	         			createFullFeatureSet(),
				        false);
    E_decisionTree = createDecisionTree(E_examplesVector,
	 			        createFullFeatureSet(),
				        false);
    W_decisionTree = createDecisionTree(W_examplesVector,
				        createFullFeatureSet(),
				        false);
    S_decisionTree = createDecisionTree(S_examplesVector,
				        createFullFeatureSet(),
				        false);

    // Print out one of the tree's for debugging (and grading) purposes.
    // Plus this will let you know its ok to push the START button.
    System.out.println("\nThe decision tree for moving North:");
    if (N_decisionTree != null)
    { // Print out the top few levels of one decision tree.
      N_decisionTree.printTree(4); 
    }
    if (false) // Allow all the trees to be printed if desired.
    {
      System.out.println("\nThe decision tree for moving East:");
      if (E_decisionTree != null) E_decisionTree.printTree(4);
      System.out.println("\nThe decision tree for moving West:");
      if (W_decisionTree != null) W_decisionTree.printTree(4);
      System.out.println("\nThe decision tree for moving South:");
      if (S_decisionTree != null) S_decisionTree.printTree(4);
    }
    System.out.println("");

    // Report some size info about the learned trees.
    if (N_decisionTree != null)
    {
      System.out.println("N tree: " + N_decisionTree.countInteriorNodes()
			 + " interior nodes and " + N_decisionTree.countLeaves()
			 + " leaves.");
    }
    if (E_decisionTree != null)
    {
      System.out.println("E tree: " + E_decisionTree.countInteriorNodes()
			 + " interior nodes and " + E_decisionTree.countLeaves()
			 + " leaves.");
    }
    if (W_decisionTree != null)
    {
      System.out.println("W tree: " + W_decisionTree.countInteriorNodes()
			 + " interior nodes and " + W_decisionTree.countLeaves()
			 + " leaves.");
    }
    if (S_decisionTree != null)
    {
      System.out.println("S tree: " + S_decisionTree.countInteriorNodes()
			 + " interior nodes and " + S_decisionTree.countLeaves()
			 + " leaves.");
    }
    
  }
  
  // This method use the induced decision trees to control your agent.
  public void run()
  { boolean goodDirections[] = new boolean[4]; // This will hold the recommended directions.
  
  try
  {
    while(threadAlive()) // Basically, loop forever.
    { String chosenDirection = null;  // The compass direction to move.
      int    numberOfGoodDirections = 0; // Count how many directions look good.
      
      sensors     = getSensors();     // See what the world looks like.
      
      // The sketch below shows how you might use your four decision-trees
      // It's fine to use this simple 'control strategy,' 
      // but do feel free to implement a more intelligent one.
      
      // Erase the old recommendations.
      for(int i = 0; i < 4; i++) goodDirections[i] = false;
      
      if      (N_decisionTree != null && N_decisionTree.makeDecision(sensors))
      {
	chosenDirection = "N"; // Remember this, in case it is the only good one.
	numberOfGoodDirections++; // But if there are multiple suggestions,
	goodDirections[0] = true; // we need this info to (randomly) choose one.
      }

      if (E_decisionTree != null && E_decisionTree.makeDecision(sensors))
      {
	chosenDirection = "E";
	numberOfGoodDirections++;
	goodDirections[1] = true;
      }

      if (W_decisionTree != null && W_decisionTree.makeDecision(sensors))
      {
	chosenDirection = "W";
	numberOfGoodDirections++;
	goodDirections[2] = true;
      }

      if (S_decisionTree != null && S_decisionTree.makeDecision(sensors))
      {
	chosenDirection = "S";
	numberOfGoodDirections++;
	goodDirections[3] = true;
      }
      
      if (numberOfGoodDirections > 1) // Have to 'resolve this conflict'
	                              // among the suggested moves.
      { // Do this by repeatedly looking at a random cell in goodDirection[]
	// until a 'true' is found.  (This isn't all that elegant nor
	// robust of a design, but at least it is simple.)
	int codedChosenDirection;
	
	do
	{
	  codedChosenDirection = Utils.getRandomIntInRange(0, 3);
	}
	while(!goodDirections[codedChosenDirection]);

	// Now map back to the direction's name.
	switch(codedChosenDirection){
	case 0: chosenDirection = "N"; break;
	case 1: chosenDirection = "E"; break;
	case 2: chosenDirection = "W"; break;
	case 3: chosenDirection = "S"; break;
	}
      }
      
      if (chosenDirection != null) // A good direction found?
      {
	setMoveVector(convertCompassDirectionToRadians(chosenDirection));
      }
      // Fill free to alter the next two ELSE's, which
      // handle the case when no move looks good.
      else if (Math.random() < 0.90)
      { // If no direction looks good, 90% of the time stand still.
	setMoveVector(-1.0);
      }
      else // Occasionally, take a random step.
      {
	setMoveVector(2 * Math.PI * Math.random());
      }
    }
  }
  catch(Exception e)
  {
    e.printStackTrace(System.err);
    Utils.errorMsg(e + ", " + toString() + " has stopped running");
  }

  Utils.errorMsg(getName() + " is no longer alive for some reason");
  // This should never happen, so exit the simulator if it does.
  Utils.exit(-1);
  }


  // **********************************************************************
  //  Implement a slight variant of Figure 18.7 tailored for the
  //  Agent World.  The variations are:
  //
  //       1) use FALSE as the default leaf node
  //          (this better matches the Agent World and our
  //          use of four decision trees)
  //
  //       2) allow the depth of the tree being constructed
  //          to be limited (this provides a crude mechanism
  //          for dealing with 'overfitting')
  //
  // **********************************************************************
  DecisionTree createDecisionTree(Vector examples, Vector features,
				  boolean defaultLabel)
  {
    return createDecisionTree(examples, features, defaultLabel, 1);
  }
  DecisionTree createDecisionTree(Vector examples, Vector features,
				  boolean defaultLabel, int currentDepth)
  {
    if (examples == null || examples.size() <= 0)
    { // If no examples, make a leaf node labeled with the default.
      return new LeafNode(defaultLabel, 0);
    }

    int numberOfPositiveExamples = countPositiveExamples(examples),
        numberOfNegativeExamples = countNegativeExamples(examples);
    
    if (numberOfNegativeExamples <= 0)
    { // Are all the examples GOOD moves?  If so, have separated the data; make a leaf node.
      return new LeafNode(true, numberOfPositiveExamples);
    }
    if (numberOfPositiveExamples <= 0)
    { // Are all the examples BAD moves?
      return new LeafNode(false, numberOfNegativeExamples);
    }

    // The following isn't part of the standard ID3, but we'll
    // use it as a crude mechanism for avoiding overfitting.
    if (currentDepth >= maxTreeDepth) // Reached the depth limit?
    {
      if (numberOfPositiveExamples > numberOfNegativeExamples)
      {
        return new LeafNode(true, numberOfPositiveExamples, numberOfNegativeExamples);
      }
      else return new LeafNode(false, numberOfNegativeExamples, numberOfPositiveExamples);
                          
    }

    if (features == null || features.size() <= 0)
    { // If out of features, usually we'd label a leaf node with the majority class.
      // However, in our design it makes sense to play it safe and default to "bad move."
      return new LeafNode(false, numberOfNegativeExamples, numberOfPositiveExamples);
    }

    // The tough case: need to choose the best feature from the candidates provided.
    Feature bestFeature       = chooseBestFeature(examples, features);
    Vector  remainingFeatures = removeFeature(bestFeature, features);

    // Now, recursively build the subtree whose root is the best feature.
    if      (bestFeature instanceof ObjectType_Feature)
    { ObjectType_Feature chosenFeature = (ObjectType_Feature)bestFeature;
      ObjectType_InteriorNode subtree // Create a new interior node.
        = new ObjectType_InteriorNode(bestFeature.getSensorDirection());
      Vector  matchingExamples;

      // Create all the subtrees.
      for(int i = 0; i < Sensors.NUMBER_OF_OBJECT_TYPES; i++)
      {
        matchingExamples
          = chosenFeature.collectMatchingExamples(examples, i);
        subtree.setChildForObjectType(i,
                                      createDecisionTree(matchingExamples,
                                                         remainingFeatures,
                                                         false, currentDepth + 1));
      }
      return subtree;
    }
    // ***** You'll need to deal with your other feature types here. *****
    //       (Ie, add some ELSE-IF's.)
    else 
    {
      Utils.println("Unknown feature type: " + bestFeature.toString());
      return null;
    }
  }

  // Use information gain to choose the best feature for splitting
  // these training examples.  (Since the info in this set of examples
  // [before splitting] is constant with respect to each feature,
  // we don't need to compute that.  Instead we only need to find
  // which feature produces subsets of the examples whose remaining
  // info is LEAST, which will then maximize the info GAINED by checking
  // the value of a feature.)
  Feature chooseBestFeature(Vector examples, Vector features)
  {
    if (features == null)
    {
      Utils.println("Calling chooseBestFeature, but features=null.");
      return null;
    }

    int length = features.size();
    double leastInfoLeft = Double.MAX_VALUE;
    Feature bestFeature = null; // Reset these as better features found.

    for(int i = 0; i < length; i++) // Look at each feature.
    { Feature featureSpec = (Feature)(features.elementAt(i));
      double remainingInfo = 0.0; // Compute the info left AFTER splitting
                                  // on this feature.

      if      (featureSpec instanceof ObjectType_Feature)
      { ObjectType_Feature chosenFeature = (ObjectType_Feature)featureSpec;

        // Compute info of the subset of examples that will follow each
        // outgoing arc.  (For efficiency and simplicity, also have
        // computeInfo WEIGHT the info by the fraction of examples that
        // follow this arc.)
        remainingInfo += chosenFeature.computeInfo(examples, Sensors.NOTHING);
        remainingInfo += chosenFeature.computeInfo(examples, Sensors.WALL);
        remainingInfo += chosenFeature.computeInfo(examples, Sensors.ANIMAL);
        remainingInfo += chosenFeature.computeInfo(examples, Sensors.MINERAL);
        remainingInfo += chosenFeature.computeInfo(examples, Sensors.VEGETABLE);
      }
      // ***** You'll need to deal with your other feature types here. *****
      //       (Ie, add some ELSE-IF's.)
      else 
      {
        Utils.println("Unknown feature type: " + featureSpec.toString());
      }

      if (remainingInfo < leastInfoLeft) // Have a new winner.
      {
        leastInfoLeft = remainingInfo;
        bestFeature   = featureSpec;
      }
    }

    return bestFeature; // Return the feature that best separates the examples.
  }

  // Create the initial list of ALL the features being considered.
  Vector createFullFeatureSet()
  { Vector result = new Vector(Sensors.NUMBER_OF_SENSORS);

    for(int dir = 0; dir < Sensors.NUMBER_OF_SENSORS; dir++)
    {
      // ***** You'll need to add your other feature types here. *****
      // (Putting them above the following line means they'll
      // be chosen when there is a tie between the scores of 
      // features of various types.)
      result.addElement(new ObjectType_Feature(dir));
    }

    return result;
  }

  // Remove this feature from this list of features (do this
  // by making a NEW COPY of the list, with the featureToRemove skipped over).
  Vector removeFeature(Feature featureToRemove, Vector features)
  { Vector result = new Vector();

    int length = features.size();
    for(int i = 0; i < length; i++)
    { Feature featureSpec = (Feature)(features.elementAt(i));

      if (!featureSpec.equals(featureToRemove))
      { // Keep this feature unless it matches the feature to remove.
        // (Actually, using != would work here, but play it safe with
        // the richer sense of equality.)
        result.addElement(featureSpec);
      }
    }

    return result;
  }

  // Map from the compass direction expressed as a string to radians.
  // (Also, "@" is the symbol for 'stand still,' which is specified
  // be setting radians to any negative value.)
  double convertCompassDirectionToRadians(String direction)
  {
    if (direction == null) return -1.0; // Stand still if an invalid direction provided.

    String chosenDirection = direction.trim(); // Discard leading or trailing spaces.

    // Angles are defined in the clockwise direction.
    if (chosenDirection.equalsIgnoreCase("E"))  return (0 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("SE")) return (1 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("S"))  return (2 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("SW")) return (3 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("W"))  return (4 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("NW")) return (5 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("N"))  return (6 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("NE")) return (7 * Math.PI) / 4;
    if (chosenDirection.equalsIgnoreCase("@"))  return -1.0; // The stand-still symbol.

    Utils.println("An unknown direction sent to convertCompassDirectionToRadians: "
                  + direction);
    return -1.0; // Stand still in this case.
  }

  // Count how many of the examples in this vector are for GOOD moves.
  int countPositiveExamples(Vector examples)
  { 
    if (examples == null) return 0;

    int count = 0, length = examples.size();
    for(int i = 0; i < length; i++) 
    { // Note that you need to cast the Object in the vector into a LabeledExample.
      LabeledExample thisExample = (LabeledExample)(examples.elementAt(i));

      if (thisExample.isaGoodDirectionToMove()) count++;
    }

    return count;
  }

  // Same as the above, except count BAD directions.
  int countNegativeExamples(Vector examples)
  { 
    if (examples == null) return 0;

    int count = 0, length = examples.size();
    for(int i = 0; i < length; i++) 
    { LabeledExample thisExample = (LabeledExample)(examples.elementAt(i));

      if (!thisExample.isaGoodDirectionToMove()) count++;
    }

    return count;
  }
}
