package net.mkonrad.synes.gui.elements;

import java.awt.Font;
import java.util.ArrayList;

import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Geometry;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.QuadArray;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransparencyAttributes;
import javax.vecmath.Color3f;
import javax.vecmath.Point3f;
import javax.vecmath.Vector2f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import net.mkonrad.synes.data.AbstractGraph;
import net.mkonrad.synes.data.Beat;
import net.mkonrad.synes.data.Instrument;
import net.mkonrad.synes.data.Layer;
import net.mkonrad.synes.data.Modulator;
import net.mkonrad.synes.data.SynthGraph;
import net.mkonrad.synes.data.SynthObj;
import net.mkonrad.synes.gui.SceneCore;
import net.mkonrad.synes.gui.elements.instruments.InstrumentShape;
import net.mkonrad.synes.gui.style.Colors;
import net.mkonrad.synes.util.Common3DTools;
import net.mkonrad.synes.util.DbgTools;

public abstract class AbstractGraphLine extends Shape3D
{
  protected SceneCore _sceneCtrl;
  
  protected AbstractGraph _graph;
  
  protected BranchGroup _bg;
  protected TransformGroup _tg;
  protected BranchGroup _lineLabelsBG;
  protected ArrayList<TransformGroup> _lineLabelsTGs;
  
  protected ArrayList<Point3f> _coords;
  
  protected SynthObj _pointerObj;
  
  protected Point3f _lastMovePoint;
  
  protected float _currDistance = 0.0f;
  
  protected static float lineWidthRatio = 3f;
  
  public AbstractGraphLine(SceneCore sceneCtrl)
  {
    _sceneCtrl = sceneCtrl;
    
    _coords = new ArrayList<Point3f>();
    _lineLabelsTGs = new ArrayList<TransformGroup>();
    
    this.setPickable(true);
    this.setCapability(ALLOW_PICKABLE_WRITE);
    
    _bg = new BranchGroup();
    _bg.setCapability(BranchGroup.ALLOW_DETACH);
    _bg.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
    _bg.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
    
    _tg = new TransformGroup();
    _tg.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
    _tg.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
    _tg.setCapability(TransformGroup.ALLOW_CHILDREN_WRITE);
    
    _bg.addChild(_tg);
    
    _lineLabelsBG = new BranchGroup();
    _lineLabelsBG.setCapability(BranchGroup.ALLOW_DETACH);
    _lineLabelsBG.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
    _lineLabelsBG.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE);
  }

  public BranchGroup getBG()
  {
    return _bg;
  }
  
  public TransformGroup getTG()
  {
    return _tg;
  }
  
  public Vector3f getPos()
  {
    Vector3f posVec = new Vector3f();
    Transform3D t3d = new Transform3D();
    _tg.getTransform(t3d);
    t3d.get(posVec);
    
    return posVec;
  }
  
  public void setPos(Point3f posVec)
  {
    setPos(new Vector3f(posVec.x, posVec.y, 0f));
  }
  
  public void setPos(Vector3f posVec)
  {
    posVec.z = 0f;
    Transform3D t3d = new Transform3D();
    _tg.getTransform(t3d);
    t3d.setTranslation(posVec);
    _tg.setTransform(t3d);
  }
  
  public AbstractGraph getGraph()
  {
    return _graph;
  }
  
  public SynthObj getPointerObj()
  {
    return _pointerObj;
  }
  
  public void changePointerObj(SynthObj pointerObj)
  {
    _pointerObj = pointerObj;
    setPointerShape(pointerObj.getInstrumentShape());
  }
  
  public void changePointerInstrument(Instrument instr)
  {
    hidePointerShape();
    _pointerObj.setInstrument(instr);
    setPointerShape(instr.createInstrumentShape(_pointerObj, _lastMovePoint));
    updateGeometry();
  }
  
  private void setPointerShape(InstrumentShape pointerShape)
  {
    _pointerObj.setInstrumentShape(pointerShape);
    pointerShape.getPrimitive().setPickable(false);
    _tg.addChild(pointerShape.getBG());
  }
  
  public void hidePointerShape()
  {
    if (_pointerObj != null)
    {
      _pointerObj.getInstrumentShape().getBG().detach();
    }
  }
  
  public void setEnabled(boolean enabled)
  {
    setPickable(enabled);
    
    if (enabled)
      setTransparency(0.0f);
    else
      setTransparency(0.75f);
  }
  
  public void initGraph(AbstractGraph graph)
  {
    _graph = graph;
  }
    
  public void initLine(Point3f pointer, SynthObj pointerObj)
  {
    changePointerObj(pointerObj);
    
    createGeometry();
    
    pointer.add(new Vector3f(0.01f, 0f, 0f));
    addLinePoints(getSynthObj(0), new Vector3f(pointer));
    addLineLabel();
    updateLineLabel(0, getSynthObj(0).getPos(), new Vector3f(pointer), "");
    _tg.addChild(_lineLabelsBG);
  }

  public void releaseMove(Point3f pointer)
  {
    this.setPickable(true);
    
    _lineLabelsBG.removeAllChildren();
    _lineLabelsBG.detach();
    _lineLabelsTGs.clear();
    _currDistance = 0.0f;
    
    hidePointerShape();
    _pointerObj = null;
  }
  
  abstract public void movedPointer(Point3f pointer);
  abstract public void movedObj(SynthObj synthObj);
  abstract public void clicked(SynthObj synthObj);
  
  public void canceled()
  {
    if (_graph.size() <= 1)
    {
      _graph.clear();
    }
    else
    {
      cleanUpTail();
    }
  }
  
  public void updateGeometry()
  {
    setGeometry(_coords);
  }
  
  public void setGeometry(ArrayList<Point3f> coords)
  {
    int numPoints = coords.size();
    
    QuadArray linesAr = new QuadArray(numPoints, QuadArray.COORDINATES | QuadArray.COLOR_3);

    Point3f[] points = new Point3f[numPoints];
    coords.toArray(points);
    coords = null;
    linesAr.setCoordinates(0, points);
    
    Color3f lineColors[] = new Color3f[numPoints];
    int objIndex;
    for (int i = 0; i < numPoints; i++)
    {
      lineColors[i] = calcLineColor(i);
    }
    
    linesAr.setColors(0, lineColors);
    
    this.setGeometry(linesAr);
  }
  
  public void cleanUpTail()
  {
    int size = _coords.size();
    
    for (int i = 1; i <= 4; i++)
    {
      _coords.remove(size - i);
    }
    
    updateGeometry();
    
    _pointerObj.getInstrumentShape().getBG().detach();
    _pointerObj = null;
  }
  
  protected SynthObj getSynthObj(int index)
  {
    return ((SynthObj)_graph.get(0));
  }
  
  protected void addLinePoints(SynthObj synth1, Vector3f pointer)
  {
    Point3f[] points = calcLinePoints(synth1.getPos(), synth1.getAmp() / lineWidthRatio, pointer, 0.1f / lineWidthRatio);
    
    for (int i = 0; i < points.length; i++)
    {
      _coords.add(points[i]);
    }
    
    //DbgTools.print("now " + _coords.size() + " points");
  }
  
  protected void updateLinePoints(SynthObj synth1, Vector3f pointer)
  {
    int updateStart = -1;
    
    if (synth1 == null)
    {
      updateStart = _coords.size() - 4;
    }
    
    updateLinePoints(synth1, pointer, 0.1f, updateStart);
  }
  
  public void updateLinePoints(SynthObj synth1, SynthObj synth2)
  {
    int updateStart = -1;
    
    if (synth1 == null)
    {
      int synthIndex = _graph.indexOf(synth2);
      
      if (synthIndex < 0) //obj not found!
      {
        updateStart =  _coords.size() - 4;
        
        if (_sceneCtrl.getCurrLayer().getType() == Layer.TYPE_SYNTH
            && synth2 instanceof Modulator)
        {
          synth1 = (SynthObj)_graph.get(0);
        }
      }
      else
      {
        updateStart = synthIndex * 4 - 4;
      }
      
      if (updateStart < 0)
      {
        synth1 = synth2;
        synth2 = ((SynthGraph)_graph).getModulator();
        
        if (synth2 == null)
          synth2 = _pointerObj;
      }
    }
    
    updateLinePoints(synth1, synth2.getPos(), synth2.getAmp(), updateStart);
  }
  
  protected void updateLinePoints(SynthObj synth1, Vector3f synth2Pos, float synth2Amp, int updateStart)
  {
    Vector3f synth1Pos;
    float synth1Amp;
    
    if (synth1 == null)
    {
      synth1Pos = ((SynthGraph)_graph).getModulator().getPos();
      synth1Amp = ((SynthGraph)_graph).getModulator().getAmp();
    }
    else
    {
      synth1Pos = synth1.getPos();
      synth1Amp = synth1.getAmp();
    }
    
    Point3f[] points = calcLinePoints(synth1Pos, synth1Amp / lineWidthRatio, synth2Pos, synth2Amp / lineWidthRatio);
    
    if (updateStart < 0)
      updateStart = _graph.indexOf(synth1) * 4;
    
    //DbgTools.print("changing line coords " + updateStart + " to " + (updateStart + points.length));
    for (int i = 0; i < points.length; i++)
    {
      _coords.set(updateStart + i, points[i]);
    }
  }
  
  protected void updateLinePointsBackWay(SynthObj synth1, Vector3f synth2Pos, float synth2Amp)
  {
    Point3f[] points = calcLinePoints(synth1.getPos(), synth1.getAmp() / lineWidthRatio, synth2Pos, synth2Amp / lineWidthRatio);
    
    int updateStart = _graph.indexOf(synth1) * 4;
    //DbgTools.print("changing line coords " + updateStart + " to " + (updateStart + points.length));
    for (int i = 0; i < points.length; i++)
    {
      _coords.set(updateStart + i, points[i]);
    }
  }
  
  protected Point3f[] calcLinePoints(Vector3f p1, float scale1, Vector3f p2, float scale2)
  {
    Vector2f normal = new Vector2f (-(p2.y - p1.y), (p2.x - p1.x));
    normal.normalize();
    Vector2f normal2 = (Vector2f)normal.clone();
    normal.scale(scale1);
    normal2.scale(scale2);

    Point3f[] points = new Point3f[4];
    
    normal.negate();
    points[0] = getCornerPoint(p1, normal);   //bottom left
    normal.negate();
    points[1] = getCornerPoint(p1, normal);   //top left
    
    points[2] = getCornerPoint(p2, normal2);  //top right
    normal2.negate();
    points[3] = getCornerPoint(p2, normal2);  //bottom right

    
    return points;
  }
  
  protected Point3f getCornerPoint(Vector3f p, Vector2f normal)
  {
    Vector3f cornerP = new Vector3f();
    cornerP.add(p, new Vector3f(normal.x, normal.y, 0.05f));
    
    return new Point3f(cornerP.x, cornerP.y, 0.005f);
  }
  
  abstract protected Color3f calcLineColor(int objIndex); 
  
  protected float calcAngleBetweenSegments(Vector2f v1, Vector2f v2, Vector2f v3)
  {
    if (v1 == null)
    {
      return v3.angle(v2);
    }
    else
    {
      Vector2f vTmp = new Vector2f(); 
      vTmp.sub(v2, v1);
      
      v3.sub(v2);
      float rad = (float)Math.acos((v3.x * vTmp.x + v3.y * vTmp.y) / (v3.length() * vTmp.length()));
      rad -= (float)Math.PI / 2f;
      return rad;
    }
  }
  
  protected void addLineLabel()
  {
    TransformGroup tg = new TransformGroup();
    tg.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
    tg.setCapability(TransformGroup.ALLOW_CHILDREN_WRITE);
    tg.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
    tg.setPickable(false);
    
    _lineLabelsBG.addChild(Common3DTools.wrapInBG(tg, new int[] { BranchGroup.ALLOW_DETACH }));
    
    _lineLabelsTGs.add(tg);
  }
  
  protected void updateLineLabel(int index, Vector3f pos1, Vector3f pos2, String labelText)
  {
    TransformGroup tg = _lineLabelsTGs.get(index);
    try
    {
      tg.removeAllChildren();
    }
    catch (IndexOutOfBoundsException e)
    {
      //do nothing... just happens sometimes!
    }
    
    //Set position
    Transform3D t3d = new Transform3D();
    t3d.setTranslation(calcLineLabelPoint(pos1, pos2));
    tg.setTransform(t3d);
    
//    if (_sceneCtrl.getCurrLayer().getType() == Layer.TYPE_SEQUENCER)
//      labelInfo = ((Beat) _graph.get(_graph.size()-1)).getDur();
//    else
//      labelInfo = prevPoint.distance(thisPoint);
    
    
    Common3DTools.writeText(tg,
        labelText, 36, Colors.importantText, "SansSerif", Font.PLAIN);
    
    _lineLabelsTGs.set(index, tg);
  }
  
  private Vector3f calcLineLabelPoint(Vector3f prevPoint, Vector3f thisPoint)
  {
    Vector3f labelPoint = new Vector3f();
    
    labelPoint.sub(thisPoint, prevPoint);
    labelPoint.scale(0.5f);
    labelPoint.add(prevPoint);
    labelPoint.add(getPos());
    labelPoint.z = 0.15f; //raise it above the other things so it's allways visible
    
    return labelPoint;
  }
  
  protected float calcDistance(Vector3f pos1, Vector3f pos2)
  {
    return calcDistance(Common3DTools.v2p(pos1), pos2);
  }
  
  protected float calcDistance(Point3f pointer, Vector3f synthPos)
  {
    return pointer.distance(Common3DTools.v2p(synthPos));
  }
  
  private void createGeometry()
  {
    Appearance app = createAppearance();
    this.setAppearance(app);
    
    this.setCapability(this.ALLOW_GEOMETRY_READ);
    this.setCapability(this.ALLOW_GEOMETRY_WRITE);
    this.setCapability(this.ALLOW_PICKABLE_WRITE);
    
    this.setPickable(false);
    
    _tg.addChild(this);
  }
  
  private Appearance createAppearance()
  {
    Appearance app = new Appearance();
    app.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_WRITE);
    
    PolygonAttributes pa = new PolygonAttributes();
    pa.setCullFace(PolygonAttributes.CULL_NONE);   
    app.setPolygonAttributes(pa);
    
    return app;
  }
  
  private TransparencyAttributes createTransparencyAttrib(float t)
  {
    TransparencyAttributes trans = new TransparencyAttributes();
    trans.setTransparency(t);
    trans.setTransparencyMode(TransparencyAttributes.BLENDED);
    trans.setCapability(TransparencyAttributes.ALLOW_VALUE_WRITE);
    
    return trans;
  }
  
  public void setTransparency(float transValue)
  {
    TransparencyAttributes transAttrib = (transValue <= 0 ?
        new TransparencyAttributes() : createTransparencyAttrib(transValue));
    
    this.getAppearance().setTransparencyAttributes(transAttrib);
  }
}
