/*
*      _______                       _____   _____ _____  
*     |__   __|                     |  __ \ / ____|  __ \ 
*        | | __ _ _ __ ___  ___  ___| |  | | (___ | |__) |
*        | |/ _` | '__/ __|/ _ \/ __| |  | |\___ \|  ___/ 
*        | | (_| | |  \__ \ (_) \__ \ |__| |____) | |     
*        |_|\__,_|_|  |___/\___/|___/_____/|_____/|_|     
*                                                         
* -------------------------------------------------------------
*
* TarsosDSP is developed by Joren Six at IPEM, University Ghent
*  
* -------------------------------------------------------------
*
*  Info: http://0110.be/tag/TarsosDSP
*  Github: https://github.com/JorenSix/TarsosDSP
*  Releases: http://0110.be/releases/TarsosDSP/
*  
*  TarsosDSP includes modified source code by various authors,
*  for credits and info, see README.
* 
*/

package be.tarsos.dsp.io;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteOrder;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import be.tarsos.dsp.util.FFMPEGDownloader;

/**
 * <p>
 * Decode audio files to PCM, mono, 16bits per sample, at any sample rate using
 * an external program. By default ffmpeg is used. Other
 * command Line  programs that are able to decode audio and pipe binary PCM
 * samples to STDOUT are possible as well (avconv, mplayer). 
 * To install ffmpeg on Debian: <code>apt-get install ffmpeg</code>.
 * </p>
 * <p>
 * This adds support for a lot of audio formats and video container formats with
 * relatively little effort. Depending on the program used also http streams,
 * rtpm streams, ... are supported as well.
 * </p>
 * <p>
 * To see which audio decoders are supported, check
 * </p>
 * <code><pre>ffmpeg -decoders | grep -E "^A" | sort
avconv version 9.8, Copyright (c) 2000-2013 the Libav developers
  built on Aug 26 2013 09:52:20 with gcc 4.4.3 (Ubuntu 4.4.3-4ubuntu5.1)
A... 8svx_exp             8SVX exponential
A... 8svx_fib             8SVX fibonacci
A... aac                  AAC (Advanced Audio Coding)
A... aac_latm             AAC LATM (Advanced Audio Coding LATM syntax)
A... ac3                  ATSC A/52A (AC-3)
A... adpcm_4xm            ADPCM 4X Movie
...
</pre></code>
 * 
 * @author Joren Six
 */
public class PipeDecoder {
	
	private final static Logger LOG = Logger.getLogger(PipeDecoder.class.getName());
	private final String pipeEnvironment;
	private final String pipeArgument;
	private final String pipeCommand;
	private final int pipeBuffer;

	private boolean printErrorstream = false;

	private String decoderBinaryAbsolutePath;
	
	public PipeDecoder(){
		pipeBuffer = 10000;

		//Use sensible defaults depending on the platform
		if(System.getProperty("os.name").indexOf("indows") > 0 ){
			pipeEnvironment = "cmd.exe";
			pipeArgument = "/C";
		}else if(new File("/bin/bash").exists()){
			pipeEnvironment = "/bin/bash";
			pipeArgument = "-c";
		}else if (new File("/system/bin/sh").exists()){
			//probably we are on android here
			pipeEnvironment = "/system/bin/sh";
			pipeArgument = "-c";
		}else{
			LOG.severe("Coud not find a command line environment (cmd.exe or /bin/bash)");
			throw new Error("Decoding via a pipe will not work: Coud not find a command line environment (cmd.exe or /bin/bash)");
		}
		
		String path = System.getenv("PATH");
		String arguments = " -ss %input_seeking%  %number_of_seconds% -i \"%resource%\" -vn -ar %sample_rate% -ac %channels% -sample_fmt s16 -f s16le pipe:1";
		if(isAvailable("ffmpeg")){
			LOG.info("found ffmpeg on the path (" + path + "). Will use ffmpeg for decoding media files.");
			pipeCommand = "ffmpeg" + arguments;	
		}else if (isAvailable("avconv")){
			LOG.info("found avconv on your path(" + path + "). Will use avconv for decoding media files.");
			pipeCommand = "avconv" + arguments;
		}else {
			if(isAndroid()) {
				String tempDirectory = System.getProperty("java.io.tmpdir");
				printErrorstream=true;
				File f = new File(tempDirectory, "ffmpeg");
				if (f.exists() && f.length() > 1000000 && f.canExecute()) {
					decoderBinaryAbsolutePath = f.getAbsolutePath();
				} else {
					LOG.severe("Could not find an ffmpeg binary for your Android system. Did you forget calling: 'new AndroidFFMPEGLocator(this);' ?");
					LOG.severe("Tried to unpack a statically compiled ffmpeg binary for your architecture to: " + f.getAbsolutePath());
				}
			}else{
				LOG.warning("Dit not find ffmpeg or avconv on your path(" + path + "), will try to download it automatically.");
				FFMPEGDownloader downloader = new FFMPEGDownloader();
				decoderBinaryAbsolutePath = downloader.ffmpegBinary();
				if(decoderBinaryAbsolutePath==null){
					LOG.severe("Could not download an ffmpeg binary automatically for your system.");
				}
			}
			if(decoderBinaryAbsolutePath == null){
				pipeCommand = "false";
				throw new Error("Decoding via a pipe will not work: Could not find an ffmpeg binary for your system");
			}else{
				pipeCommand = '"' + decoderBinaryAbsolutePath + '"' + arguments;
			}
		}
	}
	
	private boolean isAvailable(String command){
		try{
			Runtime.getRuntime().exec(command + " -version");
			return true;
		}catch (Exception e){
			return false;
		}	
	}
	
	public PipeDecoder(String pipeEnvironment,String pipeArgument,String pipeCommand,String pipeLogFile,int pipeBuffer){
		this.pipeEnvironment = pipeEnvironment;
		this.pipeArgument = pipeArgument;
		this.pipeCommand = pipeCommand;
		this.pipeBuffer = pipeBuffer;
	}

	
	public InputStream getDecodedStream(final String resource,final int targetSampleRate,final double timeOffset, double numberOfSeconds) {
		
		try {
			String command = pipeCommand;
			command = command.replace("%input_seeking%",String.valueOf(timeOffset));
			//defines the number of seconds to process
			// -t 10.000 e.g. specifies to process ten seconds 
			// from the specified time offset (which is often zero).
			if(numberOfSeconds>0){
				command = command.replace("%number_of_seconds%","-t " + String.valueOf(numberOfSeconds));
			} else {
				command = command.replace("%number_of_seconds%","");
			}
			command = command.replace("%resource%", resource);
			command = command.replace("%sample_rate%", String.valueOf(targetSampleRate));
			command = command.replace("%channels%","1");
			
			ProcessBuilder pb;
			pb= new ProcessBuilder(pipeEnvironment, pipeArgument , command);

			LOG.info("Starting piped decoding process for " + resource);
			LOG.info(" with command: " + command);
			final Process process = pb.start();
			
			final InputStream stdOut = new BufferedInputStream(process.getInputStream(), pipeBuffer){
				@Override
				public void close() throws IOException{
					super.close();
					// try to destroy the ffmpeg command after close
					process.destroy();
				}
			};
			
			//print std error if requested
			if(printErrorstream) {
				new ErrorStreamGobbler(process.getErrorStream(),LOG).start();
			}
			
			new Thread(new Runnable(){
				@Override
				public void run() {
					try {
						process.waitFor();
						LOG.info("Finished piped decoding process");
					} catch (InterruptedException e) {
						LOG.severe("Interrupted while waiting for decoding sub process exit.");
						e.printStackTrace();
					}
				}},"Decoding Pipe").start();
			return stdOut;
		} catch (IOException e) {
			LOG.warning("IO exception while decoding audio via sub process." + e.getMessage() );
			e.printStackTrace();
		}
		return null;
	}
	
	public double getDuration(final String resource) {
		double duration = -1;
		try {
			String command = "ffmpeg -i '%resource%'";
			
			command = command.replace("%resource%", resource);
					
			ProcessBuilder pb;
			pb= new ProcessBuilder(pipeEnvironment, pipeArgument , command);

			LOG.info("Starting duration command for " + resource);
			LOG.fine(" with command: " + command);
			final Process process = pb.start();
			
			final InputStream stdOut = new BufferedInputStream(process.getInputStream(), pipeBuffer){
				@Override
				public void close() throws IOException{
					super.close();
					// try to destroy the ffmpeg command after close
					process.destroy();
				}
			};
			
			ErrorStreamStringGlobber essg = new ErrorStreamStringGlobber(process.getErrorStream());
			essg.start();
			
			new Thread(new Runnable(){
				@Override
				public void run() {
					try {
						process.waitFor();
						LOG.info("Finished piped decoding process");
					} catch (InterruptedException e) {
						LOG.severe("Interrupted while waiting for decoding sub process exit.");
						e.printStackTrace();
					}
				}},"Decoding Pipe").run();
			
			String stdError = essg.getErrorStreamAsString();
			Pattern regex = Pattern.compile(".*\\s.*Duration:\\s+(\\d\\d):(\\d\\d):(\\d\\d)\\.(\\d\\d), .*", Pattern.DOTALL | Pattern.MULTILINE);
			Matcher regexMatcher = regex.matcher(stdError);
			if (regexMatcher.find()) {
				duration = Integer.valueOf(regexMatcher.group(1)) * 3600+ 
				Integer.valueOf(regexMatcher.group(2)) * 60+
				Integer.valueOf(regexMatcher.group(3)) * 1 +
				 Double.valueOf("." + regexMatcher.group(4) );
			}
		} catch (IOException e) {
			LOG.warning("IO exception while decoding audio via sub process." + e.getMessage() );
			e.printStackTrace();
		}
		return duration;
	}

	public void printBinaryInfo(){
		try {
			Process p = Runtime.getRuntime().exec(decoderBinaryAbsolutePath);
			BufferedReader input = new BufferedReader(new InputStreamReader(p.getErrorStream()));
			String line = null;
			while ((line = input.readLine()) != null) {
				System.out.println(line);
			}
			input.close();
			//int exitVal = 
			p.waitFor();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Constructs the target audio format. The audio format is one channel
	 * signed PCM of a given sample rate.
	 * 
	 * @param targetSampleRate
	 *            The sample rate to convert to.
	 * @return The audio format after conversion.
	 */
	public static TarsosDSPAudioFormat getTargetAudioFormat(int targetSampleRate) {
		TarsosDSPAudioFormat audioFormat = new TarsosDSPAudioFormat(TarsosDSPAudioFormat.Encoding.PCM_SIGNED, 
	        		targetSampleRate, 
	        		2 * 8, 
	        		1, 
	        		2 * 1, 
	        		targetSampleRate, 
	                ByteOrder.BIG_ENDIAN.equals(ByteOrder.nativeOrder()));
		 return audioFormat;
	}


	private boolean isAndroid(){
		try {
			// This class is only available on android
			Class.forName("android.app.Activity");
			System.out.println("Running on Android!");
			return true;
		} catch(ClassNotFoundException e) {
			//the class is not found when running JVM
			return false;
		}
	}


	private class ErrorStreamGobbler extends Thread {
		private final InputStream is;
		private final Logger logger;

		private ErrorStreamGobbler(InputStream is, Logger logger) {
			this.is = is;
			this.logger = logger;
		}

		@Override
		public void run() {
			try {
				InputStreamReader isr = new InputStreamReader(is);
				BufferedReader br = new BufferedReader(isr);
				String line = null;
				while ((line = br.readLine()) != null) {
					logger.info(line);
				}
			}
			catch (IOException ioe) {
				ioe.printStackTrace();
			}
		}
	}
	
	private class ErrorStreamStringGlobber extends Thread {
		private final InputStream is;
		private final StringBuilder sb;

		private ErrorStreamStringGlobber(InputStream is) {
			this.is = is;
			this.sb = new StringBuilder();
		}

		@Override
		public void run() {
			try {
				InputStreamReader isr = new InputStreamReader(is);
				BufferedReader br = new BufferedReader(isr);
				String line = null;
				while ((line = br.readLine()) != null) {
					sb.append(line);
				}
			}
			catch (IOException ioe) {
				ioe.printStackTrace();
			}
		}
		
		public String getErrorStreamAsString(){
			return sb.toString();
		}
	}
}