// RotateSquare.java
//
// Illustrate D4 by rotating a nicely colored square.
//
// Original code by Erin Carmody for a summer math institute at
// Reed College.
//
// Modified by Ethan Bolker for Yu Zhang, June 2007
//
// Modification notes.
//
// I tried (insofar as possible) to separate the group action from the
// geometric rotations of the square. 
//
// The code in main seems to be duplicated in RotateSquareApplet.java,
// for which I don't have the source. So I have avoided making 
// any changes there.
//
// I have added to but haven't altered any of the existing fields, since
// they are not marked "private" and I don't know who might be referring 
// to them.
//
// At first I followed Carmody's design, constructing (parallel) arrays 
// to hold data, with static ints for constants. But when I wanted to 
// recover her elegant color changes in the middle of a rotation I 
// refactored, introducing real objects to represent group elements, with
// polymorphism replacing the if-else if ... constructions. 

package graphics3d;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
   @author Erin Carmody
 */
public class RotateSquare extends JPanel implements ActionListener 
{

    // *****************************************
    // static fields and methods

//         private static boolean DEBUG = false;
    private static boolean DEBUG = true;

    private static String VERSION = "2.15";

    // Here's the declaration for the RotateSquare object.
    static RotateSquare rotate;

    // Geometric manipulations of the square 
    // Unfortunately, these are in a random order ...
    // and HORIZONTAL and VERTICAL are reversed in the gui.
    static final int HORIZONTAL = 0;
    static final int VERTICAL   = 1;
    static final int XYROTATION1 = 2;
    static final int XYROTATION2 = 3;
    static final int XYROTATION3 = 4;
    static final int DIAGONAL1 = 5;
    static final int DIAGONAL2 = 6;
    static final int RESET = 7;

    // These fields specify the vertices and faces of a cube.
    // They are probably used by the rotation code in the graphics3d package.
    private static double[][] vertices = {{1, 1, 1}, 
				  { 1, 1, -1},
				  { 1, -1, 1 },
				  { 1, -1, -1 },
				  { -1, 1, 1 },
				  { -1, 1, -1 },
				  { -1, -1, 1 },
				  { -1, -1, -1 } };
    static int[][] faces = { {1, 0, 2, 3},
			     {4, 5, 7, 6},
			     {0, 1, 5, 4},
			     {3, 2, 6, 7},
			     {2, 0, 4, 6},
			     {1, 3, 7, 5}};


    // end of static stuff (except for main()
    // *****************************************

    // Group elements
    private final int IDENTITY = 0;
    private final int R0 = 0;
    private final int R1 = 1;
    private final int R2 = 2;
    private final int R3 = 3;
    private final int M1 = 4; // vertical mirror
    private final int M2 = 5; // horizontal mirror
    private final int D1 = 6;
    private final int D2 = 7;

    // Map modes to states
    private int[] mode2state = {
	4, //     static final int HORIZONTAL = 0;
	5, //     static final int VERTICAL   = 1;
	1, //     static final int XYROTATION1 = 2;
	2, //     static final int XYROTATION2 = 3;
	3, //     static final int XYROTATION3 = 4;
	6, //     static final int DIAGONAL1 = 5;
	7, //     static final int DIAGONAL2 = 6;
	0  //     static final int RESET = 7;
    };

    // array of group elements
    private GroupElement[] elements;
    
    // Group multiplication, specified as a two dimensional array.
    // If you want to generalize to an arbitrary dihedral group you'll
    // need to replace this with an object with a method that performs
    // the multiplication - probably by representing the group
    // elements in terms of two generators - a rotation through 2*pi/n
    // and one of the flips.

    private int[][] groupTable = {
	{IDENTITY, R1, R2, R3, M1, M2, D1, D2 },
	{R1, R2, R3, IDENTITY, D1, D2, M2, M1 },
	{R2, R3, IDENTITY, R1, M2, M1, D2, D1 },
	{R3, IDENTITY, R1, R2, D2, D1, M1, M2 },
	{M1, D2, M2, D1, IDENTITY, R2, R3, R1 },
	{M2, D1, M1, D2, R2, IDENTITY, R1, R3 },
	{D1, M1, D2, M2, R1, R3, IDENTITY, R2 },
	{D2, M2, D1, M1, R3, R1, R2, IDENTITY }
    };

    // for debugging
    private void printGroupTable() 
    {
	System.out.print("   ");
	for (int i=0; i < 8; i++) {
	    System.out.print( elements[i].name );
	}
	System.out.println();
	for (int i=0; i < 8; i++) {
	    System.out.print( elements[i].name );
	    for (int j=0; j < 8; j++) {	    
		System.out.print( elements[groupTable[i][j]].name );
	    }
	    System.out.println();
	}
    }

    // the geometric operation that's about to be performed
    private int mode; 

    // the current state of the square (the symmetry that would
    // get you here from RESET).
    private int state;

    // for debugging - count number of rotations
    private int startCounter = 0;

    // True while a rotation is happening.
    private boolean isPainting;

    // Counts iterations in a painting loop - will always
    // end at a multiple of 30 since rotations are through 
    // a multiple of 90 degrees and there's a repaint every
    // three degrees. Reset for each new rotation.
    private int paintCounter = 0;

    // fields that the rotation package needs -
    // don't touch them!
    //
    // The dxxx fields seem to represent deltas for angles.
    // I take advantage of the fact that they are all 3 
    // when changing the colors on the fly - every 45/3 = 15
    // calls from the timer to repaint.

    double theta = 0;
    double dtheta = 3;
    double phi = 0;
    double dphi = 3;
    double psi = 0;
    double dpsi = 3;
    double nu = 0;
    double dnu = 3;
    double ro = 0;
    double dro = 3;

    // These seem to be used to represent a permutation of the
    // vertices. My design uses group multiplication directly 
    // rather than relying on the permutation representation.
    int[] P = {0, 1, 2, 3};
    int p0 = 0;
    int p1 = 1;
    int p2 = 2;
    int p3 = 3;

    javax.swing.Timer timer;
    int interval = 30;

    /**
     * Constructor
     */
    public RotateSquare() {
	super();
	buildGroup();
	setBackground(new Color(.9f, .9f, .9f));
	timer = new javax.swing.Timer(interval, this);
	if (DEBUG) {
	    System.out.println("RotateSquare constructor " + VERSION );
	    printGroupTable();
	}
	paintCounter = 0;
	isPainting = false;
    }

    // populate the array of group elements
    private void buildGroup()
    {
	elements = new GroupElement[8];
	elements[R0] 
	    = new GroupElement( R0, "R0 ", new Color(.2f, 0f, 1f), 
				new int[] {R0}); 
	elements[R1] 
	    = new GroupElement( R1, "R1 ", new Color(.6f, 0f, 1f), 
				new int[]  {R1, R0} ); //45, 90 
	elements[R2] 
	    = new GroupElement( R2, "R2 ", new Color(.6f, 0f, .6f), 
				new int[] {R1, R0, R1, R0}); //45, 90, 135, 180
	elements[R3] 
	    = new GroupElement( R3, "R3 ", new Color(1f, .2f, .4f), 
				new int[] {R1, R0, R1, R0, R1, R0} );
	elements[M1] 
	    = new GroupElement( M1, "M1 ", new Color(1f, .4f, 0), 
				new int[] {R0, M1, R0, R0}); //45, 90, 135, 180
	elements[M2] 
	    = new GroupElement( M2, "M2 ", new Color(.8f, .8f, 0f), 
				new int[] {R0, M2, R0, R0} );
	elements[D1] 
	    = new GroupElement( D1, "D1 ", new Color(.2f, .8f, .2f), 
				new int[] {R0, D1, R0, R0});
	elements[D2] 
	    = new GroupElement( D2, "D2 ", new Color(0f, .8f, .6f), 
				new int[] {R0, D2, R0, R0} );
    };

    public void setMode(int m) {
	if (isPainting) {
	    return;
	}
	if (DEBUG) {
	    System.out.println("Set mode " + m);
	}
	mode = m;
    }

    public void setState(int s) {
	if (DEBUG) {
	    System.out.println("Set state " + elements[s].name);
	}
	state = s;
    }

    // Here's the code that governs the geometric rotations.
    // It's unchanged. It could probably be simplified 
    // since I'm not using the array P[] to set colors.
    // But I won't try to fix something that's not broken.
    //
    // Only change is the preamble, to change colors during a
    // rotation my way.
    //
    public void actionPerformed(ActionEvent event) {

	if (DEBUG) {
	    System.out.println("actionPerformed(): isPainting " + isPainting);
	}
	if (isPainting) {
	    paintCounter++;
	    if ((paintCounter % 15) == 0 ) {
		int newState = elements[mode2state[mode]].nextState(state);
		setState( newState );
		// check to see if rotation is done
		isPainting =  elements[mode2state[mode]].isPainting();
	    }
	}

	if (mode == HORIZONTAL) {
	    theta -= dtheta;
	    if (theta == -180) {
		timer.stop();
		p0 = P[0];
		P[0] = P[1];
		P[1] = p0;
		p3 = P[3];
		P[3] = P[2];
		P[2] = p3;
		theta = 0;
	    }
	}
	if (mode == VERTICAL) {
	    phi -= dphi;
	    if (phi == -180) {
		timer.stop();
		p0 = P[0];
		P[0] = P[3];
		P[3] = p0;
		p1 = P[1];
		P[1] = P[2];
		P[2] = p1;
		phi = 0;
	    }
	}
	if (mode == DIAGONAL1) {
	    nu -= dnu;
	    if (nu == -180) {
		timer.stop();
		p0 = P[0];
		P[0] = P[2];
		P[2] = p0;
		nu = 0;
	    }
	}
	if (mode == DIAGONAL2) {
	    ro -= dro;
	    if (ro == -180) {
		timer.stop();
		p1 = P[1];
		P[1] = P[3];
		P[3] = p1;
		ro = 0;
	    }
	}
	if (mode == XYROTATION1) {
	    psi += dpsi;
	    if (psi == 90) {
		timer.stop();
		p0 = P[0];
		P[0] = P[1];
		P[1] = P[2];
		P[2] = P[3];
		P[3] = p0;
		psi = 0;
	    }
	}
	if (mode == XYROTATION2) {
	    psi += dpsi;
	    if (psi == 90) {
		p0 = P[0];
		P[0] = P[1];
		P[1] = P[2];
		P[2] = P[3];
		P[3] = p0;
	    }
	    if (psi == 180) {
		timer.stop();
		p0 = P[0];
		P[0] = P[1];
		P[1] = P[2];
		P[2] = P[3];
		P[3] = p0;
		psi = 0;
	    }
	}
	if (mode == XYROTATION3) {
	    psi += dpsi;
	    if (psi == 90) {
		p0 = P[0];
		P[0] = P[1];
		P[1] = P[2];
		P[2] = P[3];
		P[3] = p0;
	    }
	    if (psi == 180) {
		p0 = P[0];
		P[0] = P[1];
		P[1] = P[2];
		P[2] = P[3];
		P[3] = p0;
	    }
	    if (psi == 270) {
		timer.stop();
		p0 = P[0];
		P[0] = P[1];
		P[1] = P[2];
		P[2] = P[3];
		P[3] = p0;
		psi = 0;
	    }
	}
	if (mode == RESET) {
	    P[0] = 0;
	    P[1] = 1;
	    P[2] = 2;
	    P[3] = 3;
	    psi = 0;
	    theta = 0;
	    phi = 0;
	    ro = 0;
	    nu = 0;
	}

	repaint();
    }

    public void start() {
	if (DEBUG) {
	    System.out.println("start(): isPainting " + isPainting);
	}
	if (isPainting) {
	    return;
	}
	if (mode == RESET) {
	    setState( IDENTITY );
	    paintCounter = 0;
	    repaint();
	    isPainting = false;
	    return;
	}
	isPainting = true;
	paintCounter = 0;
	if (DEBUG) {
	    System.out.println("startCounter " + ++startCounter);
	}

	// reset counters in the GroupElement that's about to
	// be applied
	elements[mode2state[mode]].startPaint();

	if (!timer.isRunning()) {
	    timer.start();
	}
    }

    public void stop() {
	if (timer.isRunning()) timer.stop();
    }

    public void paintComponent(Graphics gfx) {
	super.paintComponent(gfx);
	if (DEBUG) {
	    System.out.println("paintComponent(): isPainting " + isPainting);
	    System.out.println("paintCounter " + paintCounter);
	}
	Graphics2D g2 = (Graphics2D) gfx;
	g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			    RenderingHints.VALUE_ANTIALIAS_ON);
	Graphics3d g = new Graphics3d(g2);
	g.setPixCenter(200, 175);
	g.setPixScale(30, 30);
	g.setEye(new double[] {0, 0, 20, 1});

	//	g.printDisplay();

	g.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_SQUARE,
				    BasicStroke.JOIN_MITER, 2));

	//square and its movements
	g.rotateY(theta);
	g.rotateX(phi);
	g.rotateZ(psi);
	g.rotate(new double[]{-1, 1, 0}, nu);
	g.rotate(new double[]{1, 1, 0}, ro);

	// setColorsErinsWay( g );

	g.setColor( elements[state].color );

	g.newpath();
	g.moveto(-4, -4, 0);
	g.lineto(-4, 4, 0);
	g.lineto(4, 4, 0);
	g.lineto(4, -4, 0);
	g.fill();
	g.draw();
    }

    // No longer called.
    // Perhaps the array P[] is no longer needed at all.
    //
    private void setColorsErinsWay( Graphics3d g )
    {
	g.setColor(new Color(.2f, 0f, 1f));
	//R1
	if (P[0] == 0 && P[1] == 1 && P[2] == 2 && P[3] == 3) {
	    g.setColor(new Color(.2f, 0f, 1f)); 
	}
	//M2
	if (P[0] == 3 && P[1] == 2 && P[2] == 1 && P[3] == 0) {
	    g.setColor(new Color(.8f, .8f, 0f));
	}
	//M1
	if (P[0] == 1 && P[1] == 0 && P[2] == 3 && P[3] == 2) {
	    g.setColor(new Color(1f, .4f, 0f));
	}
	//R2	
	if (P[0] == 1 && P[1] == 2 && P[2] == 3 && P[3] == 0) {
	    g.setColor(new Color(.6f, 0f, 1f));
	}
	//R3
	if (P[0] == 2 && P[1] == 3 && P[2] == 0 && P[3] == 1) {
	    g.setColor(new Color(.6f, 0f, .6f));
	}
	//R4
	if (P[0] == 3 && P[1] == 0 && P[2] == 1 && P[3] == 2) {
	    g.setColor(new Color(1f, .2f, .4f));
	}
	//D1
	if (P[0] == 2 && P[1] == 1 && P[2] == 0 && P[3] == 3) {
	    g.setColor(new Color(.2f, .8f, .2f));
	}
	//D2
	if (P[0] == 0 && P[1] == 3 && P[2] == 2 && P[3] == 1) {
	    g.setColor(new Color(0f, .8f, .6f));
	}
	if (mode == RESET) {
	    g.setColor(new Color(.2f, 0f, 1f));
	}
    }

    private class GroupElement
    {
	protected int elementId;
	protected String name;
	protected Color color;

	// array of group elements to multiply by
	// at each 45 degree increment in rotation
	protected int[] intermediates;

	// index into intermediates array
	// increases each 45 degrees of rotation
	private int paintIndex;

	public GroupElement( int elementId,
			     String name, 
			     Color color,
			     int[] intermediates)
	{
	    this.elementId     = elementId;
	    this.name          = name;
	    this.color         = color;
	    this.intermediates = intermediates;
	    paintIndex = 0;
	}

	public void startPaint()
	{
	    paintIndex = 0;
	}

	public boolean isPainting() 
	{
	    return paintIndex < intermediates.length;
	}

	public int nextState( int currentState )
	{
	    int next = groupTable[intermediates[paintIndex]][currentState];
	    paintIndex++;
	    return next;
	}
    }

    // DON'T CHANGE ANYTHING IN MAIN SINCE CODE HERE 
    // IS (PROBABLY) DUPLICATED IN RotateSquareApplet.java
    //
    public static void main(String[] args) {
	rotate = new RotateSquare();
	rotate.setPreferredSize(new Dimension(400, 350));

	JButton horizontal = new JButton("Mirror 1");
	horizontal.setBackground(new Color(1f, .4f, 0));
	JButton vertical = new JButton("Mirror 2");
	vertical.setBackground(new Color(.8f, .8f, 0f));
	JButton xyrotation1 = new JButton("Rotation 1");
	xyrotation1.setBackground(new Color(.6f, 0f, 1f));
	JButton xyrotation2 = new JButton("Rotation 2");
	xyrotation2.setBackground(new Color(.6f, 0f, .6f));
	JButton xyrotation3 = new JButton("Rotation 3");
	xyrotation3.setBackground(new Color(1f, .2f, .4f));
	JButton xyrotation0 = new JButton("Rotation 0");
	xyrotation0.setBackground(new Color(.2f, 0f, 1f));
	JButton diagonal1 = new JButton("Diagonal 1");
	diagonal1.setBackground(new Color(.2f, .8f, .2f));
	JButton diagonal2 = new JButton("Diagonal 2");
	diagonal2.setBackground(new Color(0f, .8f, .6f));
	JButton reset = new JButton("Reset");

	//	start.addActionListener(rotate);
	horizontal.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.HORIZONTAL);
		    rotate.start();
		}
	    });

	vertical.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.VERTICAL);
		    rotate.start();
		}
	    });

	xyrotation1.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.XYROTATION1);
		    rotate.start();
		}
	    });

	xyrotation2.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.XYROTATION2);
		    rotate.start();
		}
	    });

	xyrotation3.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.XYROTATION3);
		    rotate.start();
		}
	    });

	diagonal1.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.DIAGONAL1);
		    rotate.start();
		}
	    });

	diagonal2.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.DIAGONAL2);
		    rotate.start();
		}
	    });

	
	reset.addActionListener(new ActionListener() 
	    {
		public void actionPerformed(ActionEvent e) {
		    rotate.setMode(RotateSquare.RESET);
		    rotate.start();
		}
	    });

	JPanel gpanel = new JPanel();
	gpanel.setLayout(new GridLayout(2,4));
	gpanel.add(xyrotation0);
	gpanel.add(xyrotation1);
	gpanel.add(xyrotation2);
	gpanel.add(xyrotation3);
	gpanel.add(horizontal);
	gpanel.add(vertical);
	gpanel.add(diagonal1);
	gpanel.add(diagonal2);
	JPanel bpanel = new JPanel();
	bpanel.add(gpanel);
	bpanel.add(reset);

	JFrame frame = new JFrame("Symmetries of the Square");
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.getContentPane().add(rotate, BorderLayout.CENTER);
	//frame.getContentPane().add(gpanel, BorderLayout.SOUTH);
	frame.getContentPane().add(bpanel, BorderLayout.SOUTH);
	frame.pack();
	frame.setVisible(true);
    }
}


