import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.*;
import javax.swing.event.*;
import java.text.*;
import java.util.*;

public class Automata extends WindowAdapter implements ActionListener 
{
	// fields -------------------------------------------------------------------
	
	// constants for the default settings
	private final static int     N_DEFAULT = 2;
	private final static int     N_MIN = 2;
	private final static int     N_MAX = 8;
	private final static int     WIDTH_DEFAULT = 800;
	private final static int     HEIGHT_DEFAULT = 400;
	private final static int     WIDTH_MIN = 50;
	private final static int     WIDTH_MAX = 2000;
	private final static int     HEIGHT_MIN = 50;
	private final static int     HEIGHT_MAX = 2000;
	private final static int     SPINNER_STEP = 10;
	private final static int     MAG_DEFAULT = 1;
	private final static boolean ZOOMED_DEFAULT = false;
	private final static boolean CENTERED_DEFAULT = false;
	private final static boolean SCROLLING_DEFAULT = false;
	private final static byte    RULE_DEFAULT[] = {2,1,1,1,0,1,1,2,2,1,2,0,0,0,2,1,2,2,2,0,0,2,1,0,1,0,0};		

	// gui constants
	private final static int     xWindow = 10;  				// pixels added to width by window border
	private final static int     yWindow = 33; 				// pixels added to height by window title bar and bottom border
	private final static Insets  inset = new Insets(0,0,0,0);  	// used to make margins 0	
	private final static String  CONTROL_TITLE = "n-Color Automata";	
	private final static String  IMAGE_TITLE = "2-Color Automaton";
	
	// variables
	private static int     n = N_DEFAULT;		    		// number of colors
	private static int     width  = WIDTH_DEFAULT;		// width of image
	private static int     height = HEIGHT_DEFAULT;   	// height of image
	private static int     mag = MAG_DEFAULT;			// magnification
	private static boolean zoomed = ZOOMED_DEFAULT; 		// zoomed in or not
	private static boolean centered = CENTERED_DEFAULT; 	// centered or not (random)
	private static boolean scrolling = SCROLLING_DEFAULT; 	// scrolling or not
	private static int     scrollCount = 0; 			// the ''page number of the current image''
	//private static boolean initializeAO = true;			// if the sizes changed, need to reinitialize the ao array
	
	private static int     spinnerWidth = width;			// number that the spinner changes
	private static int     spinnerHeight = height;		// number that the spinner changes
	private static int	   nSpin = n;					// number of colors in spinner
	
	private static byte rule[] = {2,1,1,1,0,1,1,2,2,1,2,0,0,0,2,1,2,2,2,0,0,2,1,0,1,0,0}; // holds the rule
	private static byte ao[][]; 				// double array holds colors			
	private static byte pixels[][]; 				   // double array holds colors	
		
	// image components
	private static BufferedImage bi; 			// this is the actual image
	private static Graphics2D bg; 			// needed to draw the pixel in bi
	private static ImageIcon imageIcon; 		// used to put bi into a controlFrame

	
	// GUI components	
	
	// control window
	private static JFrame controlFrame;
	
	private static JLabel numberColors = new JLabel("How many colors?");
	private static SpinnerNumberModel nModel = new SpinnerNumberModel(n,N_MIN,N_MAX,1);                			
	private static JSpinner nSpinner = new JSpinner(nModel);        
	
	// controls for drawing mode: centered or random
	private static JLabel modeLabel = new JLabel("First line:");
	private static JRadioButton[] drawMode = { new JRadioButton("Centered"), new JRadioButton("Random") };
	
	// controls for magnification: x1 or x2 or x5
	private static JLabel magLabel = new JLabel("Magnification:");
	private static JRadioButton[] magLevel = { new JRadioButton("x 1"), new JRadioButton("x 2"), new JRadioButton("x 5") };
	
	// buttons for scrolling, saving, refreshing, resetting, setting a random rule
	private static JButton scroll = new JButton("Scroll Down");
	private static JButton save = new JButton("Save Image");	
	private static JButton refresh = new JButton("Refresh");
	private static JButton reset = new JButton("Reset");	
	private static JButton random = new JButton("Random Rule");	
	
	// controls for setting image size
	private static JLabel xLabel = new JLabel(" Image Width: ");
	private static SpinnerNumberModel xModel = new SpinnerNumberModel(width,WIDTH_MIN,WIDTH_MAX,SPINNER_STEP);                			
	private static JSpinner xSpinner = new JSpinner(xModel);        
	private static JLabel yLabel = new JLabel(" Image Height: ");        
	private static SpinnerNumberModel yModel = new SpinnerNumberModel(height,WIDTH_MIN,WIDTH_MAX,SPINNER_STEP);      
	private static JSpinner ySpinner = new JSpinner(yModel);
		 
	// controls for setting the colors  
   	private static Color[] colorList = { Color.white, Color.black, Color.blue, 
   								Color.red, Color.yellow, Color.green, 
   								Color.orange, Color.pink, Color.lightGray, 
   								Color.cyan, Color.magenta, Color.gray, Color.darkGray };   	
   	
   	// constants for action commands	
	protected final static String SCROLL = "scroll";
	protected final static String SAVE = "save";
	protected final static String REFRESH = "refresh";
	protected final static String RESET = "reset";
	protected final static String RANDOM = "random";
   	
   	// image window components
   	private static JFrame imageFrame;   // window for the image
	private static JLabel imageLabel;   // the label holds the image, which is imageIcon above
   
	// end of fields -----------------------------------------------------------------------   
   
   
	public Automata() { }


	// this handles the size spinners
	ChangeListener sListener = new ChangeListener() 
	{
		public void stateChanged(ChangeEvent e) 
		{			
			spinnerWidth = xModel.getNumber().intValue();
			spinnerHeight = yModel.getNumber().intValue();
			//System.out.println("spinnerWidth = " + spinnerWidth);
		}		
	};

	// this handles the color spinner
	ChangeListener cListener = new ChangeListener() 
	{
		public void stateChanged(ChangeEvent e) 
		{			
			nSpin = nModel.getNumber().intValue();
			//System.out.println("nSpin = " + nSpin);
		}		
	};

	//show the image window
	public static void showNewWindow() 
	{ 
		imageFrame.setSize(width+xWindow,height+yWindow);              
		imageFrame.setVisible(true);
	}

	// Create the window-creation controls that go in the main window.
	protected JComponent createOptionControls() 
	{    	
	
		JPanel numberPanel = new JPanel();
			numberPanel.add(numberColors);
			numberPanel.add(nSpinner);
		numberPanel.setLayout(new BoxLayout(numberPanel,BoxLayout.X_AXIS));					
						                      
		JPanel modePanel = new JPanel(); 
		ButtonGroup modeGroup = new ButtonGroup();
			modePanel.add(modeLabel);
			modePanel.add(drawMode[0]);
			modeGroup.add(drawMode[0]);	
			modePanel.add(drawMode[1]);
			modeGroup.add(drawMode[1]);
			drawMode[0].setSelected(true);
			drawMode[1].setSelected(false);
		
		JPanel magPanel = new JPanel(); 
		ButtonGroup magGroup = new ButtonGroup();
			magPanel.add(magLabel);
			magPanel.add(magLevel[0]);
			magGroup.add(magLevel[0]);	
			magPanel.add(magLevel[1]);			
			magGroup.add(magLevel[1]);
			magPanel.add(magLevel[2]);
			magGroup.add(magLevel[2]);			
			magLevel[0].setSelected(true);
			magLevel[1].setSelected(false);
			magLevel[2].setSelected(false);
		
		JPanel sizePanel = new JPanel();	
			sizePanel.add(xLabel);
			sizePanel.add(xSpinner);
			sizePanel.add(yLabel);
			sizePanel.add(ySpinner);		
		sizePanel.setLayout(new BoxLayout(sizePanel,BoxLayout.X_AXIS));		
						 			
		JPanel buttonsPanel = new JPanel();
          	buttonsPanel.add(scroll);          
          	scroll.setActionCommand(SCROLL);	
          	buttonsPanel.add(save);
          	save.setActionCommand(SAVE);
          	buttonsPanel.add(refresh);
          	refresh.setActionCommand(REFRESH);
			buttonsPanel.add(reset);
			reset.setActionCommand(RESET);
			buttonsPanel.add(random);
			random.setActionCommand(RANDOM);
		
  		// set listeners
  		
  		refresh.addActionListener(this);
  		reset.addActionListener(this);
  		scroll.addActionListener(this);
  		save.addActionListener(this);  
  		random.addActionListener(this);  	
   		xSpinner.addChangeListener(sListener);
  		ySpinner.addChangeListener(sListener);
  		nSpinner.addChangeListener(cListener);

    
		//Add everything to a container.
		Box box = Box.createVerticalBox();
			box.add(numberPanel);
			box.add(modePanel);
			box.add(magPanel);
			box.add(sizePanel);	     	
	     	box.add(buttonsPanel);

		return box;
	}	

	//Handle action events from all the buttons.
	public void actionPerformed(ActionEvent e) 
	{ 	
		String command = e.getActionCommand();
		if( SCROLL.equals(command) ) 
		{ 
			//System.out.println("scroll"); 			
			// if the scroll button was pressed, set scrolling = true, then refresh
			scrolling = true;
			scrollCount++;
			
			// ensure no funny business with the spinners before scrolling
			xModel.setValue(width);
			yModel.setValue(height);
			
			createImage();	
          	imageFrame.requestFocus();			
		}
		else if( SAVE.equals(command) ) 
		{ 
			//System.out.println("save"); 
			// Write image to a file
    			try 
    			{		
     	   		File file = new File(fileNameGenerator());
        			ImageIO.write(bi, "png", file);    
    			}
    			catch (IOException ex) {     }				
		}
		else if( REFRESH.equals(command) ) 
		{ 
			//System.out.println("refresh"); 										
			
			// is it centered or random
			centered = drawMode[0].isSelected();
			
			// what is the magnification?
			if( magLevel[0].isSelected() == true ) 
			{ 
				zoomed = false;
				mag = 1;
			}
			else if( magLevel[1].isSelected() == true ) 
			{ 
				zoomed = true;
				mag = 2;
			}
			else
			{ 
				zoomed = true;
				mag = 5;
			}
			
			//reset the scrollcount
			scrolling = false;
			scrollCount = 0;			
  			
  			// if n has changed...  						
  			// create new array for the rule of length n^3, fill it randomly
			if ( n != nSpin )
			{
				//System.out.println("updating n to" + nSpin);
				n = nSpin;
				rule = new byte[(int)Math.pow(n,3)];
				for(int k = 0; k < rule.length; k++) 
				{
					rule[k] = (byte)(Math.round(Math.exp(Math.random()+1))%n);								
				}						
			}
  				
  			//System.out.println("rule:");	
  			//for(int k = 0; k < rule.length; k++) 
			//{
			//	System.out.print(rule[k]);							
			//}			
			//System.out.print("\n");
  						
  			String str = n + "-Color Automaton";
			imageFrame.setTitle(str);  						
  						
			createImage();
						
			// if the window was closed, open a new one
			if( !imageFrame.isVisible() ) { showNewWindow(); }
			else 
			{
				// need to change the size of this window...	
				imageFrame.setSize(width+xWindow,height+yWindow);
          		imageFrame.requestFocus();
          	}          	          	
          	//System.out.println("mag = " + mag + " height = " + height/mag + " width = " + width/mag); 			
		}
		else if( RESET.equals(command) ) 
		{ 
			//System.out.println("reset"); 
			
			// reset main variables
			width = WIDTH_DEFAULT;
			height = HEIGHT_DEFAULT;
			mag = MAG_DEFAULT;		
			zoomed = ZOOMED_DEFAULT; 	
			centered = CENTERED_DEFAULT; 	
			scrolling = SCROLLING_DEFAULT; 	
			scrollCount = 0; 
			spinnerWidth = width;
			spinnerHeight = height;
			n = N_DEFAULT;
								
			// reset the rule
			rule = new byte[(int)Math.pow(n,3)];
			for(int i = 0; i < rule.length; i++) { rule[i] = RULE_DEFAULT[i]; }
					
			// reset the mode radios
			drawMode[0].setSelected(true);
			drawMode[1].setSelected(false);
			
			// reset zoom level radios
			magLevel[0].setSelected(true);
			magLevel[1].setSelected(false);
			magLevel[2].setSelected(false);
			mag = 1;
				
			// reset sizes
			xModel.setValue(width);
			yModel.setValue(height);
								
			imageFrame.setTitle(IMAGE_TITLE);
								
			createImage();			
			
			// if the window was closed, open a new one of proper size
			if( !imageFrame.isVisible() ) 
			{ 
				showNewWindow(); 
			}
			else // update the size of the open window
			{
				imageFrame.setSize(width+xWindow,width+yWindow);
          		imageFrame.requestFocus();
          	}				
		}
		else if( RANDOM.equals(command) ) 
		{
			// randomize the rule, set the status and icons, 
			// then refresh in the current window, if it exists
			
			rule = new byte[(int)Math.pow(n,3)];
			for(int k = 0; k < rule.length; k++) 
			{
				rule[k] = (byte)(Math.round(Math.exp(Math.random()+1))%n);								
			}		
				
			createImage();	 
			
			// if the window was closed, open a new one
			if( !imageFrame.isVisible() ) { showNewWindow(); }
			else 
			{
				imageFrame.setSize(width+xWindow,height+yWindow);
          		imageFrame.requestFocus();
          	}
			
			// need to change the size of this window...	
			imageFrame.setSize(width+xWindow,height+yWindow);		
          	imageFrame.requestFocus();					
		
		}
		else { System.out.println("oops"); }                
    }
   

    //Creates the Image 
	public static void createImage() {
    
    		// (re)initialize the image components
    		if( scrolling == false )
    		{    			
    			if( ao.length != height/mag || ao[0].length != width/mag )
    			{    			
    				width = spinnerWidth;
    				height = spinnerHeight;
				ao = new byte[width/mag][height/mag+1];  				
   				bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
   				bg = bi.createGraphics();
   				//System.out.println("created new AO with height = " + height/mag + " width = " + (width/mag+1)); 
   			}
   			//else { System.out.println("didn't initialize new array...  height = " + height/mag + " width = " + (width/mag+1)); }
   		
   		}
   		//else { System.out.println("didn't initialize new array...  height = " + height/mag + " width = " + (width/mag+1)); }
   		      		    
   		setFirstRow();
		
		//System.out.println("ao:");
		//fill the rest
		for(int j = 1; j <= (height/mag); j++) // j = rows
	      {
			for(int i = 0; i < (width/mag); i++) // i = columns
      		{	
      		
      		
      			// cycle through all the rules
      			for(int k = 0; k < n; k++) // first box
      			{
      				for(int l = 0; l < n; l++) // second box
      				{
      					for(int m = 0; m < n; m++) // third box
      					{
      						if( ao[(i-1+(width/mag))%(width/mag)][j-1] == k && 
            	                   	    ao[i][j-1]           	             == l && 
                   	                   ao[(i+1+(width/mag))%(width/mag)][j-1] == m )  
                  	          	{                  	         		
                  	         			ao[i][j] = rule[ n*n*k + n*l + m];   //as a cube: total levels + total rows + partial row
                  	         			drawPixel(i,j,ao[i][j]);
                  	         			//System.out.print(ao[i][j]);
							}
						}
					}										
	      		}
	      	}
	      	//System.out.print("\n");
      	}	
      	
      	//System.out.println("end ao.");	   
      	
      	imageIcon = new ImageIcon(bi);
      	imageLabel.setIcon(imageIcon);
    }
    
    private static void setFirstRow()
    {
    		// if the scroll button has been presssed
    		if( scrolling == true )
    		{    			
    			// set first row equal to continuation of last(undrawn) row
    			for(int k = 0; k < (width/mag); k++) 
			{
				ao[k][0] = ao[k][(height/mag)];
				drawPixel(k,0,ao[k][(height/mag)]);
			}
			
			//reset scrolling to false
			scrolling = false;
    		}
    		// if scroll button has not been pressed
		else
		{
			// if centered
			if( drawMode[0].isSelected() ) 
			{
				for(int k = 0; k < (width/mag); k++) 
				{
					ao[k][0] = 0;
					drawPixel(k,0,ao[k][0]);
				}
		     	ao[Math.round((width/mag)/2)][0] = 1; 
		     	drawPixel(Math.round((width/mag)/2),0,(byte)1);
			}
			else // if random
			{
				for(int k = 0; k < (width/mag); k++) 
				{
					if( Math.round(Math.exp(Math.random()+1))%n == 0 ) 
					{ 
						ao[k][0] = 0; 
						drawPixel(k,0,ao[k][0]);
					}
					if( Math.round(Math.exp(Math.random()+1))%n == 1 ) 
					{ 
						ao[k][0] = 1; 
						drawPixel(k,0,ao[k][0]);
					}
					else 
					{ 
						ao[k][0] = 2; 
						drawPixel(k,0,ao[k][0]);
					}
				}
			}
		}         
	}    
    
	// sets the color of the (i,j) pixel in the BufferedImage to c
	public static void drawPixel(int i ,int j, byte c)
	{
		// don't draw the last line (which is overlap)
		if( j < (height/mag) )
		{
				bg.setColor(colorList[c]);
				bg.fillRect(mag*i,mag*j,mag,mag);								
		}		
	}        

    	/**
	 * Create the GUI and show it.  For thread safety, this method should be 
	 * invoked from the event-dispatching thread.
      */
	private static void createAndShowGUI() 
	{  		
		//Use the Java look and feel.
		try 
		{
			UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
		} 
		catch (Exception e) { }

		//Make sure we have nice window decorations.
		JFrame.setDefaultLookAndFeelDecorated(true);
		JDialog.setDefaultLookAndFeelDecorated(true);

		//Instantiate the controlling class.
		controlFrame = new JFrame(CONTROL_TITLE);
		controlFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		//Create and set up the control pane.
		Automata auto = new Automata();

		Container contentPane = controlFrame.getContentPane();
		contentPane.add(auto.createOptionControls(),BorderLayout.CENTER);    

		controlFrame.pack();
		controlFrame.setLocationRelativeTo(null); //center it
		        
		// set up the image frame, but don't display it        
		imageFrame = new JFrame(IMAGE_TITLE);        
		imageLabel = new JLabel();        
		Container imageFrameContentPane = imageFrame.getContentPane();
		imageFrameContentPane.add(imageLabel,BorderLayout.CENTER);
		imageFrame.pack();        
        
        	// initialize image
        	ao = new byte[width/mag][height/mag+1];  				
   		bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
   		bg = bi.createGraphics();
		createImage();
		
		// display the windows
		controlFrame.setVisible(true);
		showNewWindow();               
    }

	//Start the program
	public static void main(String[] args) 
	{		
		createAndShowGUI();
	}

   
	// Returns an ImageIcon, or null if the path was invalid. 
	protected static ImageIcon createImageIcon(String path) 
	{
		java.net.URL imgURL = Automata.class.getResource(path);
		if (imgURL != null) { return new ImageIcon(imgURL); }
		else 
		{ 
			System.err.println("Couldn't find file: " + path); 
        		return null;
    		}
	}        
	
	public String fileNameGenerator()
	{
		String fileName = "./saved/automata-" + n + "-" + width + "x" + height + "-" + mag + "-";		
		
		if(!centered) { fileName = fileName + "-random-"; }
		
		for(int i = 0; i < rule.length; i++)
		{
			fileName = fileName + rule[i];
		}
		
		return fileName + "-" + scrollCount + ".png";
	}        	
}
