package com.wowza.wms.plugin.livestreamrecord.module;

import java.io.File;
import java.util.*;

import com.wowza.wms.amf.*;
import com.wowza.wms.application.IApplicationInstance;
import com.wowza.wms.application.WMSProperties;
import com.wowza.wms.client.IClient;
import com.wowza.wms.livestreamrecord.model.*;
import com.wowza.wms.logging.WMSLoggerFactory;
import com.wowza.wms.media.model.*;
import com.wowza.wms.module.IModuleOnStream;
import com.wowza.wms.module.ModuleBase;
import com.wowza.wms.request.RequestFunction;
import com.wowza.wms.stream.*;
import com.wowza.wms.stream.mediacaster.MediaStreamMediaCasterUtils;
import com.wowza.wms.vhost.*;

public class ModuleLiveStreamRecord  extends ModuleBase implements IModuleOnStream
{
    private static final Class<ModuleLiveStreamRecord> CLASS = ModuleLiveStreamRecord.class;

	public class StartRecordParameters
	{
		public boolean append = false;
		public String outputPath = null;
		public int format = 2;  // default to mp4
		public boolean versionFile = true;
		public boolean startOnKeyFrame = true;
		public boolean recordData = true;
		public long segmentSize = 1048576*10;  // 10 MB default
		public long segmentDuration = 15*60000; // 15 minutes default
		public String segmentSchedule = null; // crontab string specifying when to split recording
		public String action = "";
		public String fileTemplate = null;
		public String outputFile = null;
	}
	
	class StreamListener implements IMediaStreamActionNotify3
	{
		public void onPause(IMediaStream stream, boolean isPause, double location)
		{
		}

		public void onPlay(IMediaStream stream, String streamName, double playStart, double playLen, int playReset)
		{
		}

		public void onPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend)
		{
		}

		public void onSeek(IMediaStream stream, double location)
		{
		}

		public void onStop(IMediaStream stream)
		{
		}

		public void onUnPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend)
		{
			// removed the recorder from the list when the stream is unPublished 
			synchronized (recorders)
			{
				recorders.remove(streamName);
			}
		}

		public void onMetaData(IMediaStream stream, AMFPacket metaDataPacket) {
		}

		public void onPauseRaw(IMediaStream stream, boolean isPause, double location) {
		}

		public void onCodecInfoVideo(IMediaStream stream, MediaCodecInfoVideo codecInfoVideo) {
		}

		public void onCodecInfoAudio(IMediaStream stream, MediaCodecInfoAudio codecInfoAudio) {
		}
	}


	public static final int FORMAT_UNKNOWN = 0;
	public static final int FORMAT_FLV = 1;
	public static final int FORMAT_MP4 = 2;
	
	private Map<String, ILiveStreamRecord> recorders = new HashMap<String, ILiveStreamRecord>();
	private IApplicationInstance appInstance = null;
	private IVHost vhost = null;
	private String recordMethod = "";
	
	public void recordStream(String streamName, int format, boolean append, String outputPath, boolean versionFile, boolean startOnKeyFrame, boolean recordData)
	{
		IMediaStream stream = appInstance.getStreams().getStream(streamName);
		if (stream != null)
		{
			StartRecordParameters params = new StartRecordParameters();
			params.format = format;
			params.append = append;
			params.outputPath = outputPath;
			params.versionFile = versionFile;
			params.startOnKeyFrame =  startOnKeyFrame;
			params.recordData = recordData;
			recordStream(stream, params);
		}
		else
			WMSLoggerFactory.getLogger(CLASS).warn("["+this.vhost+"]"+"ModuleLiveStreamRecord.recordStream: Stream not found: "+streamName);
	}
	
	private void recordStream(IMediaStream stream, StartRecordParameters p)
	{
		String streamName = stream.getName();
		
		// if a format was not specified then check the stream prefix and choose accordingly
		if (p.format == FORMAT_UNKNOWN)
		{
			p.format = FORMAT_FLV;
			String extStr = stream.getExt();
			if (extStr.equals("mp4"))
				p.format = FORMAT_MP4;
		}
		
		String params = "action:"+p.action;
		params += "stream:"+streamName;
		params += " format:"+(p.format==FORMAT_MP4?"mp4":"flv");
		params += " append:"+p.append;
		if (p.outputPath != null)
			params += " outputPath:"+p.outputPath;
		else
		{
			File writeFile = stream.getStreamFileForWrite(stream.getName(), (p.format==FORMAT_MP4?MediaStream.MP4_STREAM_EXT:MediaStream.BASE_STREAM_EXT), stream.getQueryStr());
			params += " outputPath:"+writeFile.getAbsolutePath();
		}
		params += " versionFile:"+p.versionFile;
		params += " startOnKeyFrame:"+p.startOnKeyFrame;
		params += " recordData:"+p.recordData;
		params += " segmentDuration:"+p.segmentDuration;
		params += " segmentSize:"+p.segmentSize;
		params += " segmentSchdule:"+p.segmentSchedule;
		
		WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"ModuleStreamRecord.startRecording: "+params);
		
		// create a stream recorder and save it in a map of recorders
		ILiveStreamRecord recorder = null;
		
		// create the correct recorder based on format
		if (p.format == FORMAT_MP4)
			recorder = new LiveStreamRecorderMP4();
		else
			recorder = new LiveStreamRecorderFLV();
		
		recorder.init(appInstance);
		
		// add it to the recorders list
		synchronized (recorders)
		{
			ILiveStreamRecord prevRecorder = recorders.get(streamName);
			if (prevRecorder != null)
				prevRecorder.stopRecording();
			recorders.put(streamName, recorder);
		}
		
		// if you want to record data packets as well as video/audio
		recorder.setRecordData(p.recordData);
		
		// Set to true if you want to version the previous file rather than overwrite it
		recorder.setVersionFile(p.versionFile);
		
		// If recording only audio set this to false so the recording starts immediately
		recorder.setStartOnKeyFrame(p.startOnKeyFrame);
		
		// If you want to be notified when a recording segment is opened/Starts and when it is complete/Ends, 
		// add your listener which implements the ILiveStreamRecordingNotify interface
		// recorder.addListener(new MyListener());

		// If you want to use your own file Version Delegate, set it here
		// recorder.setFileVersionDelegate(new MyFileVersionDelegate());

		// if the default file version delegate is being used, you can set the template here
		/*
		if ((recorder.getFileVersionDelegate() instanceof LiveStreamRecordFileVersionDelegate))
		{
			if (p.fileTemplate != null && p.fileTemplate.length() > 0)
			{
				LiveStreamRecordFileVersionDelegate delegate = (LiveStreamRecordFileVersionDelegate)recorder.getFileVersionDelegate();
				delegate.setFileTemplate(p.fileTemplate);
			}
		}
		 */
		
		// start recording
		if (p.action.equalsIgnoreCase("startRecordingSegmentBySize"))
		{
			// start recording
			recorder.startRecordingSegmentBySize(stream, p.outputPath, null, p.segmentSize);
		}
		else if (p.action.equalsIgnoreCase("startRecordingSegmentByDuration"))
		{
			// start recording
			recorder.startRecordingSegmentByDuration(stream, p.outputPath, null, p.segmentDuration);
		} 
		else if (p.action.equalsIgnoreCase("startRecordingSegmentBySchedule"))
		{
			// start recording
			recorder.startRecordingSegmentBySchedule(stream, p.outputPath, null, p.segmentSchedule);
		}
		else
		{
			// start recording
			recorder.startRecording(stream, p.outputPath, p.append);
		}
	}

	public void startRecordingSegmentBySize(IClient client, RequestFunction function, AMFDataList params)
	{
		recordMethod = "startRecordingSegmentBySize";
		startRecording( client, function, params);
	}

	public void startRecordingSegmentByDuration(IClient client, RequestFunction function, AMFDataList params)
	{
		recordMethod = "startRecordingSegmentByDuration";
		startRecording( client, function, params);
	}
	
	public void startRecordingSegmentBySchedule(IClient client, RequestFunction function, AMFDataList params)
	{
		recordMethod = "startRecordingSegmentBySchedule";
		startRecording( client, function, params);
	}
	
	public void startRecording(IClient client, RequestFunction function, AMFDataList params)
	{
		StartRecordParameters recordParams = new StartRecordParameters();
		String streamName = params.getString(PARAM1);

		// Decode options if sent
		if (PARAM2 < params.size())
		{
			AMFDataObj dataObj = params.getObject(PARAM2);
			
			while(true)
			{
				if (dataObj == null)
					break;
				
				@SuppressWarnings("unchecked")
				List<String> keys = dataObj.getKeys();
				Iterator<String> iter = keys.iterator();
				while(iter.hasNext())
				{
					String key = iter.next();
					
					try
					{
						if (key.equalsIgnoreCase("format"))
						{
							String value = dataObj.getString(key);
							if (value.equals("flv"))
								recordParams.format = FORMAT_FLV;
							else if (value.equals("mp4"))
								recordParams.format = FORMAT_MP4;
						}
						else if (key.equalsIgnoreCase("append"))
						{
							recordParams.append = dataObj.getBoolean(key);
						}
						else if (key.equalsIgnoreCase("outputPath"))
						{
							recordParams.outputPath = client.getAppInstance().decodeStorageDir(dataObj.getString(key));
							if (recordParams.outputFile != null)
								recordParams.outputFile = (recordParams.outputFile.equalsIgnoreCase("")?null:recordParams.outputFile);
						}
						else if (key.equalsIgnoreCase("versionFile"))
						{
							recordParams.versionFile = dataObj.getBoolean(key);
						}
						else if (key.equalsIgnoreCase("startOnKeyFrame"))
						{
							recordParams.startOnKeyFrame = dataObj.getBoolean(key);
						}
						else if (key.equalsIgnoreCase("recordData"))
						{
							recordParams.recordData = dataObj.getBoolean(key);
						}
						else if (key.equalsIgnoreCase("segmentSize"))
						{
							recordParams.segmentSize = dataObj.getLong(key);
						}
						else if (key.equalsIgnoreCase("segmentDuration"))
						{
							recordParams.segmentDuration = dataObj.getLong(key);
						}
						else if (key.equalsIgnoreCase("segmentSchedule"))
						{
							recordParams.segmentSchedule = dataObj.getString(key);
						}
					}
					catch (Exception e)
					{
						WMSLoggerFactory.getLogger(CLASS).error("["+this.vhost+"]"+"ModuleLiveStreamRecord.startRecording: Error processing param["+key+"]: "+e.toString());
					}
				}
				break;
			}
			
		}

		if (recordMethod.equalsIgnoreCase("startRecordingSegmentBySize") || 
			recordMethod.equalsIgnoreCase("startRecordingSegmentByDuration") ||
			recordMethod.equalsIgnoreCase("startRecordingSegmentBySchedule")) 
		{
			recordParams.action = recordMethod;
		}
		else
		{
			recordParams.action = "startRecording";
		}
		
		IApplicationInstance appInstance = client.getAppInstance();

		String streamTypeStr = "default";
		String tmpStr = client.getStreamType();
		if (tmpStr != null)
			streamTypeStr = tmpStr;
		
		boolean isLiveRepeaterEdge = false;
		while(true)
		{
			StreamList streamDefs = appInstance.getVHost().getStreamTypes();
			StreamItem streamDef = streamDefs.getStreamDef(streamTypeStr);
			if (streamDef == null)
				break;			
			isLiveRepeaterEdge = streamDef.getProperties().getPropertyBoolean("isLiveRepeaterEdge", isLiveRepeaterEdge);
			break;
		}
		
		if (isLiveRepeaterEdge)
			streamName = MediaStreamMediaCasterUtils.mapMediaCasterName(appInstance, client, streamName);
		
		IMediaStream stream = appInstance.getStreams().getStream(streamName);
		if (stream != null)
		{
			recordStream(stream, recordParams);
		}
		else
			WMSLoggerFactory.getLogger(CLASS).warn("["+this.vhost+"]"+"ModuleStreamRecord."+recordParams.action+": stream not found: "+streamName);
	}
	
	public String[] getRecorderNames()
	{
		List<String> retList = new ArrayList<String>();
		retList.addAll(recorders.keySet());
		return retList.toArray(new String[retList.size()]);
	}
	
	public void getPublishStreamNames(IClient client, RequestFunction function, AMFDataList params)
	{
		AMFDataArray ret = new AMFDataArray();
		
		WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"ModuleStreamRecord.getPublishStreamNames");

		List<String> namesList = client.getAppInstance().getPublishStreamNames();
		Iterator<String> iter = namesList.iterator();
		int index = 0;
		while(iter.hasNext())
		{
			String streamName = iter.next();
			ret.add(new AMFDataItem(streamName));
			WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"  publishName["+index+"]: "+streamName);
			index++;
		}
		
		sendResult(client, params, ret);
	}

	public String stopRecording(String streamName)
	{
		ILiveStreamRecord recorder = null;
		synchronized (recorders)
		{
			recorder = recorders.remove(streamName);
		}
		
		String outputPath  = null;
		if (recorder != null)
		{
			// grab the current path to the recorded file
			outputPath = recorder.getFilePath();
			
			// stop recording
			recorder.stopRecording();
		}
		else
			WMSLoggerFactory.getLogger(CLASS).warn("["+this.vhost+"]"+"ModuleStreamRecord.stopRecording: stream recorder not found: "+streamName);
		
		return outputPath;
	}
	
	public void stopRecording(IClient client, RequestFunction function, AMFDataList params)
	{
		String streamName = params.getString(PARAM1);
		WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"ModuleStreamRecord.stopRecording: "+streamName);
		
		ILiveStreamRecord recorder = null;
		synchronized (recorders)
		{
			recorder = recorders.remove(streamName);
		}
		
		if (recorder != null)
		{
			// stop recording
			recorder.stopRecording();
		}
		else
			WMSLoggerFactory.getLogger(CLASS).warn("["+this.vhost+"]"+"ModuleStreamRecord.stopRecording: stream recorder not found: "+streamName);
	}
	
	public void splitRecordingNow(IClient client, RequestFunction function, AMFDataList params)
	{
		String streamName = params.getString(PARAM1);
		WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"ModuleStreamRecord.splitRecordingNow: "+streamName);
		
		ILiveStreamRecord recorder = null;
		synchronized (recorders)
		{
			recorder = recorders.get(streamName);
		}
		
		if (recorder != null)
		{
			// split recording
			recorder.splitRecordingNow();
		}
		else
			WMSLoggerFactory.getLogger(CLASS).warn("["+this.vhost+"]"+"ModuleStreamRecord.splitRecordingNow: stream recorder not found: "+streamName);
	}

	public void onAppStart(IApplicationInstance appInstance)
	{
		this.appInstance = appInstance;
		this.vhost = appInstance.getVHost();
		
		WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"ModuleStreamRecord.onAppStart");
	}
	
	public void onAppStop(IApplicationInstance appInstance)
	{
		WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"ModuleStreamRecord.onAppStop");
		
		// cleanup any recorders that are still running
		synchronized (recorders)
		{
			Iterator<String> iter = recorders.keySet().iterator();
			while(iter.hasNext())
			{
				String streamName = iter.next();
				ILiveStreamRecord recorder = recorders.get(streamName);
				recorder.stopRecording();
				WMSLoggerFactory.getLogger(CLASS).info("["+this.vhost+"]"+"  stopRecording: "+streamName);
			}
			recorders.clear();
		}
	}
	
	public void onStreamCreate(IMediaStream stream)
	{
		getLogger().info("onStreamCreate["+stream+"]: clientId:" + stream.getClientId());
		IMediaStreamActionNotify3 actionNotify = new StreamListener();

		WMSProperties props = stream.getProperties();
		synchronized (props)
		{
			props.put("streamActionNotifier", actionNotify);
		}
		stream.addClientListener(actionNotify);
	}

	public void onStreamDestroy(IMediaStream stream)
	{
		getLogger().info("onStreamDestroy["+stream+"]: clientId:" + stream.getClientId());

		IMediaStreamActionNotify3 actionNotify = null;
		WMSProperties props = stream.getProperties();
		synchronized (props)
		{
			actionNotify = (IMediaStreamActionNotify3) stream.getProperties().get("streamActionNotifier");
		}
		if (actionNotify != null)
		{
			stream.removeClientListener(actionNotify);
			getLogger().info("removeClientListener: " + stream.getSrc());
		}
	}
}
