package com.videonext.mplayer.applet;

/*
  #  $Id:$
  # -----------------------------------------------------------------------------
  #  The part of 'MediaPlayer' project
  # -----------------------------------------------------------------------------
  #  Author: Petrov Maxim, 19/02/2010
  #  Edited by:
  #  QA by:
  #  Copyright: videoNEXT LLC
  # -----------------------------------------------------------------------------
*/
import java.applet.*;
import java.awt.Container;
import java.awt.FileDialog;
import java.awt.Frame;
import java.net.*;
import java.io.*;
import java.util.*;
import javax.sound.sampled.*;
import java.util.concurrent.SynchronousQueue;

import netscape.javascript.*;

public class AudioRecorder extends Applet {
   private Vector<Mixer> mixers = null;
   private AudioFormat audioFormat = null;
   private DataLine.Info dataLineInfo = null;
   private TargetDataLine targetDataLine = null;
   private int currentMixer = -1;
   private boolean isRecording = false;
   private boolean isConfigured = false;
   private OutputStream recordOutputStream = null;
   private Timer cancelRecordingTimer = null;

   private SynchronousQueue<String[]> javaScriptEventQueue = null;
   private JavaScriptEventThread javaScriptEventThread = null;  

   private JSObject jsObject = null;

   private static int JSCALL_AUDIORECORDER_ERROR    = 0;
   private static int JSCALL_AUDIORECORDER_READY    = 1;
   private static int JSCALL_AUDIORECORDER_TIMEOUT  = 2;
   private String jsCallbacks[] = new String[3];


   public void init()
   {  
      mixers = new Vector<Mixer>();
      audioFormat = getAudioFormat();
      dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);

      Mixer.Info[] allMixersInfo = AudioSystem.getMixerInfo();

      // Test data lines for compatability with us audio format
      for (int i = 0; i < allMixersInfo.length; i++)
      {
         try
         {
            Mixer mixer = AudioSystem.getMixer(allMixersInfo[i]);      

            targetDataLine = (TargetDataLine)mixer.getLine(dataLineInfo);
            targetDataLine = null;   
            
            mixers.add(mixer);
         }
         catch (Exception ex) 
         {
            System.out.println("Not compatible mixer:" + allMixersInfo[i].getName() + "(" + ex + ")");
         }
      }
//      mixers = AudioSystem.getMixerInfo();

   }

   public void start()
   {
      if (javaScriptEventThread == null)
      {
         jsObject = JSObject.getWindow(this);

         // Get JavaScript callback names 
         jsCallbacks[JSCALL_AUDIORECORDER_ERROR]    = getParameter("onAudioRecorderError");
         jsCallbacks[JSCALL_AUDIORECORDER_READY]    = getParameter("onAudioRecorderReady");
         jsCallbacks[JSCALL_AUDIORECORDER_TIMEOUT]  = getParameter("onAudioRecorderTimeout");

         javaScriptEventQueue = new SynchronousQueue<String[]>();
      
         javaScriptEventThread = new JavaScriptEventThread();
         javaScriptEventThread.start();
      }
   }
   
   public void stop()
   {
      stopRecording(); 
      if (javaScriptEventThread != null) javaScriptEventThread.done();
   }

   // Get list of all available mixers in format: "Device Name1|Device Name2|...."
   public String getAllInputDevices()
   {
      String result = new String();

      System.out.println("Available mixers: ");

      for(int i = 0; i < mixers.size(); i++)
      {
         String name = mixers.elementAt(i).getMixerInfo().getName();

         System.out.println(name);

         result += name + "|";        

//          try
//          {
//            
//             //   str += mixers[cnt].getName() + "|";        
//             str += new String(mixers[cnt].getName().getBytes("UTF8")) + "|";
//          }
//          catch (UnsupportedEncodingException ex) {}
      
        
      } 

      return result;
   }

   public void setInputDevice(int index)
   {
      currentMixer = index;

      System.out.println("Current mixer: " +  mixers.elementAt(currentMixer).getMixerInfo().getName() );
   }

   public void startRecording(String url, int timeout)
   {
      if (isRecording) return;

      String params[] = {url, Integer.toString(timeout)};
        
      try 
      {
         javaScriptEventQueue.put(params);
         isRecording = true;
      }  
      catch (Exception ex) 
      {	
         System.out.println(ex);
      }
   }

   private void callJSFunction(int name, String params)
   {      	  
      String callbackName = jsCallbacks[name];
      if (callbackName != null)
      {
         try 
         {
            jsObject.eval(callbackName + "(" + params + ")");
         }
         catch (Exception ex) 
         {
            System.out.println("Caught " + ex.getClass().getName() + ", msg=" + ex.getMessage() + " in " + callbackName + " callback");
         }
      }
   }

   private void setupRecording(String urlString) throws Exception
   {
      try 
      {
         String host  = getHostFromURL(urlString); 
         int    port  = getPortFromURL(urlString, 8554); 

         Socket socket = new Socket(host, port);
      
         BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream( )));
         recordOutputStream = socket.getOutputStream();

         System.out.println("Connected to " + host + ":" + port);

         String req = "XRECORD " + urlString + " RTSP/1.0\r\nCSeq: 1\r\n\r\n";
         
         System.out.println("Request:");
         System.out.println(req);

         recordOutputStream.write(req.getBytes());

         String resp  = in.readLine();
         while (true) 
         {       
            String line =  in.readLine();
            if (line == null || line.length() == 0)
               break;

//            resp += line + "\r\n";
         }

         System.out.println("Response:");
         System.out.println(resp);

         if (!resp.contains("RTSP/1.0 200"))
         {
            socket.close();
            throw new Exception("Could not connect to receiver: "  + resp);
         }
      }
      catch (Exception ex) 
      {
         throw (ex);
      }
   }
     
   private void startRecordingThread(String url, int timeout)
   {
      if (!isConfigured()) return;
    
      System.out.println("Starting recording from \"" + mixers.elementAt(currentMixer).getMixerInfo().getName() + "\" for " + url);

      try 
      {
         setupRecording(url);
         
         // Get a TargetDataLine on the selected mixer.
         targetDataLine = (TargetDataLine)mixers.elementAt(currentMixer).getLine(dataLineInfo);
         // Prepare the line for use.
         targetDataLine.open(audioFormat);
         targetDataLine.start();

         // Create a thread to capture the microphone
         // data and start it running.  It will run
         // until the stopRecording() will be called
         Thread recordThread = new RecordThread(timeout);
         recordThread.start();
      } 
      catch (Exception ex) 
      {
         isRecording = false;

         System.out.println(ex);

         callJSFunction(JSCALL_AUDIORECORDER_ERROR, "\"" + ex.getMessage() + "\"");
      }
   }

 
   public void stopRecording()
   {
      isRecording = false;
   }

   public boolean isRecording()
   {
      return isRecording;
   }

   public boolean isConfigured()
   {
      return currentMixer != -1; 
   }
	
   private AudioFormat getAudioFormat() 
   {
      float sampleRate = 8000.0F;
      int sampleSizeInBits = 16;
      int channels = 1;
      boolean signed = true;
      boolean bigEndian = false;
      
      return new AudioFormat(
         sampleRate,
         sampleSizeInBits,
         channels,
         signed,
         bigEndian);


   }

   /** 
       To prevent Security exception from js code
       Taken from http://forum.java.sun.com/thread.jspa?forumID=63&threadID=524815
   */
   class JavaScriptEventThread extends Thread 
   {
      public void run() 
      {
         try
         {
            // HACK: give chance to leave "start()" method before call any JS function
            Thread.sleep(3000);

         }
         catch(Exception e) {}

         callJSFunction(JSCALL_AUDIORECORDER_READY, "");

         while (true) 
         {
            try 
            {
               String[] params = new String[2];
               params = javaScriptEventQueue.take();
               if (params[0].compareTo("done") == 0) 
               {
                  break;
               }
               else 
               {
                  try 
                  {
                     startRecordingThread(params[0], Integer.parseInt(params[1]));
                  } 
                  catch (Exception ex) 
                  {
                     ex.printStackTrace();						
                  }         
               }
            } 
            catch (InterruptedException ex) {}
         }
      }
     
      public void done()
      {
         try 
         {
            String params[] = {"done"};
            javaScriptEventQueue.put(params);
         } 
         catch (InterruptedException e) {}        	
      }
   }
  
   class CancelRecordingTask extends TimerTask 
   {
      /**
       * Implements TimerTask's abstract run method.
       */
      public void run()
      {
         isRecording = false;
         System.out.println("Recording canceled by timeout...");
         callJSFunction(JSCALL_AUDIORECORDER_TIMEOUT, "");
      }
   }

   class RecordThread extends Thread
   {
      byte buffer[] = new byte[480];
      int timeout;

      public RecordThread(int timeout)
      {
         super("RecordThread");
         setDaemon(true);
         this.timeout = timeout;
      }

      public void run()
      {
         try
         {
             cancelRecordingTimer = new Timer();
             cancelRecordingTimer.schedule(new CancelRecordingTask(), new Date(new Date().getTime() + timeout * 1000)); 
             AudioInputStream inputPCM = new AudioInputStream(targetDataLine);
             AudioInputStream inputULAW = AudioSystem.getAudioInputStream(AudioFormat.Encoding.ULAW, inputPCM);

            while (isRecording) 
            {
               int cnt = inputULAW.read(buffer,
                                        0,
                                        buffer.length);
               
//               System.out.println("Read from line: " + cnt + " bytes");
               
               if (cnt > 0)
               {
                  String hdr = "Content-Type: audio/PCMU\r\n"
                     + "Content-Length: " + cnt + "\r\n\r\n";
                  recordOutputStream.write(hdr.getBytes());
                  recordOutputStream.write(buffer);
               }
            }
         }
         catch (Exception ex) 
         {
            isRecording = false;
            System.out.println(ex);

            callJSFunction(JSCALL_AUDIORECORDER_ERROR, ex.getMessage());
         }

         System.out.println("Done recording");

         if (cancelRecordingTimer != null) cancelRecordingTimer.cancel();
         if (targetDataLine != null) targetDataLine.close();
      }
   }

   private int getPortFromURL(String url, int defaultPort) 
   {
      // Trim schema and path
      String hostStr = null;
      int sepIdx = url.indexOf("//");
      if (sepIdx < 0) return defaultPort;
      hostStr = url.substring(sepIdx + 2);
      sepIdx = hostStr.indexOf("/");
      if (sepIdx < 0) return defaultPort;
      hostStr = hostStr.substring(0, sepIdx);
    
      sepIdx = hostStr.indexOf(':');
      if (sepIdx < 0) 
      {
          return defaultPort;
      } 
      else 
      {
          String portStr = hostStr.substring(sepIdx + 1).trim();
          try 
          {
             return Integer.parseInt(portStr);
          } 
          catch (NumberFormatException e) 
          {
             return defaultPort;
          }
      }
   }

   private String getHostFromURL(String url) 
   {
      // Trim schema and path
      String hostStr = null;
      int sepIdx = url.indexOf("//");
      if (sepIdx < 0) return "";
      hostStr = url.substring(sepIdx + 2);
      sepIdx = hostStr.indexOf("/");
      if (sepIdx < 0) return "";
      hostStr = hostStr.substring(0, sepIdx);
    

      sepIdx =  hostStr.indexOf(':');
      if (sepIdx < 0) 
      {
         return hostStr;
      } 
      else 
      {
         return hostStr.substring(0, sepIdx);
      }
   }
	 
}
