/*
   Source code by David Eck
                  Department of Mathematics and Computer Science
                  Hobart and William Smith Colleges
                  Geneva, NY 14456
                  eck@hws.edu
                  
   This Java source code file can be used IN UNMODIFIED FORM for
   any purpose.  You can also make and distribute modified versions,
   as long as you include an acknowledgement of the original
   author in the modified version.
*/

package xca;


import java.util.Random;

public class World {

   final private long randomSeed;
   final private int neighborhoodSize;
   final private int numberOfStates;
   final private boolean isIsotropic;
   final private boolean isWrapped;
   final private int worldSize;

   private byte[] rule;
   private int[] lambdaPath;
   private int rulesUsed;
   private boolean[] ruleIsUsed;
   private int generationNumber;
   private int[] currentWorld;
   private int[] nextWorld;

   
   private static Random randomMaster = new Random();
   

   public World() {
      this(5,4,640,true,true);
   }
   

   public World(int neighborhoodSize, int numberOfStates, int worldSize,
                boolean isIsotropic, boolean isWrapped) {
      this(neighborhoodSize,numberOfStates,worldSize,isIsotropic,isWrapped,randomMaster.nextLong());
   }
   

   public World(int neighborhoodSize, int numberOfStates, int worldSize,
                boolean isIsotropic, boolean isWrapped, long randomSeed) {
      this.randomSeed = randomSeed;
      this.neighborhoodSize = neighborhoodSize;
      this.numberOfStates = numberOfStates;
      this.worldSize = worldSize;
      this.isIsotropic = isIsotropic;
      this.isWrapped = isWrapped;
      Random random = new Random(randomSeed);
      int ruleCt = (int)Math.pow(numberOfStates, neighborhoodSize);
      rule = new byte[ruleCt];
      ruleIsUsed = new boolean[ruleCt];
      rule[0] = 0;
      for (int i = 1; i < ruleCt; i++) {
         rule[i] = (byte)(1 + Math.abs(random.nextInt()) % (numberOfStates-1));
         if (isIsotropic)
            rule[isotropicMate(i)] = rule[i];
      }
      int lambdaCt;
      if (isIsotropic)
         lambdaCt = 
           (((int)Math.pow(numberOfStates, neighborhoodSize) 
              + (int)Math.pow(numberOfStates, (neighborhoodSize+1)/2)
            ) / 2) - 1;
      else
         lambdaCt = ruleCt - 1;
      lambdaPath = new int[lambdaCt];
      int ct = 0;
      for (int i = 1; i < ruleCt; i++) {
         if (!ruleIsUsed[i]) {
            lambdaPath[ct] = i;
            ct++;
            ruleIsUsed[i] = true;
            if (isIsotropic)
               ruleIsUsed[isotropicMate(i)] = true;
         }
      }
      if (ct != lambdaCt)  // for debugging
         throw new RuntimeException("Bad programming; wrong lambdaCt???");
      for (int i = 0; i < lambdaCt; i++) {
         int r = Math.abs(random.nextInt()) % lambdaCt;
         int temp = lambdaPath[i];
         lambdaPath[i] = lambdaPath[r];
         lambdaPath[r] = temp;
      }
      currentWorld = new int[worldSize];
      for (int i = 0; i < worldSize; i++)
         currentWorld[i] = (Math.abs(random.nextInt()) % numberOfStates);
      nextWorld = new int[worldSize];
      setRulesUsed((int)(0.333*lambdaPath.length));  // NB: this resets the rulesUsed[] array.
   }
   
   
   public void nextGeneration() {
      for (int i = 0; i < worldSize; i++) {
         int code = neighborhoodCode(i);
         if (ruleIsUsed[code])
            nextWorld[i] = rule[code];
         else
            nextWorld[i] = 0;
         if (rulesUsed == 0 && nextWorld[i] != 0)
            System.out.println("boo");
      }
      int[] temp = currentWorld;
      currentWorld = nextWorld;
      nextWorld = temp;
      generationNumber++;
   }
   
   public long getRandomSeed() {
      return randomSeed;
   }
   
   public int getNeighborhoodSize() {
      return neighborhoodSize;
   }
   
   public int getNumberOfStates() {
      return numberOfStates;
   }
   
   public boolean isIsotropic() {
      return isIsotropic;
   }
   
   public boolean isWrapped() {
      return isWrapped;
   }
   
   public int getWorldSize() {
      return worldSize;
   }
   
   public void setWorld(int[] newWorld) {
      if (newWorld.length == currentWorld.length) {
         generationNumber = 0;
         for (int i = 0; i < newWorld.length; i++)
            currentWorld[i] = newWorld[i];
      }
      else
         throw new IllegalArgumentException("Incorrect size for world data array in World.setWorld().");
   }
   
   public int[] getWorld() {
      return (int[])currentWorld.clone();  // return a COPY
   }
   
   public int getGenerationNumber() {
      return generationNumber;
   }
   
   public void zeroGenerationNumber() {
      generationNumber = 0;
   }
   
   public double getLambda() {
      return (double)rulesUsed / lambdaPath.length;
   }

   public int getRulesUsed() {
      return rulesUsed;
   }
   
   public int getRuleCount() {
      return lambdaPath.length;
   }
   
   public void setRulesUsed(int ct) {
      rulesUsed = ct;
      if (rulesUsed < 0)
         rulesUsed = 0;
      else if (rulesUsed > lambdaPath.length)
         rulesUsed = lambdaPath.length;
      if (isIsotropic) {
         for (int i = 0; i < rulesUsed; i++) {
            int r = lambdaPath[i];
            ruleIsUsed[r] = true;
            ruleIsUsed[isotropicMate(r)] = true;
         }
         for (int i = rulesUsed; i < lambdaPath.length; i++) {
            int r = lambdaPath[i];
            ruleIsUsed[r] = false;
            ruleIsUsed[isotropicMate(r)] = false;
         }
      }
      else {
         for (int i = 0; i < rulesUsed; i++)
            ruleIsUsed[lambdaPath[i]] = true;
         for (int i = rulesUsed; i < lambdaPath.length; i++)
            ruleIsUsed[lambdaPath[i]] = false;
      }
   }
   
   private int isotropicMate(int n) {
      int partner = 0;
      int s = numberOfStates;
      for (int i = 0; i < neighborhoodSize; i++) {
         partner = partner*s + (n % s);
         n /= s;
      }
      return partner;
   }
   
   private int getState(int n) {
      if (n < 0) {
         if (isWrapped)
            n += worldSize;
         else
            return 0;
      }
      else if (n >= worldSize) {
         if (isWrapped)
            n -= worldSize;
         else
            return 0;
      }
      return currentWorld[n];
   }
   
   private int neighborhoodCode(int n) {
      int code = 0;
      for (int i = 0; i < neighborhoodSize; i++)
         code = code*numberOfStates + 
                    getState(n+i-(neighborhoodSize/2));
      return code;
   }
   
   
}

