/*
   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.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.image.*;
import java.io.*;
import java.util.Vector;

public class WorldPane extends JPanel {
   
   private BufferedImage canvas;
   private int canvasWidth, canvasHeight;
   private int canvasHeightUsed;
   private World world;
   private int[] palette;
   private int[] initialWorld;
   
   private Timer timer;
   private int delay = 10;
   
   private int hOffset;
   
   private int topLine;  // The position in canvas of the line that appears at the top of the window
   
   private JSlider lambdaSlider;
   private JLabel lambdaLabel, infoLabel;
   private ScrollingWorldPane scrollPane;
   
   private boolean computingFirstImage;
   
   private static int[] basicPalette =
                 { 0xFFFFFF, 0x000000, 0xDD0000, 0x0000DD,
                   0xDDDD00, 0xDD00DD, 0x00DDDD, 0x00DD00 };
   
   public WorldPane(boolean create) {
         // call with create=false to get a worldpane with no world;
         // then call create later to install a world.  This is for
         // use with EdgeOfChaosApplet.
      if (create)
         installWorld(null,0,null);
   }
   
   public WorldPane() {
      this(null,0);
   }
   
   public WorldPane(World world, int imageHeight) {
      this(world,imageHeight, null);
   }
   
   public WorldPane(World world, int imageHeight, int[] palette) {
      installWorld(world,imageHeight, palette);
   }
   
   public void create() {
   
   }
   
   public void installWorld(World world) {
      installWorld(world,0,null);
   }
   
   public void installWorld(World world, int imageHeight) {
      installWorld(world,imageHeight,null);
   }
   
   public void installWorld(World world, int imageHeight, int[] palette) {
      if (world == null)
         world = new World();
      this.world = world;
      initialWorld = (int[])world.getWorld().clone();
      canvasWidth = world.getWorldSize();
      canvasHeight = imageHeight;
      if (canvasHeight < 1)
         canvasHeight = (canvasWidth*3)/4;
      setPreferredSize(new Dimension(canvasWidth,canvasHeight));
      if (palette == null || palette.length != world.getNumberOfStates())
         this.palette = makeStandardPalette(world.getNumberOfStates());
      else
         this.palette = palette;
      canvas = new BufferedImage(canvasWidth,canvasHeight,BufferedImage.TYPE_BYTE_INDEXED,makeColorModel(this.palette));
      Graphics g = canvas.getGraphics();
      Color deadColor = new Color(this.palette[0]);
      g.setColor(deadColor);
      g.fillRect(0,0,canvasWidth,canvasHeight);
      g.dispose();
      canvasHeightUsed = 0;
      topLine = 0;
      world.zeroGenerationNumber();
      setupSlider();
      setupInfoLabel();
      if (scrollPane != null)
         scrollPane.check(true);
      worldToCanvas();
      repaint();
   }
   
   public void paintComponent(Graphics g) {
      g.setColor(Color.gray);
      if (canvas == null)
         g.fillRect(0,0,getWidth(),getHeight());
      else {
         int vOffset = 0;
         if (canvasHeightUsed > getHeight())
            vOffset = canvasHeightUsed - getHeight();
         if (canvasHeight < getHeight())
            g.fillRect(0, canvasHeight, getWidth(), getHeight() - canvasHeight);
         if (canvasWidth - hOffset < getWidth())
            g.fillRect(canvasWidth - hOffset, 0, getWidth() - (canvasWidth-hOffset), getHeight());
         //g.drawImage(canvas,-hOffset, -vOffset, null);
         putWorld(g,-hOffset,-vOffset);
      }
   }
   
   public JPanel getScrollingWorldPane() {
      if (scrollPane == null)
         scrollPane = new ScrollingWorldPane();
      return scrollPane;
   }

   public World getWorld() {
      return world;
   }
   
   public Dimension getCanvasSize() {
      return new Dimension(canvasWidth,canvasHeight);
   }
      
   public static int[] makeStandardPalette(int numberOfColors) {
      int[] palette = new int[numberOfColors];
      if (numberOfColors <= 8) {
         for (int i = 0; i < numberOfColors; i++)
            palette[i] = basicPalette[i];
      }
      else {
          palette[0] = Color.white.getRGB();
          for (int i = 1; i < numberOfColors; i++)
            palette[i] = Color.getHSBColor( (float)(i-1)/(numberOfColors-1),1.0F,0.8F ).getRGB();
      }
      return palette;
   }
   
   private IndexColorModel makeColorModel(int[] palette) {
      return new IndexColorModel(6,palette.length,palette,0,false,-1,DataBuffer.TYPE_BYTE);
   }
   
   private void worldToCanvas() {
      int position;
      if (world.getGenerationNumber() < canvasHeight) {
         position = world.getGenerationNumber();
         canvasHeightUsed = position+1;
         topLine = 0;
      }
      else {  // AFTER FILLING IN ENTIRE CANVAS, START OVER FROM THE TOP
         position = topLine;
         topLine++;
         if (topLine >= canvasHeight)
            topLine = 0;
      }
      canvas.getRaster().setPixels(0,position,canvasWidth,1,world.getWorld());
   }
   
   private void putWorld(Graphics g, int dx, int dy) {
      if (topLine == 0)
         g.drawImage(canvas,dx,dy,null);
      else {   // COPY CANVAS IN TWO PIECES -- line at position topLine goes to top of screen
         g.drawImage(canvas,
                dx,dy,dx+canvasWidth,dy+canvasHeight-topLine,
                0,topLine,canvasWidth,canvasHeight,
                null);
         g.drawImage(canvas,
                dx,dy+canvasHeight-topLine,dx+canvasWidth,dy+canvasHeight,
                0,0,canvasWidth,topLine,null);
      }
   }
   
   public void setHOffset(int offset) {
      hOffset = offset;
   }
   
   public int getHOffset() {
      return hOffset;
   }
   
   public int getAvailableHeight() {
      return scrollPane == null ? getHeight() : scrollPane.getHeight();
   }
   
   public void start(int initialMillisecondsDelay) {
      if (timer == null) {
         timer = new Timer( delay,
                    new ActionListener() {
                       public void actionPerformed(ActionEvent evt) {
                          next();
                       }
                    });
         if (initialMillisecondsDelay > 0)
            timer.setInitialDelay(initialMillisecondsDelay);
         timer.start();
      }
      computingFirstImage = false;
   }
   
   public void stop() {
      if (timer != null) {
         timer.stop();
         timer = null;
      }
      computingFirstImage = false;
   }
   
   public void next() {
      world.nextGeneration();
      worldToCanvas();
      repaint();
      if (computingFirstImage && world.getGenerationNumber() == canvasHeight - 1)
         stop();
   }
   
   public boolean isRunning() {
      return timer != null;
   }
   
   public int getDelay() {
      return delay;
   }
   
   public void setDelay(int d) {
      if (d > 0 && d < 10000)
         delay = d;
      if (timer != null)
         timer.setDelay(d);
   }

   public int[] getPalette() {
      return palette;
   }
   
   public void setPalette(int[] palette) {
      if (palette.length == world.getNumberOfStates()) {
         this.palette = palette;
         canvas = new BufferedImage(makeColorModel(palette),canvas.getRaster(),false,null);
         repaint();
      }
   }
   
   public int[] getInitialWorld() {
      return initialWorld;
   }
   
   public BufferedImage getImage() {
      if (topLine == 0)
          return canvas;
      else {  // actual image needs to be reassembled from two pieces in the canvas; (topLine is the vertical position of the actual start of the image)
         BufferedImage image = new BufferedImage(canvasWidth,canvasHeight,BufferedImage.TYPE_BYTE_INDEXED,makeColorModel(this.palette));
         Graphics g = image.getGraphics();
         putWorld(g,0,0);
         return image;
      }
   }
   
   public void restart() {
      restart(null);
   }
   
   public void firstImage() {
      if (world.getGenerationNumber() >= canvasHeight - 1)
         restart(null);
      else
         start(0);
      computingFirstImage = true;
   }
   
   public void restartRandom() {
      int[] w = new int[world.getWorldSize()];
      for (int i = 0; i < w.length; i++)
         w[i] = (int)(world.getNumberOfStates() * Math.random());
      restart(w);
   }
   
   public void restartRandomClump() {
      int[] w = new int[world.getWorldSize()];
      for (int i = w.length/2 - 25; i < w.length/2+25; i++)
         w[i] = (int)(world.getNumberOfStates() * Math.random());
      restart(w);
   }
   
   public void restartSingleDot() {
      int[] w = new int[world.getWorldSize()];
      w[w.length/2] = 1;
      restart(w);
   }
   
   public void restart(int[] w) {
      if (isRunning())
         stop();
      if (w == null)
         w = initialWorld;
      world.setWorld(w);
      initialWorld = w;
      Graphics g = canvas.getGraphics();
      g.setColor(new Color(palette[0]));
      g.fillRect(0,0,canvasWidth,canvasHeight);
      g.dispose();
      canvasHeightUsed = 0;
      topLine=0;
      worldToCanvas();
      start(100);
   }
   
   public JSlider getLambdaSlider() {
      if (lambdaSlider == null) {
         lambdaSlider = new JSlider();
         setupSlider();
         lambdaSlider.addChangeListener( new ChangeListener() {
                public void stateChanged(ChangeEvent e) {
                   if (world == null)
                      return;
                   if (isRunning())
                      stop();
                   world.setRulesUsed(lambdaSlider.getValue());
                   setupLabel();
                   if ( ! lambdaSlider.getModel().getValueIsAdjusting())
                      restart();
                }
            });
      }
      return lambdaSlider;
   }
   
   public JLabel getLambdaLabel() {
      if (lambdaLabel == null) {
         lambdaLabel = new JLabel();
         setupLabel();
      }
      return lambdaLabel;
   }
   
   public JLabel getInfoLabel() {
      if (infoLabel == null) {
         infoLabel = new JLabel("World Not Created Yet",JLabel.CENTER);
         setupInfoLabel();
      }
      return infoLabel;
   }
   
   private void setupSlider() {
      if (lambdaSlider == null || world == null)
         return;
      lambdaSlider.setMinimum(0);
      int val = world.getRulesUsed();
      int max = world.getRuleCount();
      lambdaSlider.setMaximum(max);
      lambdaSlider.setValue(val);
      setupLabel();
   }
   /*private void setupSlider() {
      if (lambdaSlider == null)
         return;
      lambdaSlider.setMinimum(0);
      int max = world.getRuleCount();
      System.out.println(world.getRulesUsed() + " " + world.getRuleCount());
      lambdaSlider.setMaximum(max);
      System.out.println(world.getRulesUsed() + " " + world.getRuleCount());
      lambdaSlider.setValue(world.getRulesUsed());
      setupLabel();
   }*/
   
   private void setupLabel() {
      if (lambdaLabel != null) {
         int lambda = (world == null)? 0 : (int)(world.getLambda()*10000 + 0.5);
         if (lambda == 0)
            lambdaLabel.setText("Lambda = 0.0000");
         else if (lambda < 10)
            lambdaLabel.setText("Lambda = 0.000" + lambda);
         else if (lambda < 100)
            lambdaLabel.setText("Lambda = 0.00" + lambda);
         else if (lambda < 1000)
            lambdaLabel.setText("Lambda = 0.0" + lambda);
         else if (lambda < 10000)
            lambdaLabel.setText("Lambda = 0." + lambda);
         else
            lambdaLabel.setText("Lambda = 1.0000");
      }
   }
   
   private void setupInfoLabel() {
       if (infoLabel != null && world != null) {
          infoLabel.setText(
               world.getNumberOfStates()
                  + " States; "
                  + world.getNeighborhoodSize()
                  + " Neighbors; "
                  + (world.isIsotropic()? "Isotropic; " : "Anisotropic; ")
                  + (world.getRuleCount() + 1)
                  + " Rules"
            );
       }
   }
   
   public class ScrollingWorldPane extends JPanel implements AdjustmentListener {

      private JScrollBar scroller;
      private boolean scrollerIsShown;
      private int oldWidth;

      public ScrollingWorldPane() {
         setLayout(new BorderLayout());
         add(WorldPane.this, BorderLayout.CENTER);
         scroller = new JScrollBar(JScrollBar.HORIZONTAL);
         scrollerIsShown = false;
         oldWidth = 0;
         addComponentListener(new ComponentAdapter() {
               public void componentResized(ComponentEvent evt) {
                  check(false);
               }
            });
         scroller.addAdjustmentListener(this);
      }
      public void check(boolean force) {
          if (world != null && (force || oldWidth != WorldPane.this.getWidth())) {
             int newWidth = WorldPane.this.getWidth();
             boolean needsVisible = newWidth < WorldPane.this.getWorld().getWorldSize();
             if (needsVisible != scrollerIsShown) {
                if (needsVisible)
                   add(scroller,BorderLayout.SOUTH);
                else
                   remove(scroller);
                WorldPane.this.invalidate();
                validate();
                scrollerIsShown = needsVisible;
             }
             if (scrollerIsShown) {
                int newValue = WorldPane.this.getHOffset();
                if (newValue + WorldPane.this.getWorld().getWorldSize() > WorldPane.this.getWidth()) {
                   newValue = WorldPane.this.getWorld().getWorldSize() - WorldPane.this.getWidth();
                   WorldPane.this.setHOffset(newValue);
                }
                scroller.setValues(newValue,WorldPane.this.getWidth(),
                                       0,WorldPane.this.getWorld().getWorldSize());
                scroller.setBlockIncrement(WorldPane.this.getWidth() - 10);
             }
             else
                 WorldPane.this.setHOffset(0);
             oldWidth = newWidth;
          }
      }
      public void adjustmentValueChanged(AdjustmentEvent evt) {
         if (scrollerIsShown) {
            WorldPane.this.setHOffset(scroller.getValue());
            WorldPane.this.repaint();
         }
      }
   }
   
   public void readWorld(Reader input) throws Exception {
      World newWorld;
      int imageHeight = -1;
      Vector palette = null;
      Vector initialWorld = null;
      int numberOfStates = 4;
      int neighborhoodSize = 5;
      int worldSize = 640;
      boolean isotropic = true;
      boolean wrapped = true;
      boolean hasRandomSeed = false;
      long randomSeed = 0;
      int rulesUsed = -1;
      StreamTokenizer tokenizer = new StreamTokenizer(input);
      tokenizer.resetSyntax();
      tokenizer.commentChar('#');
      tokenizer.eolIsSignificant(false);
      tokenizer.whitespaceChars(',',',');
      tokenizer.whitespaceChars(' ',' ');
      tokenizer.whitespaceChars('\t','\t');
      tokenizer.whitespaceChars('\n','\n');
      tokenizer.whitespaceChars('\r','\r');
      tokenizer.wordChars('0','9');
      tokenizer.wordChars('-','-');
      tokenizer.wordChars('+','+');
      tokenizer.wordChars('a','z');
      tokenizer.wordChars('A','Z');
      tokenizer.nextToken();
      if (tokenizer.ttype == StreamTokenizer.TT_EOF)
         throw new Exception("The file does not contains any data.");
      while (tokenizer.ttype != StreamTokenizer.TT_EOF) {
         if (tokenizer.ttype != StreamTokenizer.TT_WORD)
            throw new Exception("Internal programming error.");
         String w = tokenizer.sval;
         if (w.equalsIgnoreCase("numberofstates")) {
            numberOfStates = getInt(tokenizer);
            if (numberOfStates < 2 || numberOfStates > 32)
               throw new IllegalArgumentException("Illegal value, " + numberOfStates + ", specified for NumberOfStates.");
         }
         else if (w.equalsIgnoreCase("neighborhoodsize")) {
            neighborhoodSize = getInt(tokenizer);
            if (neighborhoodSize < 3 || neighborhoodSize > 19)
               throw new IllegalArgumentException("Illegal value, " + neighborhoodSize + ", specified for NeighborhoodSize.");
         }
         else if (w.equalsIgnoreCase("worldsize")) {
            worldSize = getInt(tokenizer);
            if (worldSize < 100 || worldSize > 10000)
               throw new IllegalArgumentException("Illegal value, " + worldSize + ", specified for WorldSize.");
         }
         else if (w.equalsIgnoreCase("isotropic")) {
            isotropic = getBoolean(tokenizer);
         }
         else if (w.equalsIgnoreCase("imageheight")) {
            imageHeight = getInt(tokenizer);
         }
         else if (w.equalsIgnoreCase("circularworld")) {
            wrapped = getBoolean(tokenizer);
         }
         else if (w.equalsIgnoreCase("randomseed")) {
            randomSeed = getLong(tokenizer);
            hasRandomSeed = true;
         }
         else if (w.equalsIgnoreCase("rulesused")) {
            rulesUsed = getInt(tokenizer);
         }
         else if (w.equalsIgnoreCase("palette")) {
            palette = getIntList(tokenizer);
         }
         else if (w.equalsIgnoreCase("initialworld")) {
            initialWorld = getIntList(tokenizer);
         }
         else
            throw new Exception("File contents not of the correct format for this program.");
         tokenizer.nextToken();
      }
      if (hasRandomSeed)
         newWorld = new World(neighborhoodSize,numberOfStates,worldSize,isotropic,wrapped,randomSeed);
      else
         newWorld = new World(neighborhoodSize,numberOfStates,worldSize,isotropic,wrapped);
      if (initialWorld != null) {
         int[] currentWorld = new int[worldSize];
         if (worldSize != initialWorld.size())
            System.out.println("WARNING:  Number of items for InitialWorld in input does not match WorldSize.");
         for (int i = 0; i < worldSize && i < initialWorld.size(); i++) {
            currentWorld[i] = ((Integer)initialWorld.elementAt(i)).intValue();
            if (currentWorld[i] < 0 || currentWorld[i] >= numberOfStates)
               throw new Exception("Out-of-range number found in initial world data.");
         }
         newWorld.setWorld(currentWorld);
      }
      if (rulesUsed >= 0)
         newWorld.setRulesUsed(rulesUsed);
      int[] p = null;
      if (palette != null) {
         p = new int[numberOfStates];
         if (numberOfStates != palette.size())
            System.out.println("WARNING:  Number of items for Palette in input does not match NumberOfStates.");
         for (int i = 0; i < numberOfStates && i < palette.size(); i++)
            p[i] = ((Integer)palette.elementAt(i)).intValue();
      }
      installWorld(newWorld,imageHeight,p);
      start(500);
   }
   
   private Vector getIntList(StreamTokenizer tokenizer) throws Exception {
      Vector v = new Vector();
      while (true) {
         try {
             v.addElement(new Integer(getInt(tokenizer)));
         }
         catch (Exception e) {
            tokenizer.pushBack();
            break;
         }
      }
      if (v.size() == 0)
         throw new Exception("No data found when looking for a list of integers");
      return v;
   }
   
   private int getInt(StreamTokenizer tokenizer) throws Exception {
      tokenizer.nextToken();
      if (tokenizer.ttype == StreamTokenizer.TT_EOF)
         throw new Exception("Unexpected end of input while looking for parameter value.");
      if (tokenizer.ttype != StreamTokenizer.TT_WORD)
         throw new Exception("Internal programming error.");
      try {
         return Integer.decode(tokenizer.sval).intValue();
      }
      catch (NumberFormatException e) {
         throw new Exception("Illegal number, " + tokenizer.sval + ", found in input.");
      }
   }
   
   private long getLong(StreamTokenizer tokenizer) throws Exception {
      tokenizer.nextToken();
      if (tokenizer.ttype == StreamTokenizer.TT_EOF)
         throw new Exception("Unexpected end of input while looking for parameter value.");
      if (tokenizer.ttype != StreamTokenizer.TT_WORD)
         throw new Exception("Internal programming error.");
      try {
         return Long.parseLong(tokenizer.sval);
      }
      catch (NumberFormatException e) {
         throw new Exception("Illegal number, " + tokenizer.sval + ", found in input.");
      }
   }
   
   private boolean getBoolean(StreamTokenizer tokenizer) throws Exception {
      tokenizer.nextToken();
      if (tokenizer.ttype == StreamTokenizer.TT_EOF)
         throw new Exception("Unexpected end of input while looking for parameter value.");
      if (tokenizer.ttype != StreamTokenizer.TT_WORD)
         throw new Exception("Internal programming error.");
      String s = tokenizer.sval;
      if (s.equals("0") || s.equalsIgnoreCase("no") || s.equalsIgnoreCase("false"))
         return false;
      if (s.equals("1") || s.equalsIgnoreCase("yes") || s.equalsIgnoreCase("true"))
         return true;
      throw new Exception("Illegal true/false value, " + tokenizer.sval + ", found in input.");
   }
   
    
   
}

