/*
 *  Copyright (C) 2006  Takashi Kasuya <kasuya@sfc.keio.ac.jp>
 *
 * This library is free software; you can redistribute it and/or
 *　modify it under the terms of the GNU Lesser General Public
 *　License as published by the Free Software Foundation; either
 *　version 2.1 of the License, or (at your option) any later version.
 *　This library is distributed in the hope that it will be useful,
 *　but WITHOUT ANY WARRANTY; without even the implied warranty of
 *　MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *　Lesser General Public License for more details.
 *
 *　You should have received a copy of the GNU Lesser General Public
 *　License along with this library; if not, write to the Free Software
 *　Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package jp.ac.naka.ec.media;

import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;

import javax.media.CaptureDeviceInfo;
import javax.media.CaptureDeviceManager;
import javax.media.DataSink;
import javax.media.Format;
import javax.media.Manager;
import javax.media.MediaException;
import javax.media.MediaLocator;
import javax.media.NoDataSourceException;
import javax.media.Processor;
import javax.media.control.FormatControl;
import javax.media.control.TrackControl;
import javax.media.format.AudioFormat;
import javax.media.format.VideoFormat;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.DataSource;
import javax.sdp.Connection;
import javax.sdp.Media;
import javax.sdp.MediaDescription;
import javax.sdp.SdpConstants;
import javax.sdp.SdpException;
import javax.sdp.SdpParseException;
import javax.sdp.SessionDescription;
import javax.swing.JButton;
import javax.swing.JFrame;

/**
 * RTPによる動画送信のためのモジュール。
 * 
 * @author 粕谷
 * 
 */
public class MediaTransmitter {

	protected int[] receivableJmfVideoFormats = new int[] { SdpConstants.H263, // javax.media.format.VideoFormat.H263_RTP
			SdpConstants.JPEG, // javax.media.format.VideoFormat.JPEG_RTP
			SdpConstants.H261, // javax.media.format.VideoFormat.H261_RTP
	};

	protected int[] receivableJmfAudioFormats = new int[] {
	// sdp format // corresponding JMF Format
			SdpConstants.G723, // javax.media.format.AudioFormat.G723_RTP
			SdpConstants.GSM, // javax.media.format.AudioFormat.GSM_RTP;
			SdpConstants.PCMU, // javax.media.format.AudioFormat.ULAW_RTP;
			SdpConstants.DVI4_8000, // javax.media.format.AudioFormat.DVI_RTP;
			SdpConstants.DVI4_16000, // javax.media.format.AudioFormat.DVI_RTP;
			SdpConstants.PCMA, // javax.media.format.AudioFormat.ALAW;
			SdpConstants.G728 // , //

	};

	List<Integer> availableVideoFormats = new ArrayList<Integer>();
	List<Integer> availableAudioFormats = new ArrayList<Integer>();

	private Processor audio_processor;
	private Processor video_processor;

	private DataSink audio_ds;
	private DataSink video_ds;

	private static boolean debugDeviceList = true;
	public static String defaultVideoDeviceName = "vfw:Microsoft WDM Image Capture (Win32):0";
	public static String defaultAudioDeviceName = "DirectSoundCapture";
	public static String defaultVideoFormatString = "size=352x288, encoding=yuv, maxdatalength=152064";
	public static String defaultAudioFormatString = "linear, 8000.0 hz, 8-bit, mono, unsigned";
	private static CaptureDeviceInfo captureVideoDevice = null;
	private static CaptureDeviceInfo captureAudioDevice = null;
	private VideoFormat captureVideoFormat = null;
	private AudioFormat captureAudioFormat = null;
	private boolean enable_audio = true;

	private WaitingListener listener = new WaitingListener();

	/**
	 * 
	 * 
	 */
	public MediaTransmitter() {
		Arrays.sort(receivableJmfVideoFormats);

		for (int format : receivableJmfVideoFormats) {
			availableVideoFormats.add(format);
		}

		Arrays.sort(receivableJmfAudioFormats);
		for (int format : receivableJmfAudioFormats) {
			availableAudioFormats.add(format);
		}
	}

	/**
	 * 音声も送るか？
	 * 
	 * @param a
	 */
	public void enableAudio(boolean a) {
		this.enable_audio = a;
	}

	/**
	 * 相手先ホストを指定してメディア送信。ポートは固定。
	 * 
	 * @param host
	 * @throws MediaException
	 * @throws IOException
	 * @throws SdpException
	 */
	public void sendMedia(String host, int audio_port, int video_port)
			throws MediaException, IOException, SdpException {
		List<Integer> audio = null;
		if (enable_audio) {
			audio = availableAudioFormats;
		}
		SessionDescription sdp = SDPGenerator.getRequestSessionDescription(0,
				"media_sender", "-", host, audio, audio_port,
				availableVideoFormats, video_port);
		sendMedia(sdp);
	}

	SessionInformation[] info;

	/**
	 * SDPで示されたセッション先に対してストリーミングを開始。
	 * 
	 * @param sdp
	 * @throws MediaException
	 * @throws IOException
	 * @throws SdpException
	 */
	public void sendMedia(SessionDescription sdp) throws MediaException,
			IOException, SdpException {
		Connection c = sdp.getConnection();
		InetAddress addr = InetAddress.getByName(c.getAddress());
		Vector<MediaDescription> mds = sdp.getMediaDescriptions(true);
		info = new SessionInformation[mds.size()];
		int i = 0;
		for (MediaDescription md : mds) {
			Media media = md.getMedia();
			Vector formats = media.getMediaFormats(true);
			// 一番目のフォーマットを使う
			int payload = Integer.parseInt((String) formats.get(0));
			int port = media.getMediaPort();
			info[i] = new SessionInformation(addr.getHostAddress(), port);
			Format format = SDPGenerator.findCorrespondingFormat(payload);
			if (format == null) {
				// format = SDPGenerator.findCorrespondingFormat(payload);
				continue;
			}
			info[i].format = format;
			i++;
		}
		initialize(info);
		play();
	}

	/**
	 * 
	 * @param info
	 * @throws MediaException
	 * @throws IOException
	 */
	private void initialize(SessionInformation[] info) throws MediaException,
			IOException {
		setUpCaptureDevice();
		for (int i = 0; i < info.length; i++) {
			setUpDataSource(info[i]);
			setUpProcessor(info[i]);
		}
	}

	/**
	 * 
	 * @param info
	 * @throws IOException
	 * @throws MediaException
	 */
	private void setUpProcessor(SessionInformation info) throws IOException,
			MediaException {
		String media = info.getMediaType();
		Processor p = null;

		if (!DeviceInfo.setFormat(info.ds, info.capture_format)
				&& info.capture_format != null) {
			throw new MediaException("Invalid format : "
					+ info.capture_format.toString());
		}
		p = Manager.createProcessor(info.ds);
		if (media.equals("video"))
			video_processor = p;
		else
			audio_processor = p;
		p.addControllerListener(listener);
		p.configure();
		synchronized (listener) {
			listener.waitForEvent(Processor.Configured);
		}
		p.setContentDescriptor(new ContentDescriptor(ContentDescriptor.RAW));
		TrackControl track[] = p.getTrackControls();
		boolean encodingOk = false;

		for (int i = 0; i < track.length; i++) {
			if (!encodingOk && track[i] instanceof FormatControl) {
				FormatControl control = (FormatControl) track[i];
				if (control.setFormat(info.format) == null)
					track[i].setEnabled(false);
				else
					encodingOk = true;
			} else {
				track[i].setEnabled(false);
			}
		}

		if (encodingOk) {
			p.realize();
			synchronized (listener) {
				listener.waitForEvent(Processor.Realized);
			}
			DataSource ds = null;
			ds = p.getDataOutput();

			try {
				MediaLocator ml = new MediaLocator("rtp://" + info.addr + ":"
						+ info.port + "/" + media + "/" + info.ttl);
				// System.err.println(ml.toString());
				DataSink d = Manager.createDataSink(ds, ml);
				if (media.equals("video")) {
					video_ds = d;
				} else {
					audio_ds = d;
				}
			} catch (Exception e) {
				throw new MediaException(e.getMessage());
			}
		}
	}

	/**
	 * データベーステーブルの作成
	 * 
	 * @param info
	 * @throws NoDataSourceException
	 * @throws IOException
	 */
	private void setUpDataSource(SessionInformation info)
			throws NoDataSourceException, IOException {
		String media = (info.format instanceof VideoFormat) ? "video" : "audio";
		DataSource ds;
		if (media.equals("video") && captureVideoDevice != null) {
			ds = Manager.createDataSource(captureVideoDevice.getLocator());
			// video_dsource = ds;
			info.ds = ds;
			info.device = captureVideoDevice;
			if (captureVideoFormat != null)
				info.capture_format = captureVideoFormat;
		} else if (media.equals("audio") && captureAudioDevice != null) {
			ds = Manager.createDataSource(captureAudioDevice.getLocator());
			// audio_dsource = ds;
			info.ds = ds;
			info.device = captureAudioDevice;
			if (captureAudioFormat != null)
				info.capture_format = captureAudioFormat;
		} else {
			throw new NullPointerException("Processor is null");
		}
	}

	/**
	 * 
	 * @throws MediaException
	 */
	private void setUpCaptureDevice() throws MediaException {
		Vector deviceListVector = CaptureDeviceManager.getDeviceList(null);
		if (deviceListVector == null || deviceListVector.size() == 0) {
			throw new MediaException("There is no available capture device.");
		}

		for (int i = 0; i < deviceListVector.size(); i++) {
			// display device name
			CaptureDeviceInfo deviceInfo = (CaptureDeviceInfo) deviceListVector
					.elementAt(i);
			String deviceInfoText = deviceInfo.getName();

			if (debugDeviceList)
				System.err.println("device " + i + ": " + deviceInfoText);

			// display device formats
			Format deviceFormat[] = deviceInfo.getFormats();
			for (int j = 0; j < deviceFormat.length; j++) {
				Format format = deviceFormat[j];
				// serach for default video device
				if (captureVideoDevice == null && format instanceof VideoFormat)
					// ビデオキャプチャーデバイス未指定の場合も含める
					if (deviceInfo.getName().indexOf(defaultVideoDeviceName) >= 0
							|| defaultVideoDeviceName.equals("")) {
						captureVideoDevice = deviceInfo;
						System.err.println(">>> capture video device = "
								+ deviceInfo.getName());

					}
				if (captureVideoDevice == deviceInfo
						&& captureVideoFormat == null) {
					// フォーマットのマッチング
					if (DeviceInfo.formatToString(format).indexOf(
							defaultVideoFormatString) >= 0) {
						captureVideoFormat = (VideoFormat) format;
						System.err.println(">>> capture video format = "
								+ DeviceInfo.formatToString(deviceFormat[j]));
					}
				}

				// serach for default audio device
				if (captureAudioDevice == null && format instanceof AudioFormat)
					if (deviceInfo.getName().indexOf(defaultAudioDeviceName) >= 0
							|| defaultAudioDeviceName.equals("")) {
						captureAudioDevice = deviceInfo;
						System.err.println(">>> capture audio device = "
								+ deviceInfo.getName());
					}

				// search for default audio format
				if (captureAudioDevice == deviceInfo
						&& captureAudioFormat == null)
					if (DeviceInfo.formatToString(format).indexOf(
							defaultAudioFormatString) >= 0) {
						captureAudioFormat = (AudioFormat) format;
						System.err.println(">>> capture audio format = "
								+ DeviceInfo.formatToString(deviceFormat[j]));
						if (debugDeviceList)
							System.err.println(" - format: "
									+ DeviceInfo.formatToString(format));
					}
			}
		}
	}

	/*
	 * public Component getVisualComponent() { if (video_processor != null) {
	 * return video_processor.getVisualComponent(); } return null; }
	 */

	/**
	 * 動画のコントロールを取得。動画のキャプチャデバイスが設定されていないときは音声のコントロールが取得できる。
	 * 
	 * @return
	 */
	public Component getControlPanelComponent() {
		if (video_processor != null) {
			return video_processor.getControlPanelComponent();
		} else if (audio_processor != null) {
			return audio_processor.getControlPanelComponent();
		}
		return null;
	}

	/**
	 * 再生中かどうか？
	 * 
	 * @return
	 */
	public boolean isPlaying() {
		if (video_processor != null) {
			return (video_processor.getState() == Processor.Started) ? true
					: false;
		} else if (audio_processor != null) {
			return (audio_processor.getState() == Processor.Started) ? true
					: false;
		}
		return false;
	}

	/**
	 * 未実装
	 * 
	 * @param sdp
	 */
	public void setNewSessionDescription(SessionDescription sdp) {
		// TODO
	}

	/**
	 * メディアのストリーミングを開始。
	 * 
	 * @throws IOException
	 */
	public void play() throws IOException {
		if (video_ds != null) {
			video_ds.open();
			video_ds.start();
			video_processor.start();
			System.out.println("Start Video Transmitting...");
		}
		if (audio_ds != null) {
			audio_ds.open();
			audio_ds.start();
			audio_processor.start();
			System.out.println("Start Audio Transmitting...");
		}
	}

	/**
	 * 
	 * @throws IOException
	 */
	public void stop() throws IOException {
		if (video_ds != null) {
			video_processor.stop();
			video_ds.stop();
		}
		if (audio_ds != null) {
			audio_processor.stop();
			audio_ds.stop();
		}
	}

	/**
	 * 
	 * 
	 */
	public void close() {
		if (video_ds != null) {
			video_processor.close();
			video_ds.close();
		}
		if (audio_ds != null) {
			audio_processor.close();
			audio_ds.close();
		}
	}

	/**
	 * レスポンス用のSDPの取得
	 * @param user
	 * @param video_port
	 * @param sdp
	 * @return
	 * @throws UnknownHostException
	 * @throws SdpException
	 */
	public SessionDescription getResponseSessionDescription(String user,
			 int video_port, SessionDescription sdp)
			throws UnknownHostException, SdpException {
		SessionDescription ret = null;
		if (sdp != null)
			ret = SDPGenerator.getResponseSessionDescription(user, 0,
					video_port, sdp);
		else
			// ret = MediaReceiver.getRequestSessionDescription();
			ret = getRequestSessionDescription(0, video_port);
		return ret;
	}
	
	/**
	 * レスポンス用のSDPの取得
	 * @param user
	 * @param audio_port
	 * @param video_port
	 * @param sdp
	 * @return
	 * @throws UnknownHostException
	 * @throws SdpException
	 */
	public SessionDescription getResponseSessionDescription(String user,
			int audio_port, int video_port, SessionDescription sdp)
			throws UnknownHostException, SdpException {
		SessionDescription ret = null;
		if (sdp != null)
			ret = SDPGenerator.getResponseSessionDescription(user, audio_port,
					video_port, sdp);
		else
			// ret = MediaReceiver.getRequestSessionDescription();
			ret = getRequestSessionDescription(audio_port, video_port);
		return ret;
	}

	public SessionDescription getRequestSessionDescription(int video_port)
			throws UnknownHostException, SdpException {
		InetAddress addr = InetAddress.getLocalHost();
		List<Integer> audio = null;
		enableAudio(false);
		SessionDescription sdp = SDPGenerator.getRequestSessionDescription(0,
				"media_sender", "-", addr.getHostAddress(), audio, 0,
				availableVideoFormats, video_port);
		return sdp;

	}

	public SessionDescription getRequestSessionDescription(int audio_port,
			int video_port) throws UnknownHostException, SdpException {
		InetAddress addr = InetAddress.getLocalHost();
		List<Integer> audio = null;

		SessionDescription sdp = SDPGenerator.getRequestSessionDescription(0,
				"media_sender", "-", addr.getHostAddress(), availableAudioFormats, audio_port,
				availableVideoFormats, video_port);
		return sdp;

	}

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		final MediaTransmitter mt = new MediaTransmitter();
		mt.enableAudio(false);
		mt.sendMedia("localhost", 44444,22222);
//		mt.setVideoPort(44444);
//		mt.setAudioPort(22222);
		JFrame jf = new JFrame("Test");
		jf.setLayout(new FlowLayout());
		JButton play = new JButton("play");
		play.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				try {
					mt.play();
					System.out.println(mt.isPlaying());
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		});
		jf.add(play);
		JButton jb = new JButton("stop");
		jb.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				try {
					mt.stop();
					System.out.println(mt.isPlaying());
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			}
		});
		jf.add(jb);
		JButton close = new JButton("close");
		close.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				mt.close();
				System.out.println(mt.isPlaying());
			}
		});
		jf.add(close);
		// jf.add(player.transmitter.getVisualComponent());
		// jf.add(player.getTransmitterController());
		jf.pack();
		jf.setVisible(true);
		jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	}
}
