package net.mkonrad.synes.gui;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.Timer;

import javax.swing.JFrame;

import de.sciss.jcollider.Group;
import de.sciss.jcollider.JCollider;
import de.sciss.jcollider.NodeWatcher;
import de.sciss.jcollider.Server;
import de.sciss.jcollider.ServerEvent;
import de.sciss.jcollider.ServerListener;
import de.sciss.jcollider.ServerOptions;
import de.sciss.jcollider.UGenInfo;
import de.sciss.net.OSCMessage;
import de.sciss.net.OSCReceiver;
import de.sciss.net.OSCTransmitter;

import net.mkonrad.synes.data.Config;
import net.mkonrad.synes.data.IdCounter;
import net.mkonrad.synes.data.Layer;
import net.mkonrad.synes.osc.OSCMsgListener;
import net.mkonrad.synes.osc.OSCMsgProcessor;
import net.mkonrad.synes.osc.OSCMsgSender;
import net.mkonrad.synes.util.DbgTools;
import net.mkonrad.synes.util.ErrTools;
import net.mkonrad.synes.util.FileTools;

public class SceneCoreOSC extends SceneCore
  implements OSCMsgProcessor, ServerListener
{
  private OSCReceiver _oscRecv;
  private OSCMsgListener _oscMsgListener;
  //private OSCMsgSender _oscServerTransmitter;
  private OSCMsgSender _oscLangTransmitter;
  
  private Server _scServer;
  private NodeWatcher _scNodeWatcher;
  private Group _scStdGrp;
  
  private Thread _scOutputDisplayThread;
  private Process _scLangProcess;
  
  private int _oscLangPort;   //The port of SCLang
  private int _oscServerPort; //The port of SCSynth
  private int _oscSendingPort;
  private int _oscListeningPort;
  
  private String _scProgramDir; /** Directory where SuperCollider resides */
  
  private boolean _initCompleted = false;
  private boolean _serverInitialized = false;
  
  /**
   * Main class for the program with OSC-extensions
   * 
   * @param parentWindow
   * @param conf
   * @param oscModeOn
   */
  public SceneCoreOSC(JFrame parentWindow, Config conf, boolean oscModeOn)
  {
    super(parentWindow, conf);
    
    OSC_MODE = oscModeOn;
    
    initContents();
    
    if (!OSC_MODE)
    {
      DbgTools.print("Started in NO-SC mode");
    }
    else
    {
      _oscServerPort = Integer.parseInt(conf.getCfgValue("osc_server_port"));
      _oscLangPort = Integer.parseInt(conf.getCfgValue("osc_lang_port"));
      _oscSendingPort = Integer.parseInt(conf.getCfgValue("osc_sending_port"));
      _oscListeningPort = Integer.parseInt(conf.getCfgValue("osc_listening_port"));
      
      _scProgramDir = conf.getCfgValue("sc_program_dir"); 
      
      if (!JCollider.isWindows) //there is no sclang on windows... we need a workaround!
      {
        copySCClasses();
        startSCScript(findSCProg("sclang"));
      }
      
      startSCServer(findSCProg("scsynth"));
      
      initOSCLevel1();
      registerOSCMsgProcessors();
      
      DbgTools.print("Started in OSC-mode");
    }
  }
  
  
  private void copySCClasses()
  {
    String homeDir = System.getProperty("user.home");
    String extDir;
    
    if (JCollider.isMacOS)
    {
      extDir = homeDir + "/Library/Application Support/SuperCollider/Extensions";
    }
    else
    {
      extDir = homeDir + "/share/SuperCollider/Extensions";
    }
    
    File extDirObj = new File(extDir);
    
    if (!extDirObj.exists())
    {
      if (!extDirObj.mkdirs())
      {
        ErrTools.print("Could not create SC extension directory: " + extDir);
      }
      else
      {
        DbgTools.print("Created SC extensions dir: " + extDir);
      }
    }
    
    Runtime rtime = Runtime.getRuntime();
    String wd = System.getProperty("user.dir");
    String[] cmd = new String[4];
    cmd[0] = "cp";
    cmd[1] = "-r";
    cmd[2] = wd + "/SuperCollider/classes";
    cmd[3] = extDir;
    
    try
    {
      //Process proc = rtime.exec("/bin/cp -r " + wd + "/SuperCollider/classes " + extDir + "");
      Process proc = rtime.exec(cmd);
      //int exitVal = proc.waitFor();
      //DbgTools.print("cp -r '" + wd + "/SuperCollider/classes' '" + extDir + "': " + exitVal);
    } catch (Exception e)
    {
      ErrTools.print(e);
    }
  }
  
  /**
   * Partly taken from the JCollider demo
   * 
   * @author Hans Holger Rutz
   */
  private String findSCProg(String prog)
  {
    final String fs = File.separator;
    
    File f = FileTools.findFile(
        JCollider.isWindows ? prog + ".exe" : prog, new String[] {
          fs + _scProgramDir,
          fs + "Applications" + fs + "SuperCollider",
          fs + "Applications" + fs + "SC3",
          fs + "usr" + fs + "local" + fs + "bin",
          fs + "usr" + fs + "bin",
          "C:\\Program Files\\SC3",
          "C:\\Program Files (x86)\\SuperCollider"
    });
    
    if (f == null)
      ErrTools.die("Could not find SC program " + prog);
    
    return f.getAbsolutePath();
  }
  
  /**
   * 
   */
  private void startSCScript(String clientProgram)
  {
    DbgTools.print("Client resides at " + clientProgram);
    
    Runtime rtime = Runtime.getRuntime();
    try
    {
      String progDir = new File(clientProgram).getParent();
      String workingDir = System.getProperty("user.dir");
      _scLangProcess = rtime.exec(clientProgram + " -d" +
          (JCollider.isMacOS ? progDir : System.getProperty("user.dir")) +
          " -D " + workingDir + "/SuperCollider/run.sc");

      Runnable startup = new Runnable()
      {
        public void run()
        {
          BufferedReader childOutput = new BufferedReader(
              new InputStreamReader(_scLangProcess.getInputStream()));
          
          String outStr;
          try
          {
            while ((outStr = childOutput.readLine()) != null)
            {
              DbgTools.print(outStr, "SC: ");
              System.out.flush();
            }
          } catch (IOException e)
          {
            //ErrTools.print(e);
          }
        }
      };
      
      _scOutputDisplayThread = new Thread(startup);
      _scOutputDisplayThread.start();
    } catch (Exception e)
    {
      ErrTools.die(e);
    }
  }

  public OSCMsgListener getOSCMsgListener()
  {
    return _oscMsgListener;
  }
  
  /**
   * Setup for receiving OSC messages
   */
  private void initOSCLevel1()
  {
    try
    {      
      DatagramChannel udpChannelRecv = DatagramChannel.open();
      udpChannelRecv.socket().bind(new InetSocketAddress("127.0.0.1", _oscListeningPort));
      
      _oscRecv = OSCReceiver.newUsing(udpChannelRecv);
      
      _oscMsgListener = new OSCMsgListener(this, true);
      _oscRecv.addOSCListener(_oscMsgListener);
      _oscRecv.startListening();

      DbgTools.print("*Receiving on port " + _oscListeningPort);
    }
    catch(Exception e)
    {
      ErrTools.print(e);
    }
  }
  
  /**
   * Setup for sending OSC messages (we need to know the port of the OSC lang
   * app (SCLang) first!)
   */
  private void initOSCLevel2()
  {
    try
    {
      _oscLangTransmitter = new OSCMsgSender(_oscLangPort, _oscSendingPort);
      
      DbgTools.print("*Sending from port " + (_oscSendingPort+1) + " to OSC server application on port " + _oscServerPort);
      DbgTools.print("*Sending from port " + _oscSendingPort + " to OSC lang application on port " + _oscLangPort);
      
      //initServer();
    }
    catch(Exception e)
    {
      ErrTools.print(e);
    }
    
    //_initCompleted = true;
  }
  
  /**
   * Start the SuperCollider Server scsynth or connect to running server
   */
  private void startSCServer(String serverProgram)
  {
    DbgTools.print("Server resides at " + serverProgram);
    try
    {
      UGenInfo.readBinaryDefinitions();
      _scServer = new Server("synesServerJava",
          new InetSocketAddress("localhost", 57110),
          new ServerOptions(),
          2
      );
      Server.setProgram(serverProgram);
      _scServer.addListener( this );
      
      _scServer.start();
      _scServer.startAliveThread();
    }
    catch(IOException e)
    {
      ErrTools.print(e);
    }
  }
  
  private void initServer() throws IOException
  {
    if (_serverInitialized)
      return;
    
    _scServer.initTree();
    
    _scNodeWatcher = NodeWatcher.newFrom(_scServer);
    _scStdGrp = _scServer.getDefaultGroup();
    _scNodeWatcher.register(_scStdGrp);

    DbgTools.print("Initialized server. Standard group has NodeID " + _scStdGrp.getNodeID());
    
    _serverInitialized = true;
  }
  
  public void serverAction(ServerEvent e)
  {
    switch (e.getID())
    {
    case ServerEvent.RUNNING:
      try
      {
        initServer();
      } catch (IOException e1)
      {
        ErrTools.print(e1);
      }
      break;

    case ServerEvent.STOPPED:
      // have to call startAliveThread to keep watching for server starts
      final javax.swing.Timer t = new javax.swing.Timer(1000,
          new ActionListener()
          {
            public void actionPerformed(ActionEvent e)
            {
              try
              {
                if (_scServer != null)
                  _scServer.startAliveThread();
              } catch (IOException e1)
              {
                ErrTools.print(e1);
              }
            }
          });
      t.setRepeats(false);
      t.start();
      break;

    default:
      break;
    }
  }
  
  private void registerOSCMsgProcessors()
  {
    _oscMsgListener.addProcessor(this);
    _oscMsgListener.addProcessor(this.getPickingCtrl());
  }
 
  public boolean canHandleOSCMsg(String name)
  {
    return (name.equals("/init") || name.equals("/initComplete"));
  }

  public void processOSCMsg(OSCMessage msg)
  {
    if (_initCompleted)
      return;
    
    DbgTools.print("Received OSC message: " + msg.getName());
    
    if (msg.getName().equals("/init"))
    {
      _oscServerPort = Integer.parseInt(msg.getArg(0).toString());
      _oscLangPort = Integer.parseInt(msg.getArg(1).toString());
      initOSCLevel2();
    }
    else if (msg.getName().equals("/initComplete"))
    {
      try
      {
        initServer();
        getCurrLayer().setSCGroup(Group.tail(getSCStdGrp()));
        DbgTools.print("Created group " + getCurrLayer().getSCGroup().getNodeID());
      } catch (IOException e)
      {
        ErrTools.print(e);
      }
      
      _hud.hideLoadingMsg();
      showContents();
      _initCompleted = true;
    }
  }

  public void shutdown()
  {
    if (!OSC_MODE)
      return;
    
    try
    {
      DbgTools.print("Sending /quit to client...");
      getSCLang().sendOSCData("/quit");
      
      //if (_scServer.didWeBootTheServer())
      //{
        DbgTools.print("Sending /quit to server...");
        _scServer.quitAndWait();
      //}
      
      if (_scLangProcess != null)
      {
        DbgTools.print("Terminating sclang...");
        _scLangProcess.destroy();
        _scOutputDisplayThread.join(500);
      }
    } catch (Exception e)
    {
      ErrTools.print(e);
    }
  }
  
  public Server getSCServer()
  {
    return _scServer;
  }
  
  public OSCMsgSender getSCLang()
  {
    return _oscLangTransmitter;
  }

  public Group getSCStdGrp()
  {
    return _scStdGrp;
  }
}
