import java.io.IOException;
import java.net.InetAddress;
import java.awt.Dimension;
import javax.media.ControllerListener;
import javax.media.MediaLocator;
import javax.media.Processor;
import javax.media.ControllerEvent;
import javax.media.NoProcessorException;
import javax.media.Format;
import javax.media.Controller;
import javax.media.DurationUpdateEvent;
import javax.media.Time;
import javax.media.EndOfMediaEvent;
import javax.media.Player;
import javax.media.Control;
import javax.media.Owned;
import javax.media.Codec;
import javax.media.ControllerClosedEvent;
import javax.media.format.VideoFormat;
import javax.media.protocol.DataSource;
import javax.media.protocol.ContentDescriptor;
import javax.media.control.TrackControl;
import javax.media.control.QualityControl;
import javax.media.rtp.ReceiveStreamListener;
import javax.media.rtp.RemoteListener;
import javax.media.rtp.RTPManager;
import javax.media.rtp.SessionAddress;
import javax.media.rtp.Participant;
import javax.media.rtp.rtcp.ReceiverReport;
import javax.media.rtp.event.ReceiveStreamEvent;
import javax.media.rtp.event.RemoteEvent;
import javax.media.rtp.event.InactiveReceiveStreamEvent;
import javax.media.rtp.event.ByeEvent;
import javax.media.rtp.event.ReceiverReportEvent;

//clase que implementa un transmisor
public class Transmisor implements ReceiveStreamListener, RemoteListener, ControllerListener
{
	//constante de la clase
	private static final float JPEGQ = 0.1f;

	//variables miembro de la clase
	protected Target destino;
    protected MediaLocator locator;
    protected Processor procesador;
    protected boolean fallo;
    protected Integer stateLock;
	protected DataSource salida;
	protected RTPManager rtpManagers[];
	protected int puertosLocales[];

	//constructor de la clase
	public Transmisor(Target destino)
	{
		//inicializamos las variables
		System.out.println(Misc.ind() + "Inicializando las variables");
		this.destino = destino;
		procesador = null;
		locator = null;
		fallo = false;
		stateLock = new Integer(0);
		salida = null;
		rtpManagers = null;
	}

	//metodo que detiene la transmisión RTP
	public void stop()
	{
		synchronized(this)
		{
			//si existe el procesador
			if (procesador != null)
			{
				//detenemos el procesador
				System.out.println(Misc.ind() + "Detenemos el procesador");
				procesador.stop();
				System.out.println(Misc.ind() + "Cerramos el procesador");
				//cerramos el procesador
				procesador.close();
				procesador = null;
				//eliminamos los manejadores RTP
				System.out.print(Misc.ind() + "Eliminamos los destinos");
				for (int i = 0; i < rtpManagers.length; i++)
				{
					//eliminamos el destino
					System.out.print(".");
					rtpManagers[i].removeTargets("Session finalizada.");
					//eliminamos el manejador
					rtpManagers[i].dispose();
				}
				System.out.println();
			}
		}
	}

	//metodo que crea un procesador
	protected String crearProcesador()
	{
		//comprobamos que existe un locator
		System.out.println(Misc.ind() + "Comprobando que existe el locator");
		if (locator == null)
			return "No hay seleccionado ningun dispositivo de captura";

		DataSource ds;
		DataSource clone;

		//creamos una fuente de datos
		System.out.println(Misc.ind() + "Creando una fuente de datos");
		try
		{
			ds = javax.media.Manager.createDataSource(locator);
		}
		//capturamos la excepcion
		catch (Exception e)
		{
			return "No se pudo encontrar una fuente de datos";
		}
		//llamamos al metodo que crea el procesador a partir de la fuente de datos
		System.out.println(Misc.ind() + "Creando el procesador");
		try
		{
			procesador = javax.media.Manager.createProcessor(ds);
			procesador.addControllerListener(this);
		}
		//capturamos la excepcion
		catch (NoProcessorException npe)
		{
			return "No se pudo crear el procesador";
		}
		//capturamos otra excepcion
		catch (IOException ioe)
		{
			return "Excepcion de E/S al crear el procesador";
		}
		System.out.println(Misc.ind() + "Configurando el procesador");
		Misc.aumentarInd();
		//esperamos a que el procesador esté creado
		if (!waitForState(procesador, Processor.Configured))
		{
			Misc.reducirInd();
			return "No se pudo configurar el procesador";
		}
		Misc.reducirInd();
		//obtenemos la lista de las pistas
		System.out.println(Misc.ind() + "Obteniendo las pistas");
		TrackControl [] tracks = procesador.getTrackControls();
		//comprobamos que haya pistas
		System.out.println(Misc.ind() + "Comprobando que hay pistas");
		if (tracks == null || tracks.length < 1)
			return "No se pudo encontrar ninguna pista en el procesador";
		//creamos un descriptor de contenido
		System.out.println(Misc.ind() + "Creando un descriptor de contenido");
		ContentDescriptor cd = new ContentDescriptor(ContentDescriptor.RAW_RTP);
		//aplicamos el descriptor de contenido
		procesador.setContentDescriptor(cd);

		Format supported[];
		Format chosen;
		boolean atLeastOneTrack = false;
		//recorremos las pistas
		System.out.println(Misc.ind() + "Comprobando las pistas");
		Misc.aumentarInd();
		for (int i = 0; i < tracks.length; i++)
		{
			//obtenemos el formato de la pista
			System.out.println(Misc.ind() + "Obteniendo formato de la pista " + i);
			Format format = tracks[i].getFormat();
			//si la pista está activa
			if (tracks[i].isEnabled())
			{
				//obtenemos un formato válido
				System.out.println(Misc.ind() + "Comprobando que tenga un formato valido");
				supported = tracks[i].getSupportedFormats();
				//comprobamos que contenga datos
				System.out.println(Misc.ind() + "Comprobando que contenga datos");
				if (supported.length > 0)
				{
					if (supported[0] instanceof VideoFormat)
					{
						//comprobamos las dimensiones del video
						chosen = checkForVideoSizes(tracks[i].getFormat(), supported[0]);
					}
					else
						chosen = supported[0];
					//aplicamos el formato a la pista
					tracks[i].setFormat(chosen);
					System.err.println(Misc.ind() + "Pista " + i + " transmitiendo como:");
					Misc.aumentarInd();
					System.err.println(Misc.ind() + chosen);
					Misc.reducirInd();
					atLeastOneTrack = true;
				}
				else
					tracks[i].setEnabled(false);
			}
			else
				tracks[i].setEnabled(false);
		}
		Misc.reducirInd();
		//comprobamos que haya al menos una pista válida
		System.out.println(Misc.ind() + "Comprobando que hay alguna pista utilizable para RTP");
		if (!atLeastOneTrack)
			return "No se pudo utilizar ninguna de las pistas para una transmisión RTP";
		//esperamos a que termine la configuración del procesador
		System.out.println(Misc.ind() + "Finalizando la creacion del procesador");
		Misc.aumentarInd();
		if (!waitForState(procesador, Controller.Realized))
		{
			Misc.reducirInd();
			return "No se pudo llevar a cabo el procesador...";
		}
		Misc.reducirInd();

		//cambiamos la calidad del formato JPG
		setJPEGQuality(procesador, JPEGQ);

		//obtenemos la salida del procesador
		System.out.println(Misc.ind() + "Obteniendo salida de datos");
		salida = procesador.getDataOutput();

		return null;
	}

	//metodo que comprueba las dimensiones del video
	private Format checkForVideoSizes(Format original, Format supported)
	{
		int width, height;
		//obtenemos las dimensiones originales
		Dimension size = ((VideoFormat)original).getSize();
		Format jpegFmt = new Format(VideoFormat.JPEG_RTP);
		Format h263Fmt = new Format(VideoFormat.H263_RTP);

		//si se trata del formato JPG
		if (supported.matches(jpegFmt))
		{
			//el alto y el ancho han de ser divisibles por 8
			width = (size.width % 8 == 0 ? size.width : (int)(size.width / 8) * 8);
			height = (size.height % 8 == 0 ? size.height : (int)(size.height / 8) * 8);
		}
		//si se trata del formato H263
		else if (supported.matches(h263Fmt))
		{
			//seleccionamos un tamaño valido
			if (size.width < 128)
			{
				width = 128;
				height = 96;
			}
			else if (size.width < 176)
			{
				width = 176;
				height = 144;
			}
			else
			{
				width = 352;
				height = 288;
			}
		}
		//en otro caso
		else
		{
			return supported;
		}

		//devolvemos el nuevo formato de video
		return (new VideoFormat(null, new Dimension(width, height), Format.NOT_SPECIFIED,
								null, Format.NOT_SPECIFIED)).intersects(supported);
	}

	//metodo que ajusta la calidad del formato JPG
	protected void setJPEGQuality(Player p, float val)
	{
		//obtenemos los controles
		Control cs[] = p.getControls();
		QualityControl qc = null;
		//generamos un formato de video JPEG
		VideoFormat jpegFmt = new VideoFormat(VideoFormat.JPEG);
		//recorremos todos los controles
		for (int i = 0; i < cs.length; i++)
		{
			//si estamos en el control de calidad
			if (cs[i] instanceof QualityControl && cs[i] instanceof Owned)
			{
				Object owner = ((Owned)cs[i]).getOwner();

				//comprobamos si se trata de un codec
				if (owner instanceof Codec)
				{
					//generamos una tabla de formatos soportados
					Format fmts[] = ((Codec)owner).getSupportedOutputFormats(null);
					//recorremos la tabla
					for (int j = 0; j < fmts.length; j++)
					{
						//si el formato es JPG
						if (fmts[j].matches(jpegFmt))
						{
							//obtenemos el control de calidad
							qc = (QualityControl)cs[i];
							//ajustamos el control de calidad
							qc.setQuality(val);
							System.err.println("  - Ajustando calidad a " + val + " sobre " + qc);
							break;
						}
					}
				}
				if (qc != null)
					break;
			}
		}
	}

	//metodo que crea una espera hasta finalizar la tarea
	protected synchronized boolean waitForState(Processor p, int state)
	{
		//añadimos un evento escucha
		p.addControllerListener(new StateListener());
		fallo = false;

		//miramos si el procesador se está configurando
		if (state == Processor.Configured)
		{
			System.out.println(Misc.ind() + "Configurando...");
			//configuramos el procesador
			p.configure();
		}
		//miramos si el procesador se está realizando
		else if (state == Processor.Realized)
		{
			System.out.println(Misc.ind() + "Finalizando configuracion...");
			//realizamos el procesador
			p.realize();
		}
		//esperamos a que termine
		System.out.print(Misc.ind() + "Esperando");
		while (p.getState() < state && !fallo)
		{
			//comprobamos el estado de bloqueo
			System.out.print(".");
			synchronized (getStateLock())
			{
				//hacemos una pausa
				try
				{
					getStateLock().wait();
				}
				catch (InterruptedException ie)
				{
					return false;
				}
			}
		}
		System.out.println();

		if (fallo)
			return false;
		else
			return true;
	}

	//metodo que devuelve el estado de bloqueo
	private Integer getStateLock()
	{
		return stateLock;
	}

	//metodo que activa un fallo
	void setFailed()
	{
		fallo = true;
	}

	//metodo que añade un destino
	public void addTarget(int localPort, RTPManager mgr, String ip, int port)
	{
		try
		{
			//creamos la sesion
			SessionAddress addr = new SessionAddress(InetAddress.getByName(ip), new Integer( port).intValue());
			//añadimos la sesion
			mgr.addTarget(addr);
		}
		//capturamos la excepcion
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	//método que se ejecuta cuando se actualiza el controlador
	public void controllerUpdate(ControllerEvent ce)
	{
		//si se actualiza la duracion
		if (ce instanceof DurationUpdateEvent)
		{
			//obtenemos la duracion
			Time duration = ((DurationUpdateEvent) ce).getDuration();
			System.out.println(Misc.ind() + "duracion: " + duration.getSeconds());
		}
		//si obtenemos el final de la secuencia
		else
		if (ce instanceof EndOfMediaEvent)
		{
			System.out.println(Misc.ind() + "Fin de la comunicacion");
		}
	}

	//metodo ejecutado al recibir una secuencia
	public void update(ReceiveStreamEvent event)
	{
		//creamos un buffer
		StringBuffer sb = new StringBuffer();

		//si se recibe una cadena de inactividad
		if (event instanceof InactiveReceiveStreamEvent)
		{
			sb.append("Cadena recivida inactiva");
		}
		//si se recibe un adios
		else if (event instanceof ByeEvent)
		{
			sb.append("Hasta luego");
		}
		else
		{
			System.out.println(Misc.ind() + "Recibido evento de cadena: "+ event);
		}
		System.out.println(Misc.ind() + sb.toString());
	}

	//metodo ejecutado al recibir un evento remoto
	public void update(RemoteEvent event)
	{
		//si se recibe un informe de recepcion
		if (event instanceof ReceiverReportEvent)
		{
			//obtenemos el informe de recepcion
			ReceiverReport rr = ((ReceiverReportEvent) event).getReport();
			//creamos un buffer
			StringBuffer sb = new StringBuffer();
			//vamos dando formato al buffer
			sb.append("Recepcion");
			//si el informe existe
			if (rr != null)
			{
				//obtenemos el participante
				Participant participant = rr.getParticipant();
				//concatenamos diversa informacion
				if (participant != null)
				{
					sb.append(" desde " + participant.getCNAME());
					sb.append(" ssrc = " + rr.getSSRC());
				}
				else
				{
					sb.append(" ssrc = " + rr.getSSRC());
				}

				System.out.println(Misc.ind() + sb.toString());
			}
		}
		else
		{
			System.out.println(Misc.ind() + "Evento remoto: " + event);
		}
	}

	//subclase que mantiene un escucha
	class StateListener implements ControllerListener
	{
		//metodo ejecutado al actualizarse el controlador
		public void controllerUpdate(ControllerEvent ce)
		{
			//si se cierra el controlador, hacemos fallo
			if (ce instanceof ControllerClosedEvent)
				setFailed();
			//si se produce un evento del controlador
			if (ce instanceof ControllerEvent)
			{
				synchronized (getStateLock())
				{
					//informamos
					getStateLock().notifyAll();
				}
			}
		}
	}
}
