// MOtion CApture program

import java.io.*;
import java.awt.*;
import java.util.*;
import java.awt.event.*;
import com.sun.j3d.utils.universe.*;
import com.sun.j3d.utils.geometry.ColorCube;
import javax.media.j3d.*;
import javax.vecmath.*;


class MoCa extends Frame implements WindowListener, KeyListener, 
                                    ActionListener, AdjustmentListener {

    Joint m_root;
    MoCaCanvas m_canvas;
    ViewingPlatform m_view;
    TextArea m_text;
    MenuBar m_menubar;
    Scrollbar m_scrollbar;
    
    SignalProcessor []m_signal;
    GraphicEqualizer m_equalizer;
    
    int m_numberOfFrames;
    int m_frameDelay;
    int m_vectorsPerLine;
    Vector m_motionVector;
    Enumeration m_motionData;
    Vector m_joints;
    
    TransformGroup m_objSpin;
    Transform3D m_objSpinTransform;
    
    int m_runState;
    CheckboxMenuItem m_freezeRoot;
    
    RotationInterpolator m_rotator;
    
    final static int READY = 0;
    final static int LOADING = 1;
    final static int PLAY = 2;

    // CONSTRUCTORS
    
    MoCa(String filename) throws IOException {
  		//{{INIT_CONTROLS
		setTitle("Motion Capture");
    	setSize(500, 650);
		setLayout(new BorderLayout());
		//}}
		//{{INIT_MENUS
		//}}

        // create and add a Canvas3D
        m_canvas = new MoCaCanvas();//MoCaCanvas();
		add("Center", m_canvas);
		
		//m_text = new TextArea();
		//getContentPane().add("South", m_text);
		
		// create and add a menu
		Menu menu;
		MenuItem item;
		
		m_menubar = new MenuBar();
		setMenuBar(m_menubar);
		
		menu = new Menu("File");
		m_menubar.add(menu);
		item = new MenuItem("Load");
		item.addActionListener(this);
		menu.add(item);
		menu.addSeparator();
		item = new MenuItem("Exit");
		item.addActionListener(this);
		menu.add(item);
		
		menu = new Menu("Motion");
		m_menubar.add(menu);
		item = new MenuItem("Play");
		item.addActionListener(this);
		menu.add(item);
		menu.addSeparator();
		item = new MenuItem("Reset Skeleton");
		item.addActionListener(this);
		menu.add(item);
		menu.addSeparator();
		m_freezeRoot = new CheckboxMenuItem("Freeze Root", false);
		m_freezeRoot.addActionListener(this);
		menu.add(m_freezeRoot);		
	
		// create and add the scroll bar
		m_scrollbar = new Scrollbar(Scrollbar.HORIZONTAL, 0, 5, 0, 100);
		m_scrollbar.setEnabled(false);
		m_scrollbar.addAdjustmentListener(this);
		add("South", m_scrollbar);
		
        // add a window listener
        addWindowListener(this);
        m_canvas.addKeyListener(this);

        m_numberOfFrames = 0;
        m_motionVector = new Vector();
        m_joints = new Vector();
        
        setRunState(READY);

        loadMotionCapture(filename);
    	initializeFrame();
	}
	
	
	// PUBLIC METHODS
	
    public static void main(String args[]) {
        MoCa capture = null;
        boolean ok = true;
        
        if (args.length == 0) {
            System.out.println("No file given");
            System.exit(0);
        }
        
        System.out.println("Loading from file "+args[0]);

        // load the BVH file
        try {
            capture = new MoCa(args[0]);
        } catch(IOException ioe) {
            System.out.println("error reading file: \n\t"+ ioe.getMessage());
            ok = false;
        }

        // spit out body structure... for debugging
        //traverseJoint(capture.m_root, 0);

        // get the form of the body to animate
        if (ok) capture.getReferenceModel();
        else System.exit(0);
    }

    public static void traverseJoint(Joint traverse, int level) {
        for (int i = 0; i < level; i++)
            System.out.print(" ");

        System.out.println(traverse.name);

        if (traverse.firstChild != null) traverseJoint(traverse.firstChild, level+1);
        if (traverse.nextSibling != null) traverseJoint(traverse.nextSibling, level);
    }

	
	
	// PRIVATE METHODS

    private synchronized void setRunState(int newState) {
        m_runState = newState;
    }
    
    private synchronized int getRunState() {
        return m_runState;
    }

    private void initializeFrame() {        
		BranchGroup scene = createSceneGraph();
		scene.compile();
		
		SimpleUniverse simpleU = new SimpleUniverse(m_canvas);

		// This moves the ViewPlatform back a bit so the
		// objects in the scene can be viewed
		m_view = simpleU.getViewingPlatform();
		m_view.setNominalViewingTransform();
		
		// Move camera back a bit
		TransformGroup viewTransformGroup = m_view.getViewPlatformTransform();
		Transform3D viewTransform3D = new Transform3D();
		viewTransformGroup.getTransform(viewTransform3D);
		
		Transform3D initialTransform3D = new Transform3D();
		initialTransform3D.setTranslation(new Vector3f(0f, 0f, 5f));
		viewTransform3D.mul(initialTransform3D);
		
		viewTransformGroup.setTransform(viewTransform3D);
		
        // add scene to universe
		simpleU.addBranchGraph(scene);
    }
    
    private void traverseUniverse(Group group, int level) {
        for (int i = 0; i < level; i++)
            System.out.print(" ");

        System.out.println(group);
        
        Enumeration children = group.getAllChildren();
        
        while (children.hasMoreElements()) {
            Node node = (Node)children.nextElement();
            if (node instanceof Group)
                traverseUniverse((Group)node, level + 1);
        }
    }
    
    private void rotateUniverse(float deltaY) {
        Transform3D rotate = new Transform3D();
        rotate.rotY(deltaY*Joint.DEGtoRAD);
        
        Transform3D objTransform = new Transform3D();
        m_objSpin.getTransform(objTransform);
        
        objTransform.mul(rotate);
        m_objSpin.setTransform(objTransform);
    }
    
	private BranchGroup createSceneGraph() {
		// Create the root of the branch graph
		BranchGroup objRoot = new BranchGroup();
		objRoot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		objRoot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		objRoot.setCapability(TransformGroup.ALLOW_CHILDREN_READ);


		// Create the transform group node and initialize it to the 
		// identity. Add it to the root of the subgraph.
		TransformGroup objSpin = new TransformGroup();
		objSpin.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		objSpin.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		objSpin.setCapability(TransformGroup.ALLOW_CHILDREN_READ);
		objRoot.addChild(objSpin);
		m_objSpin = objSpin;
		m_objSpinTransform = new Transform3D();
		objSpin.getTransform(m_objSpinTransform);
		
		// create time varying function to drive the animation
		Alpha rotationAlpha = new Alpha(-1, 4000);
		
		// Create a new Behavior object that performs the desired
		// operation on the specified transform object and add it into
		// the scene graph.
		m_rotator = new RotationInterpolator(rotationAlpha, objSpin);

		// a bounding sphere specifies a region a behavior is active
		BoundingSphere bounds = new BoundingSphere();
		m_rotator.setSchedulingBounds(bounds);
		objSpin.addChild(m_rotator);
		objSpin.addChild(m_root.getTransformGroup());
		
		return objRoot;
	} // end of createSceneGraph method
    

    private void loadMotionCapture(String filename) throws IOException {
        setRunState(LOADING);
        // Read in the file
        BufferedReader inFile = new BufferedReader(new FileReader(filename));

        loadReferenceSkeleton(inFile);
        loadMotionData(inFile);

        inFile.close();
        if (m_root != null) addJoints(m_root);
        m_scrollbar.setEnabled(true);
        setRunState(READY);
    }
    
    private void loadReferenceSkeleton(BufferedReader input) throws IOException {
        // get to start of reference skeleton
        if (!input.readLine().equals("HIERARCHY")) {
            throw new IOException("File does not appear to be in proper BVH format");
        } else {
            m_root = new Joint(input);
        }
    }
    
    private void loadMotionData(BufferedReader input) throws IOException {
        setRunState(LOADING);
        StringTokenizer parser;
        String line="";
        
        int num_frames;
        float frame_frequency;
        
        // get to start of captured data
        while(!input.readLine().equals("MOTION"));
        
        // read capture data header info: number of frames; display frequency
        try {
            line = input.readLine();
            parser = new StringTokenizer(line);
            parser.nextToken();
            num_frames = Integer.parseInt(parser.nextToken());
        
            line = input.readLine();
            parser = new StringTokenizer(line);
            parser.nextToken();
            parser.nextToken();
            frame_frequency = (Float.valueOf(parser.nextToken())).floatValue();
        } catch (NumberFormatException nfe) {
            throw new IOException(line+"   ->  number could not be parsed");
        }
        m_numberOfFrames = num_frames;
        m_scrollbar.setMaximum(m_numberOfFrames);
        m_frameDelay = (int)(frame_frequency * 1000f);
        
        float nums[] = new float[3];
        nums[0] = nums[1] = nums[2] = 0;
        
        int angleCount=0;
        int lineCount=0;
        Vector3f tuple = null;
        
        try {
            for (lineCount = 0; lineCount < num_frames; lineCount++) {
                parser = new StringTokenizer(input.readLine());
                angleCount = -1;
                while (parser.hasMoreTokens()) {
                    for (int numCount = 0; numCount < 3; numCount++) {
                        nums[numCount] = (Float.valueOf(parser.nextToken())).floatValue();
                    }
                    
                    if (angleCount == -1) {     // if this is position data
                        nums[0] = nums[0] / 25.0f;
                        nums[1] = nums[1] / 25.0f;
                        nums[2] = nums[2] / 25.0f;
                        
                        tuple = new Vector3f(nums[0], nums[1], nums[2]);
                    }
                    else                        // else if this is an angle
                        tuple = new Vector3f(nums[1], nums[2], nums[0]);

                    angleCount++;
                    m_motionVector.addElement(tuple);
                }
                m_vectorsPerLine = angleCount+1;
            }
        } catch (NumberFormatException nfe) {
            throw new IOException("number could not be parsed on frame number "+lineCount);
        }
        
        m_motionData = m_motionVector.elements();
        
        System.out.println(num_frames+" frames read in");
        setRunState(READY);
    }

    public void getReferenceModel() {
        if (m_root != null) {
            addBones(m_root);       // set bone structure to joints
            //m_canvas.printBones();
        }

        setVisible(true);
    }

    private void addBones(Joint root) {
		m_canvas.addBone(root);
		
        if (root.firstChild != null) addBones(root.firstChild);
        if (root.nextSibling != null) addBones(root.nextSibling);
    }

    private void addJoints(Joint root) {
		if (!root.name.equals("End Site")) {
		    m_joints.addElement(root);
		}
		
        if (root.firstChild != null) addJoints(root.firstChild);
        if (root.nextSibling != null) addJoints(root.nextSibling);
    }
    
    private void resetSkeleton() {
        // reset character to face front again
        m_root.setOriginalOrientation();
        m_objSpin.setTransform(m_objSpinTransform);
    }

   
    private void playMotionCapture() {
        setRunState(PLAY);
        if (m_rotator.getEnable()) {
            m_rotator.setEnable(false);
            resetSkeleton();
        }
        
        // PLAY!
        for (int frameCount = m_scrollbar.getValue(); frameCount < m_numberOfFrames; frameCount++) {
            setToFrame(frameCount);
            m_scrollbar.setValue(frameCount);
            if (getRunState() != PLAY) return;
            try {
                Thread.sleep(m_frameDelay);
            } catch(InterruptedException ie) {}
        }
        setRunState(READY);
    }
    
    private void processSignals() {
        int bands = 0;
        
        boolean showGraphs = false;
        m_signal = new SignalProcessor[9];
        for (int i = 1; i < 9; i++) {
            if (i == 1) showGraphs = true;
            else showGraphs = false;
            
            m_signal[i] = new SignalProcessor(m_motionVector, 
                                            m_numberOfFrames, i, showGraphs);
            
            bands = m_signal[i].getNumberOfBands();
        }
        
        m_equalizer = new GraphicEqualizer(bands);
        m_equalizer.setVisible(true);
    }
    
    private void reconstructSignals() {
        Vector gains = new Vector();
        gains = m_equalizer.getBands();
        
        for (int i = 1; i < 9; i++) {
            //m_signal = new SignalProcessor(m_motionVector, 
            //                                m_numberOfFrames, i, false);
            m_signal[i].reconstructSignal(gains);
        }
    }
    
    public void setToFrame(int frameCount) {
        int num_joints = m_joints.size();
        Joint joint;
        
        // Set root position
        //Vector3f rootPos = (Vector3f)m_motionData.nextElement();
        if (!m_freezeRoot.getState()) {
            Vector3f rootPos = (Vector3f)m_motionVector.elementAt(frameCount*m_vectorsPerLine);
            joint = (Joint)m_joints.elementAt(0);
            joint.setPosition(new Vector3f(rootPos.x, rootPos.y, rootPos.z));
        }
        
        // Rotate all joints
        for (int jointCount = 0; jointCount < num_joints; jointCount++) {
            //Vector3f angle = (Vector3f)m_motionData.nextElement();
            Vector3f angle = (Vector3f)m_motionVector.elementAt(
                                    frameCount*m_vectorsPerLine+jointCount+1);
            joint = (Joint)m_joints.elementAt(jointCount);
                
            joint.setRotationsDegrees(angle.x, angle.y, angle.z);
        }
    }
    
    // INTERFACE IMPLEMENTED FUNCTIONS
    public void windowActivated(WindowEvent e){}
    public void windowClosed(WindowEvent e) {}
    public void windowClosing(WindowEvent e) {System.exit(0);}
    public void windowDeactivated(WindowEvent e) {}
    public void windowDeiconified(WindowEvent e) {}
    public void windowIconified(WindowEvent e) {}
    public void windowOpened(WindowEvent e) {}
    
    public void keyPressed(KeyEvent e) {
        TransformGroup viewTransform= m_view.getViewPlatformTransform();
        Transform3D transform = new Transform3D();
        viewTransform.getTransform(transform);
                    
        Transform3D move = new Transform3D();

        switch (e.getKeyChar()) {
            case '1':
                //move.rotY(-5*Math.PI/180);
                rotateUniverse(-5f);
                return;

            case '3':
                //move.rotY(5*Math.PI/180);
                rotateUniverse(5f);
                return;

            case '9':
                move.setTranslation(new Vector3f(0f, 0f, -0.1f));
                break;

            case '7':
                move.setTranslation(new Vector3f(0f, 0f, 0.1f));
                break;

            case '8':
                move.setTranslation(new Vector3f(0f, 0.1f, 0f));
                break;

            case '2':
                move.setTranslation(new Vector3f(0, -0.1f, 0f));
                break;
                
            case '4':
                move.setTranslation(new Vector3f(-0.1f, 0f, 0f));
                break;

            case '6':
                move.setTranslation(new Vector3f(0.1f, 0f, 0f));
                break;
                
            case 's':
            case 'S':
                processSignals();
                return;

            case 'r':
            case 'R':
                reconstructSignals();
                return;
                
            case 'P':
            case 'p':
                playMotionCapture();
                return;
        }
        
        transform.mul(move);
        viewTransform.setTransform(transform);
    }
    public void keyReleased(KeyEvent e) {}
    public void keyTyped(KeyEvent e) {}

    public void actionPerformed(ActionEvent e) {
        String cmd = e.getActionCommand();
        
        if (cmd.equals("Play")) {
            playMotionCapture();
        } else
        if (cmd.equals("Exit")) {
            System.exit(0);
        } else
        if (cmd.equals("Reset Skeleton")) {
            resetSkeleton();
        }
    }
    
    public void adjustmentValueChanged(AdjustmentEvent e) {
        if (m_rotator.getEnable()) {
            m_rotator.setEnable(false);
        }
        setToFrame(e.getValue());
    }

	//{{DECLARE_CONTROLS
	//}}
	//{{DECLARE_MENUS
	//}}
}
