package net.mkonrad.synes.ctrl;

import java.awt.AWTEvent;
import java.awt.Event;
import java.awt.event.MouseEvent;
import java.util.Enumeration;

import javax.media.j3d.BranchGroup;
import javax.media.j3d.Node;
import javax.media.j3d.SceneGraphPath;
import javax.media.j3d.Shape3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.WakeupCriterion;
import javax.media.j3d.WakeupOnAWTEvent;
import javax.media.j3d.WakeupOr;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;

import net.mkonrad.synes.data.AbstractGraph;
import net.mkonrad.synes.data.Beat;
import net.mkonrad.synes.data.BeatGraph;
import net.mkonrad.synes.data.Instrument;
import net.mkonrad.synes.data.Layer;
import net.mkonrad.synes.data.LayerSet;
import net.mkonrad.synes.data.SynthGraph;
import net.mkonrad.synes.data.SynthObj;
import net.mkonrad.synes.gui.SceneCore;
import net.mkonrad.synes.gui.SceneCoreOSC;
import net.mkonrad.synes.gui.elements.AbstractGraphLine;
import net.mkonrad.synes.gui.elements.BeatGraphLine;
import net.mkonrad.synes.gui.elements.GroundShape;
import net.mkonrad.synes.gui.elements.SynthGraphLine;
import net.mkonrad.synes.gui.elements.instruments.InstrumentShape;
import net.mkonrad.synes.osc.OSCMsgProcessor;
import net.mkonrad.synes.util.Common3DTools;
import net.mkonrad.synes.util.DbgTools;
import net.mkonrad.synes.util.MusicTools;

import com.sun.j3d.utils.geometry.Primitive;
import com.sun.j3d.utils.picking.PickIntersection;
import com.sun.j3d.utils.picking.PickResult;
import com.sun.j3d.utils.picking.PickTool;
import com.sun.j3d.utils.picking.behaviors.PickMouseBehavior;

import de.sciss.net.OSCMessage;

public class CentralPickingCtrl extends PickMouseBehavior implements OSCMsgProcessor
{
  public final static int MOUSE_NOCLICK  = -1;
  public final static int MOUSE_ONECLICK = 0;
  public final static int MOUSE_DBLCLICK = 1;
  public final static int MOUSE_DRAGGING = 2;
  public final static int MOUSE_RELEASED = 3;
  
  public final static int STATE_READY = 0;
  public final static int STATE_GRAPH_CREATE = 1;
  public final static int STATE_BEAT_IMPRO = 2;
  public final static int STATE_GRAPH_PAUSED = 3;
  public final static int STATE_GRAPH_EDIT = 4;
  public final static int STATE_GRAPH_MOVE = 5;
  
  private LayerSet _layers;
  private SceneCoreOSC _coreCtrl;
  
  private int _currentGraphId = -1;
  private AbstractGraphLine _currGraphLine = null;
  
  private SeqLayerPickingCtrl _seqLayerPickingCtrl;
  private SynthLayerPickingCtrl _synthLayerPickingCtrl;
  
  private SeqLinePickingCtrl _seqLinePickingCtrl;
  private SynthLinePickingCtrl _synthLinePickingCtrl;
  
  private BeatPickingCtrl _beatPickingCtrl;
  private SynthObjPickingCtrl _synthPickingCtrl;
  
  private AbstractLayerPickingCtrl _layerPickingCtrl;
  private AbstractPickableObj _synthObjPickingCtrl;
  private AbstractLinePickingCtrl _linePickingCtrl;
  
  private int _mouseState = MOUSE_NOCLICK;
  private int _state = STATE_READY;
  
  private Object _notify = new Object();
  
  private Instrument _currInstrument;
  
  public CentralPickingCtrl(BranchGroup root, SceneCore coreCtrl)
  {
    super(coreCtrl.getCanvas3D(), root, SceneCore.getSceneBounds());
    setSchedulingBounds(SceneCore.getSceneBounds());
    
    _layers = coreCtrl.getLayerSet();
    _coreCtrl = (SceneCoreOSC)coreCtrl;
    
    _seqLayerPickingCtrl = new SeqLayerPickingCtrl(this);
    _synthLayerPickingCtrl = new SynthLayerPickingCtrl(this);
    
    _seqLinePickingCtrl = new SeqLinePickingCtrl(this);
    _synthLinePickingCtrl = new SynthLinePickingCtrl(this);
    
    _beatPickingCtrl = new BeatPickingCtrl(this);
    _synthPickingCtrl = new SynthObjPickingCtrl(this);
    
    _layerPickingCtrl = _seqLayerPickingCtrl;  //Seq. is standard at the beginning
    _synthObjPickingCtrl = _beatPickingCtrl;
    _linePickingCtrl = _seqLinePickingCtrl;
    
    _layerPickingCtrl.instrumentSelect(0);
    
    pickCanvas.setMode(PickTool.GEOMETRY_INTERSECT_INFO); //much more accurate...
    //pickCanvas.setMode(pickCanvas.BOUNDS);  //...than this
    pickCanvas.setTolerance(0.0f);
  }
  
  public int getState()
  {
    return _state;
  }

  public void setState(int state)
  {
    _state = state;
  }
  
  public int getMouseState()
  {
    return _mouseState;
  }

  public AbstractLayerPickingCtrl getLayerPickingCtrl()
  {
    return _layerPickingCtrl;
  }

  public AbstractPickableObj getSynthObjPickingCtrl()
  {
    return _synthObjPickingCtrl;
  }

  public AbstractLinePickingCtrl getLinePickingCtrl()
  {
    return _linePickingCtrl;
  }
  
  public SynthLayerPickingCtrl getSynthLayerPickingCtrl()
  {
    return _synthLayerPickingCtrl;
  }
  
  public BeatGraphLine createGraphLine(BeatGraph graph)
  {
    _currGraphLine = new BeatGraphLine(_coreCtrl);
    _currGraphLine.initGraph(graph);
    graph.setGraphLine(_currGraphLine);
    
    return (BeatGraphLine) _currGraphLine;
  }
  
  public SynthGraphLine createGraphLine(SynthGraph graph)
  {
    _currGraphLine = new SynthGraphLine(_coreCtrl);
    _currGraphLine.initGraph(graph);
    graph.setGraphLine(_currGraphLine);
    
    return (SynthGraphLine) _currGraphLine;
  }
  
  public void addNewGraphElementsToScene(Node node)
  {
    if (_currGraphLine.getBG().getParent() == null) //the graph itself has not been attached, yet
      _coreCtrl.getCurrLayer().getLayerTG().addChild(_currGraphLine.getBG());
    
    _currGraphLine.getTG().addChild(node);  //attach the beat to the graph BG
  }
  
  public void initialize()
  {
    conditions = new WakeupCriterion[4];
    conditions[0] = new WakeupOnAWTEvent(Event.MOUSE_MOVE);
    conditions[1] = new WakeupOnAWTEvent(Event.MOUSE_DOWN);
    conditions[2] = new WakeupOnAWTEvent(Event.MOUSE_DRAG);
    conditions[3] = new WakeupOnAWTEvent(Event.MOUSE_UP);
    wakeupCondition = new WakeupOr(conditions);

    wakeupOn(wakeupCondition);
  }
  
  private void processMouseEvent(MouseEvent evt)
  {
    buttonPress = false;
    _mouseState = MOUSE_NOCLICK;
    
    if (evt.getID() == MouseEvent.MOUSE_PRESSED)
    {
      buttonPress = true;
      _mouseState = MOUSE_ONECLICK;

      if (evt.getClickCount() >= 2)
      {
        // DbgTools.print("dblclicked");
        _mouseState = MOUSE_DBLCLICK;
      }

      return;
    }
    else if (evt.getID() == MouseEvent.MOUSE_DRAGGED)
    {
      _mouseState = MOUSE_DRAGGING;
      return;
    }
    else if (evt.getID() == MouseEvent.MOUSE_RELEASED)
    {
      buttonPress = false;
      _mouseState = MOUSE_RELEASED;
      return;
    }
    else if (evt.getID() == MouseEvent.MOUSE_MOVED)
    {
      return;
    }
  }
  
  public void processStimulus (Enumeration criteria)
  {
    WakeupCriterion wakeup;
    AWTEvent[] evt = null;
    int xpos = 0, ypos = 0;

    while(criteria.hasMoreElements()) {
      wakeup = (WakeupCriterion)criteria.nextElement();
      if (wakeup instanceof WakeupOnAWTEvent)
          evt = ((WakeupOnAWTEvent)wakeup).getAWTEvent();
    }
    
    if (evt[0] instanceof MouseEvent){
      mevent = (MouseEvent) evt[0];

      processMouseEvent((MouseEvent)evt[0]);
      xpos = mevent.getPoint().x;
      ypos = mevent.getPoint().y;
    }
    
    //DbgTools.print("buttonPress: " + buttonPress + " / _state: " + _state);
    
    if (buttonPress || _state != STATE_READY)
    {
      updateScene(xpos, ypos);
    }
    wakeupOn (wakeupCondition);
  }

  public void updateScene(int posX, int posY)
  {
    pickCanvas.setShapeLocation(posX, posY);
    
    Point3d eyePos = pickCanvas.getStartPosition();  // the viewer location

    PickResult pickResult = pickCanvas.pickClosest();

    if (pickResult != null)
    {
      /*
       * Get interception coordinates
       */
      PickIntersection pickedIntersect = pickResult.getClosestIntersection(eyePos);
      
      if (pickedIntersect == null)
        return;

      Point3f intercept = new Point3f(pickedIntersect.getPointCoordinatesVW());
      
      /*
       * Get picked object
       */
      Primitive pickedPrimitive = (Primitive) pickResult.getNode(PickResult.PRIMITIVE);
      Shape3D pickedShape = null;
      
      if (pickedPrimitive == null)
        pickedShape = (Shape3D) pickResult.getNode(PickResult.SHAPE3D);
      
      //if (pickedShape != null) DbgTools.print("picked shape: " + pickedShape);
      //if (pickedPrimitive != null) DbgTools.print("picked primitive: " + pickedPrimitive);

      /*
       * Process events
       */
      boolean dontFallBackToLayer = false;

      // Picked objects, that handle mouse clicks:
      if (_mouseState == MOUSE_ONECLICK || _mouseState == MOUSE_DBLCLICK)
      {
        if (pickedPrimitive != null && pickedPrimitive.getUserData() instanceof SynthObj)
        {
          if (_coreCtrl.isOrbitMode())
            switchToLayer(pickedPrimitive);
          
          dontFallBackToLayer = _synthObjPickingCtrl.clicked(pickedPrimitive, intercept,
              _mouseState);
        }
        else if (pickedShape instanceof AbstractGraphLine)
        {
          if (_coreCtrl.isOrbitMode())
            switchToLayer(pickedShape);
          
          dontFallBackToLayer = _linePickingCtrl.clicked(pickedShape, intercept,
              _mouseState);
        }
      }
      // Picked objects, that handle mouse drags:
      else if (_mouseState == MOUSE_DRAGGING)
      {
        _coreCtrl.disableOrbitCtrl();
        
        if (_state == STATE_BEAT_IMPRO || _state == STATE_GRAPH_EDIT)
        {
          dontFallBackToLayer = _synthObjPickingCtrl
              .dragged(intercept, _mouseState);
        }
        else if (_state == STATE_GRAPH_MOVE)
        {
          dontFallBackToLayer = _linePickingCtrl.dragged(intercept, _mouseState);
        }
      }
      // Picked objects, that handle mouse releases:
      else if (_mouseState == MOUSE_RELEASED)
      {
        if (_state == STATE_BEAT_IMPRO || _state == STATE_GRAPH_EDIT)
        {
          dontFallBackToLayer = _synthObjPickingCtrl.released(intercept,
              _mouseState);
        }
        else if (_state == STATE_GRAPH_PAUSED || _state == STATE_GRAPH_MOVE)
        {
          dontFallBackToLayer = _linePickingCtrl.released(intercept,
              _mouseState);
        }
        
        _coreCtrl.enableOrbitCtrl();
      }

      // Picked objects, that handle mouse over:
      //if (pickedShape != null)
      //  DbgTools.print("shape is " + pickedShape.toString() + " and dontFallBackToLayer is " + dontFallBackToLayer);
      if (pickedShape instanceof GroundShape || !dontFallBackToLayer)
      {
        if (_mouseState == MOUSE_NOCLICK)
          _layerPickingCtrl.moved(pickedShape, intercept, _mouseState);
        else if (!dontFallBackToLayer && buttonPress)
          _layerPickingCtrl.clicked(pickedShape, intercept, _mouseState);
      }
    }
  }

  private void switchToLayer(Object pickedObj)
  {
    int newLayer = -1;
    Instrument instr = null;
    
    if (pickedObj instanceof Primitive)
    {
      SynthObj synthObj = (SynthObj)((Primitive)pickedObj).getUserData();
      newLayer = synthObj.getGraph().getLayerLevel();
      instr = synthObj.getInstrument();
    }
    else if (pickedObj instanceof AbstractGraphLine)
    {
      newLayer = ((AbstractGraphLine)pickedObj).getGraph().getLayerLevel();
    }
    
    if (newLayer > -1 && newLayer != _coreCtrl.getLayerSet().getCurrLayerLevel())
    {
      DbgTools.print("Switch to new layer: " + newLayer);
      
      _coreCtrl.changeLayer(newLayer, null);
      _coreCtrl.getHUD().updateLayerInfo();
      _coreCtrl.getHUD().updateAvailableInstr();
    }
    
    if (instr != null)
    {
      DbgTools.print("Switch to new instrument: " + instr.getName());
      
      if (instr.getName() != null)
        _layerPickingCtrl.instrumentSelect(instr.getKey() - 1);
      else
        _coreCtrl.getGround().hideKeyboard();
    }
  }
  

  public boolean canHandleOSCMsg(String name)
  {
    return name.equals("/startBeatGraph") || name.equals("/resumeBeatGraph");
  }

  /**
   * @param OSCMessage is null when in Non-OSC-Mode
   */
  public void processOSCMsg(OSCMessage msg)
  {
    if (msg != null)
      DbgTools.print("Received OSC msg " + msg.getName());
    
    if (msg == null || msg.getName().equals("/startBeatGraph"))
    {
      long startMs;
      if (msg != null)
        startMs = MusicTools.beats2ms(Float.parseFloat(msg.getArg(0).toString()));
      else
        startMs = 1;
      
      DbgTools.print("Starting beat timing in: " + startMs);
      
      BeatTimingBehavior beatTiming = new BeatTimingBehavior((BeatGraphLine)_currGraphLine, startMs);
      ((BeatGraphLine)_currGraphLine).setBeatTiming(beatTiming);
      beatTiming.setSchedulingBounds(SceneCore.getSceneBounds());
      
      _currGraphLine.getBG().addChild(Common3DTools.wrapInBG(beatTiming));
    }
    else if (msg.getName().equals("/resumeBeatGraph"))
    {
      int beatGraphId = Integer.parseInt(msg.getArg(0).toString());
      long resumeMs = MusicTools.beats2ms(Float.parseFloat(msg.getArg(1).toString()));
      DbgTools.print("Resuming beat timing of beatgraph# " + beatGraphId + " in: " + resumeMs);
      
      ((BeatGraphLine)_coreCtrl.getCurrLayer().getGraph(beatGraphId).getGraphLine()).getBeatTiming().resumeIn(resumeMs);
    }
    
    synchronized(_notify)
    {
      _notify.notifyAll();
    }
  }

  public AbstractGraphLine getCurrGraphLine()
  {
    return _currGraphLine;
  }

  public int getCurrGraphId()
  {
    return _currentGraphId;
  }

  public SceneCoreOSC getCoreCtrl()
  {
    return _coreCtrl;
  }

  public void resetCurrValues()
  {
    _state = STATE_READY;
    _currentGraphId = - 1;
    _currGraphLine = null;
  }

  public Object getNotifyObj()
  {
    return _notify;
  }

  public void setCurrInstrument(Instrument instrument)
  {
    _currInstrument = instrument;
  }
  
  public Instrument getCurrInstrument()
  {
    return _currInstrument;
  }

  public MouseEvent getMouseEvent()
  {
    return mevent;
  }

  public AbstractGraph getCurrGraph()
  {
    if (_currGraphLine == null)
      return null;
    else
      return _currGraphLine.getGraph();
  }

  public void pauseBeatGraph(BeatGraph beatGraph)
  {
    ((BeatGraphLine)beatGraph.getGraphLine()).getBeatTiming().reinit();
    
    DbgTools.print("Sending pause");
    _coreCtrl.getSCLang().sendOSCData("/change", new String("pause;" + beatGraph.getId()) );
    
    ((BeatGraphLine)beatGraph.getGraphLine()).getBeatTiming().setEnable(false);
  }

  public void resumeBeatGraph(BeatGraph beatGraph)
  {
    DbgTools.print("Sending resume");
    _coreCtrl.getSCLang().sendOSCData("/change", new String("resume;" + beatGraph.getId()) );
  }

  public void changeToLayerType(int newType, Instrument instr)
  {
    if (newType == Layer.TYPE_SEQUENCER)
    {
      _layerPickingCtrl = _seqLayerPickingCtrl;
      _linePickingCtrl = _seqLinePickingCtrl;
      _synthObjPickingCtrl = _beatPickingCtrl;
    }
    else
    {
      _layerPickingCtrl = _synthLayerPickingCtrl;
      _linePickingCtrl = _synthLinePickingCtrl;
      _synthObjPickingCtrl = _synthPickingCtrl;
    }
    
    _layerPickingCtrl.instrumentSelect((instr == null) ? 0 : (instr.getKey() - 1));
  }

  public void setCurrGraphId(int id)
  {
    _currentGraphId = id;
  }
}
