<?php
    
    /**
     * This file is part of the PHP Video Toolkit v2 package.
     *
     * @author Oliver Lillie (aka buggedcom) <publicmail@buggedcom.co.uk>
     * @license Dual licensed under MIT and GPLv2
     * @copyright Copyright (c) 2008-2014 Oliver Lillie <http://www.buggedcom.co.uk>
     * @package PHPVideoToolkit V2
     * @version 2.1.7-beta
     * @uses ffmpeg http://ffmpeg.sourceforge.net/
     */
     
    namespace PHPVideoToolkit;

    /**
     * This class provides generic data parsing for the output from FFmpeg from specific
     * media files. Parts of the code borrow heavily from Jorrit Schippers version of 
     * PHPVideoToolkit v 0.1.9.
     *
     * @access public
     * @author Oliver Lillie
     * @author Jorrit Schippers
     * @package default
     */
    class Media extends MediaParser
    {
        /**
         * Overwrite constants used in save, saveNonBlocking and getExecutionCommand
         */
        const OVERWRITE_FAIL = -1;
        const OVERWRITE_EXISTING = -2;
        const OVERWRITE_UNIQUE = -3;

        protected $_media_file_path;
        protected $_media_input_format;
        
        private $_blocking;

        public $last_error_message;
        public $error_messages;
        
        protected $_extract_segment;
        protected $_split_options;
        
        protected $_metadata;
        protected $_supported_meta_data;
        
        private $_output_path;
        private $_processing_path;
        
        private $_post_process_callbacks;
        
        protected $_require_d_in_output;
        
        protected $_process;

        protected $_ignore_format;

        /**
         * Constructs a media object.
         *
         * @access public
         * @author Oliver Lillie
         * @param string $media_file_path The file path of a media file.
         * @param Config $config A PHPVideoToolkit Config object
         * @param Format $input_format An input Format object
         */
        public function __construct($media_file_path, Config $config=null, Format $input_format=null)
        {
            parent::__construct($config, 'ffmpeg');
            
            if($media_file_path !== null)
            {
                $this->setMediaPath($media_file_path);
            }
            
            $this->setInputFormat($input_format);
            
            $this->last_error_message = null;
            $this->error_messages = array();
            
            $this->_extract_segment = array();
            $this->_split_options = array();
            $this->_metadata = array();
            
            $this->_output_path = null;
            $this->_processing_path = null;
            $this->_blocking = null;
            
            $this->_require_d_in_output = false;

            $this->_ignore_format = false;
            
            // @see http://multimedia.cx/eggs/supplying-ffmpeg-with-metadata/
            // @see http://wiki.multimedia.cx/index.php?title=FFmpeg_Metadata
            $this->_supported_meta_data = array(
                'title', 
                'date', 
                'author', 
                'album_artist', 
                'album', 
                'grouping', 
                'composer', 
                'year', 
                'track', 
                'comment', 
                'genre', 
                'copyright', 
                'description', 
                'synopsis', 
                'show', 
                'episode_id', 
                'network', 
                'lyrics', 
            );
            
            $this->_post_process_callbacks = array();
            
            $this->_process = new FfmpegProcessProgressable('ffmpeg', $this->_config);
        }

        protected function _validateMedia($media_type)
        {
            $type = $this->readType();
            return $media_type === $type;
        }
        
        /**
         * Sets the input Format of the input file.
         *
         * @access public
         * @author Oliver Lillie
         * @param Format $input_format 
         * @return self
         */
        public function setInputFormat(Format $input_format=null)
        {
//          create a default input format if none is set.
            if($input_format === null)
            {
                $format = null;
                $ext = pathinfo($this->_media_file_path, PATHINFO_EXTENSION);
                if(empty($ext) === false)
                {
//                  check we have a format we know about.
                    $format = Extensions::toBestGuessFormat($ext);
                }
                
                $this->_media_input_format = $this->getDefaultFormat(Format::INPUT, $format);
            }
            else
            {
                $this->_media_input_format = $input_format;
            }
            
            return $this;
        }
        
        /**
         * Returns the input format.
         *
         * @access public
         * @author Oliver Lillie
         * @return Format
         */
        public function getInputFormat()
        {
            return $this->_media_input_format;
        }

        public function getDefaultFormatClassName()
        {
            return 'Format';
        }

        /**
         * Returns the default (empty) input format for the type of media object this class is.
         *
         * @access public
         * @author Oliver Lillie
         * @param string $type Either input for an input format or output for an output format.
         * @return Format
         */
        public function getDefaultFormat($type, $format=null)
        {
            // $format is purposely ignored
            return $this->_getDefaultFormat($type, $this->getDefaultFormatClassName(), null);
        }
        
        /**
         * Returns a format class set to the specific output/input type.
         *
         * @access protected
         * @author Oliver Lillie
         * @param string $type Either input for an input format or output for an output format.
         * @param string $class_name The class name of the Format instance to return.
         * @package Format Returns an instance of a Format object or child class.
         */
        protected function _getDefaultFormat($type, $default_class_name, $format)
        {
            // TODO replace with reference to Format::getFormatFor
            if(in_array($type, array(Format::OUTPUT, Format::INPUT)) === false)
            {
                throw new \InvalidArgumentException('Unrecognised format type "'.$type.'".');
            }
            
//          check the requested class exists
            $class_name = '\\PHPVideoToolkit\\'.$default_class_name.(empty($format) === false ? '_'.ucfirst(strtolower($format)) : '');
            if(class_exists($class_name) === false)
            {
                $requested_class_name = $class_name;
                $class_name = '\\PHPVideoToolkit\\'.$default_class_name;
                if(class_exists($class_name) === false)
                {
                    throw new \InvalidArgumentException('Requested default format class does not exist, "'.($requested_class_name === $class_name ? $class_name : $requested_class_name.'" and "'.$class_name.'"').'".');
                }
            }
            
//          check that it extends from the base Format class.
            if($class_name !== '\\PHPVideoToolkit\\Format' && is_subclass_of($class_name, '\\PHPVideoToolkit\\Format') === false)
            {
                throw new \InvalidArgumentException('The class "'.$class_name.'" is not a subclass of \\PHPVideoToolkit\\Format.');
            }
            
            return new $class_name($type, $this->_config);
        }
        
        /**
         * Returns the real path of the media asset.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getMediaPath()
        {
            return $this->_media_file_path;
        }
        
        /**
         * Returns the real path of the media asset.
         *
         * @access public
         * @author Oliver Lillie
         * @return self
         */
        public function setMediaPath($media_file_path)
        {
            $real_file_path = realpath($media_file_path);
                
            if($real_file_path === false || is_file($real_file_path) === false)
            {
                throw new \InvalidArgumentException('The file "'.$media_file_path.'" cannot be found in \\PHPVideoToolkit\\Media::__construct.');
            }
            else if(is_readable($real_file_path) === false)
            {
                throw new \InvalidArgumentException('The file "'.$media_file_path.'" is not readable in \\PHPVideoToolkit\\Media::__construct.');
            }
            
            $this->_media_file_path = $real_file_path;
            
            return $this;
        }
        
        /**
         * Sets global meta data on the media. That being said "global" does not mean it sets the
         * meta data on the media streams, rather just the meta data on the container.
         *
         * @access public
         * @author Oliver Lillie
         * @param string $key 
         * @param string $value 
         * @param boolean $force 
         * @return self
         */
        public function setMetaData($key, $value=null, $force=false)
        {
            if(is_array($key) === true)
            {
                foreach ($key as $k => $v)
                {
                    $this->setMetaData($k, $v);
                }
                return $this;
            }
            
            if(empty($key) === true)
            {
                throw new \InvalidArgumentException('Empty metadata key. Metadata keys must be at least one character long.');
            }
            
//          check that meta key is supported by this format.
            $key = strtolower($key);
            if($force === false && in_array($key, $this->_supported_meta_data) === false)
            {
                throw new Exception('The metadata key "'.$key.'" cannot be set as it is not honoured by the muxer.');
            }
            
            $this->_metadata[$key] = $value;
            return $this;
        }
        
        /**
         * Removes all the global meta data. That being said "global" does not mean it removes the
         * meta data from the media streams, rather just the meta data on the container.
         *
         * @access public
         * @author Oliver Lillie
         * @return self
         */
        public function purgeMetaData()
        {
            $this->_metadata = array();

            $meta = $this->readGlobalMetaData();
            if(empty($meta) === false)
            {
                foreach ($meta as $key => $ignored)
                {
                    $this->setMetaData($key, '', true);
                }
            }
            
            return $this;
        }
        
        /**
         * Extracts a segment of the media object.
         *
         * @access public
         * @author Oliver Lillie
         * @param Timecode $from_timecode 
         * @param Timecode $to_timecode 
         * @param boolean $accurate If true then accuracy is prefered over performance.
         * @return Media
         */
        public function extractSegment(Timecode $from_timecode=null, Timecode $to_timecode=null, $accurate=false)
        {
//          check that a segment extract has not already been set
            if(empty($this->_extract_segment) === false)
            {
                throw new \LogicException('Extract segment options have already been set. You cannot call extractSegment more than once on a '.get_class($this).' object.');
            }
            
//          check that a split has already been set as if it has we can't extract a segment
//          however we can extract a segment, then split it.
            if(empty($this->_split_options) === false)
            {
                throw new \LogicException('You cannot extract a segment once '.get_class($this).'::split has been called. You can however extract a segment, the call '.get_class($this).'::split.');
            }
            
//          check the timecodes against the duration
            $duration = $this->readDuration();
            if($from_timecode !== null && $duration->total_seconds < $from_timecode->total_seconds)
            {
                throw new \InvalidArgumentException('The duration of the media is less than the starting timecode specified.');
            }
            else if($to_timecode !== null && $duration->total_seconds < $to_timecode->total_seconds)
            {
                throw new \InvalidArgumentException('The duration of the media is less than the end timecode specified.');
            }
            
            $this->_extract_segment = array(
                'preseek' => null,
                'seek' => null,
                'length' => null,
            );
            
//          if the from timecode is greater than say 15 seconds, we will stream seek to 15 seconds before the 
//          required extracted segment before the input to improve extract performance.
//          See http://ffmpeg.org/trac/ffmpeg/wiki/Seeking%20with%20FFmpeg
            $pre_input_stream_seek_offset = 0;
            $pre_input_stream_seek_adjustment = 15;
            if($from_timecode !== null)
            {
                if($accurate === false)
                {
                    if($from_timecode->total_seconds > $pre_input_stream_seek_adjustment)
                    {
                        $pre_input_stream_seek_offset = $from_timecode->total_seconds-$pre_input_stream_seek_adjustment;
                
                        $seek_timecode = new Timecode($pre_input_stream_seek_offset, Timecode::INPUT_FORMAT_SECONDS);
                        $this->_extract_segment['preseek'] = $seek_timecode;
                    }

//                  if we have a pre input stream seek then input video is then offset by that ammount
                    if($pre_input_stream_seek_offset > 0)
                    {
                        $from_timecode = new Timecode($pre_input_stream_seek_adjustment, Timecode::INPUT_FORMAT_SECONDS);
                    }
                }

//              then seek the exact position after input
                $begin_position = $from_timecode->getTimecode('%hh:%mm:%ss.%ms', false);
                $this->_extract_segment['seek'] = $from_timecode;
            }
            else
            {
                $from_timecode = new Timecode(0, Timecode::INPUT_FORMAT_SECONDS);
            }

//          then add the number of seconds to export for if there is an end timecode.
            if($to_timecode !== null)
            {
//              if we have a pre input stream seek then input video is then offset by that ammount
                if($pre_input_stream_seek_offset > 0)
                {
                    $to_timecode = new Timecode($to_timecode->total_seconds-$pre_input_stream_seek_offset, Timecode::INPUT_FORMAT_SECONDS);
                }
                $this->_extract_segment['length'] = $to_timecode->total_seconds - $from_timecode->total_seconds;
            }
            
            return $this;
        }
        
        /**
         * Splits (aka ffmpeg segment) the output into multiple files.
         *
         * @access public
         * @author Oliver Lillie
         * @return self
         */
        public function split($split_by, $time_delta=0, $output_list_path=null)
        {
//          check that segment is available to ffmpeg
            $ffmpeg = new FfmpegParser($this->_config);
            $formats = $ffmpeg->getFormats();
            if(isset($formats['segment']) === false)
            {
                throw new Exception('Unable to split media as the ffmpeg option "-segment" is not supported by your version of ffmpeg.');
            }
            
//          check to see if split options are already set
            if(empty($this->_split_options) === false)
            {
                throw new \LogicException('Split options have already been set. You cannot call split more than once on a '.get_class($this).' object.');
            }
            
            $this->_split_options = array();
            $duration = $this->readDuration();
            
//          check the split by
            if(empty($split_by) === true)
            {
                throw new \InvalidArgumentException('The split by value is empty, in \\PHPVideoToolkit\\'.get_class($this).'::split');
            }
//          if we have an array, it's either timecodes (seconds) or integers (frames)
            else if(is_array($split_by) === true)
            {
//              we check to see if we have a timecode object, if we do then we are spliting at exact points
                if(is_object($split_by[0]) === true)
                {
                    $times = array();
                    foreach ($split_by as $key=>$timecode)
                    {
                        if(get_class($timecode) !== 'PHPVideoToolkit\Timecode')
                        {
                            throw new \InvalidArgumentException('The split by timecode specified in index '.$key.' is not a \\PHPVideoToolkit\\Timecode object.');
                        }
                        
//                      check the timecode against the total number of seconds in the media duration.
                        $seconds = $timecode->total_seconds;
                        if($seconds > $duration->total_seconds)
                        {
                            throw new \InvalidArgumentException('The split by timecode specified in index '.$key.' is greater than the duration of the media ('.$duration->total_seconds.' seconds).');
                        }
                        
                        array_push($times, $seconds);
                    }
                
                    $this->_split_options['segment_times'] = implode(',', $times);
                }
//              otherwise we are spliting at frames
                else
                {
                    $times = array();
                    foreach ($split_by as $key=>$integer)
                    {
                        if(is_int($integer) === false)
                        {
                            throw new \InvalidArgumentException('The split by frame number specified in index '.$key.' is not an integer.');
                        }
                        
                        
//                      check the frame number against the total number of frames in the media duration.
                        // TODO total frame rate comparison
                        // $seconds = ceil($timecode->total_seconds);
                        // if($seconds > $duration->total_seconds)
                        // {
                        //  throw new Exception('The split by timecode specified in index '.$key.' is greater than the duration of the media ('.$duration->total_seconds.' seconds).');
                        // }
                        // 
                        array_push($times, $integer);
                    }
                
                    $this->_split_options['segment_frames'] = implode(',', $times);
                }
            }
//          anything else is treated as an integer of which each split is the same length.
            else 
            {
                if($split_by < 1)
                {
                    throw new \InvalidArgumentException('The split by value must be >= 1, in \\PHPVideoToolkit\\'.get_class($this).'::split');
                }
                        
//              check the split time against the total number of seconds in the media duration.
                if($split_by > $duration->total_seconds)
                {
                    throw new \InvalidArgumentException('The split by value is greater than the duration of the media ('.$duration->total_seconds.' seconds).');
                }
                        
                $this->_split_options['segment_time'] = (int) $split_by;
            }

//          check time delta
            if($time_delta < 0)
            {
                throw new \InvalidArgumentException('The time delta specified "'.$time_delta.'", in \\PHPVideoToolkit\\'.get_class($this).'::split must be >= 0');
            }
            else if($time_delta > 0)
            {
                $this->_split_options['segment_time_delta'] = (float) $time_delta;
            }
            
//          check the directory that contains the output list is writeable
            if(empty($output_list_path) === false)
            {
                $output_list = realpath($output_list_path);
                $output_list_dir = dirname($output_list);
                if(is_dir($output_list_dir) === false)
                {
                    throw new \InvalidArgumentException('The directory for the output list file "'.$output_list_path.'" does not exist, in \\PHPVideoToolkit\\'.get_class($this).'::split');
                }
                else if(is_writeable($output_list_dir) === false)
                {
                    throw new \InvalidArgumentException('The directory for the output list file "'.$output_list_path.'" is not writeable, in \\PHPVideoToolkit\\'.get_class($this).'::split');
                }
                
                $this->_split_options['segment_list'] = $output_list_path;
            }
            
//          mark that we require a %d (or in phpvideotoolkits case %index or %timecode) in the file name output as multiple files will be outputed.
            $this->_require_d_in_output = true;
            
            return $this;
        }
        
        /**
         * Returns the FfmpegProcess object by reference.
         *
         * @access public
         * @author Oliver Lillie
         * @return FfmpegProcess
         */
        public function &getProcess()
        {
            return $this->_process;
        }
        
        /**
         * Gets the final length of the output based upon the extraction/split commands
         * If the output is bing split, then an array will be returned, otherwise a float.
         * IMPORTANT! The duration(s) returned are based of various configurable options
         * and the resulting output by ffmpeg may vary slightly.
         *
         * @access public
         * @author Oliver Lillie
         * @return mixed
         */
        public function getEstimatedFinalDuration()
        {
            $duration = $this->readDuration();
            $duration_seconds = $duration->total_seconds;
            
//          as extractSegment must always be called before split therefore we process the segment options first
            if(empty($this->_extract_segment) === false)
            {
                if(empty($this->_extract_segment['length']) === false)
                {
                    $duration_seconds = $this->_extract_segment['length'];
                }
                else
                {
                    if(empty($this->_extract_segment['preseek']) === false)
                    {
                        $duration_seconds -= $this->_extract_segment['preseek']->total_seconds;
                    }
                    if(empty($this->_extract_segment['seek']) === false)
                    {
                        $duration_seconds -= $this->_extract_segment['seek']->total_seconds;
                    }
                }
                
                $duration = new Timecode($duration_seconds, Timecode::INPUT_FORMAT_SECONDS);
            }
            
//          do we have any split options?
            if(empty($this->_split_options) === false)
            {
                // TODO
                // segment_time, a single time in seconds
                // segment_times, multiple times in seconds
                // segment_frames, frames
            }
            
            return $duration;
        }

        /**
         * Returns a string value of a portable identifier used in conjunction with ProgressHandlerPortable.
         * WARNING. If this function is called it automatically disables the garbage collection of the ExceBuffer.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getPortableId()
        {
            return $this->_process->getPortableId().'.'.$this->getEstimatedFinalDuration()->total_seconds;
        }
        
        /**
         * Registers an output post process function, that is called after output has been generated.
         * It is important to note, that when an output post process is registered, the conversion
         * must then become blocking.
         * 
         * @access protected
         * @author Oliver Lillie
         * @param Function $callback 
         * @return self
         */
        public function registerOutputPostProcess($callback, $args=array())
        {
            if(is_callable($callback) === false)
            {
                throw new \InvalidArgumentException('The callback "'.$callback.'" is not callable.');
            }
            if(is_array($args) === false)
            {
                throw new \InvalidArgumentException('The $args argument is not an array.');
            }
            array_push($this->_post_process_callbacks, array($callback, $args));

//          if a callback has been supplied then the process becomes blocking and must be set.      
            $this->_blocking = true;
            
            return $this;
        }
        
        /**
         * The callback intentionally public, but should be regarded as protected that is used
         * to post process the output of a save command.
         *
         * @access protected
         * @author Oliver Lillie
         * @return mixed
         */
        public function _postProcessOutput(FfmpegProcess $process)
        {
            $output = $process->completeProcess();
            if(empty($this->_post_process_callbacks) === false)
            {
                foreach ($this->_post_process_callbacks as $callback)
                {
                    $args = $callback[1];
                    array_unshift($args, $output, $this);
                    $output = call_user_func_array($callback[0], $args);
                }
            }

            return $output;
        }

        /**
         * Converts a string path and output format into a MultiOutput object.
         *
         * @access protected
         * @author Oliver Lillie
         * @param  string $save_path The string based path of a MultiObject.
         * @param  Format $output_format An output format object.
         * @return MultiObject
         */
        protected function _convertOutputPathToMultiOutput($save_path=null, Format $output_format=null)
        {
            $class = 'PHPVideoToolkit\MultiOutput'; // prevents unneccesary autoload.
            if($save_path instanceof $class === true)
            {
                return $save_path;
            }
            $multi_output = new MultiOutput($this->_config);
            $multi_output->setDefaultOutputFormat($this->getDefaultFormatClassName());
            $multi_output->addOutput($save_path, $output_format);
            return $multi_output;
        }
        
        /**
         * Saves any changes to the media file to the given save path.
         * IMPORTANT! This save blocks PHP execution, meaning that once called, the PHP interpretter
         * will NOT continue untill the video/audio/media file(s) have been transcoded.
         *
         * @access public
         * @author Oliver Lillie
         * @param MultiOutput $save_path If a string then it is treated as a single output and the argument is the output path
         *  of the generated file, otherwise if a PHPVideoToolkit\MultiOutput object is given then we treat the output
         *  as multiple output.
         * @param Format $output_format It is the output format for the saved file.
         * @param string $overwrite One of the following constants determining the overwrite status of the save.
         *  Media::OVERWRITE_FAIL - the save call will fail with an excetion if the save path already exists.
         *  Media::OVERWRITE_EXISTING - the save call will overwrite any existing file with the same name.
         *  Media::OVERWRITE_UNIQUE - the save call will augment the save path with a unique hash so that if a file with
         *      the same name exists then there is no overwrite.
         * @param ProgressHandlerAbstract $progress_handler The progress handler object to supply to the save process.
         * @return mixed If the blocking mode of the process is set to block, the it returns a new 
         *  Media object on a successfull completion, otherwise an exception is thrown. If the blocking
         *  mode is non blocking then the underlying FfmpegProcess is returned.
         */
        public function save($save_path=null, Format $output_format=null, $overwrite=Media::OVERWRITE_FAIL, ProgressHandlerAbstract &$progress_handler=null)
        {
            $this->_saveAddInputFormatCommands($this->_media_input_format);

//          set the input files.
            $this->_process->setInputPath($this->_media_file_path);

//          loop and process the save path to multioutput so we can loop
            $multi_output = $this->_convertOutputPathToMultiOutput($save_path, $output_format);
            $index = 0;
            foreach ($multi_output as $save_path => $output_format)
            {
//              increment the output index so the process moves on to the next process to build if the loop continues.
                $this->_process->setOutputIndex($index);
                $index += 1;

//              pre process all of the common functionality and pre process the output format.
                $this->_savePreProcess($output_format, $save_path, $overwrite);

//              add the commands from the output format to the exec buffer
//              NOTE; this cannot be done in _savePreProcess as it must be done after, to ensure all the subclass
//              _savePreProcess functionality and main media class functionality is properly executed.
                $this->_saveAddOutputFormatCommands($output_format);

//              update the output path as the processing path?
                $this->_process->setOutputPath($this->_processing_path);
            }

//          set the progress handler 
            if($progress_handler !== null)
            {
                $progress_handler->setTotalDuration($this->getEstimatedFinalDuration());
                $this->_process->attachProgressHandler($progress_handler);
            }
            
//          exec the buffer
//          set the blocking mode
//          and execute the ffmpeg process.
            $buffer = $this->_process->setOutputPath($this->_processing_path)
                                     ->getExecBuffer()
                                     ->setBlocking($this->_blocking === null ? true : $this->_blocking);

            $process = $this->_process;
            $callback = array($this, '_postProcessOutput');
            $buffer->registerCompletionCallback(
                function() use ($callback, $process)
                {
                    call_user_func($callback, $process);
                }
            );

            if($progress_handler !== null && $progress_handler->getNonBlockingCompatibilityStatus() === false)
            {
                $buffer->execute(
                    function($exec_buffer, $null, $completed) use ($progress_handler, $callback, $process)
                    {
                        if($progress_handler !== null)
                        {
                            $progress_handler->callback();
                        }
                    }
                );
            }
            else
            {
                $buffer->execute();
            }

//          just return the process if the process
            return $this->_process;
        }
        
        /**
         * Saves any changes to the media file to the given save path.
         * IMPORTANT! This save does NOT block PHP execution, meaning that once called, the PHP interpretter
         * will IMMEDIATELY continue. PHP will continue, in all likelyhood, exit before the ffmpeg has
         * completed the transcoding of any output.
         *
         * If you need to monitor the output for completion or processing then you can supplied a progress handler to
         * return information about the process.
         * 
         * @access public
         * @author Oliver Lillie
         * @param string $save_path 
         * @param Format $output_format 
         * @param string $overwrite 
         * @return boolean If the command is sent without error then true is returned, otherwise false.
         *  The last error message is set to Media->last_error_message. A full list of error messages is 
         *  available through Media->error_messages.
         */
        public function saveNonBlocking($save_path=null, Format $output_format=null, $overwrite=self::OVERWRITE_FAIL, ProgressHandlerAbstract &$progress_handler=null)
        {
//          check to see if the blocking mode has already been set to true. If it has we cannot save
//          non-blocking and must trigger error.
            if($this->_blocking === true)
            {
                throw new \LogicException('The blocking mode has been enabled by a function that you have enabled, or a Format that you have supplied. As a result you cannot use saveNonBlocking() and must use the blocking save method save() instead.');
            }
            
//          set the non blocking of the exec process
            $this->_blocking = false;
            
//          set the progress handler 
            if($progress_handler !== null)
            {
//              because only certain types of handlers are compatible with non blocking saves we need to check for compatibility.
                if($progress_handler->getNonBlockingCompatibilityStatus() === false)
                {
                    throw new \LogicException('The progress handler given is not compatible with a non blocking save. This typically means that you have supplied a callback function in the constructor of the progress handler. Any progress handler with a supplied callback blocks PHP. Instead you should call $handler->probe() after the saveNonBlocking function call to get the progress of the encode.');
                }
            }
            
            return $this->save($save_path, $output_format, $overwrite, $progress_handler);
        }
        
        /**
         * All three save functions, save, saveNonBlocking and getExecutionCommand have common things they 
         * have to do before they are processed. This function contains those execution "warm-up" procedures.
         *
         * @access protected
         * @author Oliver Lillie
         * @param Format $output_format 
         * @param string $save_path 
         * @param string $overwrite 
         * @return void
         */
        protected function _savePreProcess(Format &$output_format=null, &$save_path, $overwrite)
        {
//          do some processing on the input format
            // $this->_processInputFormat();
            
//          if the save path is null then we are overwriting the existing media file.
            if($save_path === null)
            {
                $overwrite = self::OVERWRITE_UNIQUE;
                $save_path = $this->_media_file_path;
            }
            
//          do some pre processing of the output format
            $this->_processOutputFormat($output_format, $save_path, $overwrite);

//          check the save path.
            $has_timecode_or_index = false;
            $has_timecode = false;
            $has_index = false;
            $basename = basename($save_path);
            $save_dir = dirname($save_path);
            $save_dir = realpath($save_dir);
            if($save_dir === false || is_dir($save_dir) === false)
            {
                throw new \InvalidArgumentException('The directory that the output is to be saved to, "'.$save_dir.'" does not exist.');
            }
            else if(is_writeable($save_dir) === false || is_readable($save_dir) === false)
            {
                throw new \RuntimeException('The directory that the output is to be saved to, "'.$save_dir.'" is not read-writeable.');
            }
//          check to see if we have a split output name.
//          although this is technically still allowed by ffmpeg, phpvideotoolkit has depreciated %d in favour of its own %index
            else if(preg_match('/\%([0-9]*)d/', $save_path) > 0)
            {
                throw new \InvalidArgumentException('The output file appears to be using FFmpeg\'s %d notation for multiple file output. The %d notation is depreciated in PHPVideoToolkit in favour of the %index or %timecode notations.');
            }
//          if a %index or %timecode output is added then we can't check for exact file existence
//          we can however check for possible interfering matches.
            else if(($has_timecode_or_index = (preg_match('/\%(timecode|[0-9]*(index))/', $save_path, $matches) > 0)))
            {
                $has_timecode = $matches[1] === 'timecode';
                $has_index = isset($matches[2]) === true && $matches[2] === 'index';
            }
//          check to see if we have to have a timecode or index in the output and that we actually have one.
            else if($has_timecode_or_index === false && $this->_require_d_in_output === true)
            {
                throw new \InvalidArgumentException('It is required that either "%timecode" or "%index" to the save path as more that one file is expected be outputed. When using %index, it is possible to specify a number to be padded with a specific amount of 0s. For example adding %5index.jpg will output files like 00001.jpg, 00002.jpg etc.');
            }
//          otherwise check that a file exists and the overrwite status of the request.
            else
            {
                if(is_file($save_dir.DIRECTORY_SEPARATOR.$basename) === true && (empty($overwrite) === true || $overwrite === self::OVERWRITE_FAIL))
                {
                    throw new \LogicException('The output file already exists and overwriting is disabled.');
                }
                else if(is_file($save_dir.DIRECTORY_SEPARATOR.$basename) === true && $overwrite === self::OVERWRITE_EXISTING && is_writeable($save_dir.DIRECTORY_SEPARATOR.$basename) === false)
                {
                    throw new \LogicException('The output file already exists, overwriting is enabled however the file is not writable.');
                }
            }
            $save_path = $save_dir.DIRECTORY_SEPARATOR.$basename;
            
//          check for a recognised output format, and if one is not supplied
//          then check the a the format has been set in the output format, if not through an error and exit
            $format = false;
            $ext = pathinfo($save_path, PATHINFO_EXTENSION);
            if(empty($ext) === false)
            {
//              check we have a format we know about.
                $format = Extensions::toBestGuessFormat($ext);
            }

//          if we still don't have a format, check from the output format.
            if(!$format)
            {
                $options = $output_format->getFormatOptions();
                if(isset($options['format']) === false || empty($options['format']) === true)
                {
                    if(empty($ext) === true)
                    {
                        throw new \LogicException('The output path of the file extension has not be given. Please either set a file extension of the output path - or - call setFormat() on the output format to set the format of the output media.');
                    }
                    throw new \LogicException('Un-recognised file extension. Please call setFormat() on the output format to set the format of the output media.');
                }
            }
            
//          process the overwrite status
            switch($overwrite)
            {
                case self::OVERWRITE_EXISTING :
                    $this->_process->addCommand('-y');
                    break;
                    
//              insert a unique id into the save path
                case self::OVERWRITE_UNIQUE :
                    $pathinfo = pathinfo($save_path);
                    $save_path = $pathinfo['dirname'].DIRECTORY_SEPARATOR.$pathinfo['filename'].'-u_'.Str::generateRandomString().'.'.$pathinfo['extension'];
                    break;

//              this is purely in case the media object is "re-used", as if the command is already been set to overwrite
//              but the subsequent save is not, we must remove any previous command so we don't get unwanted overwrites.
                default : 
                    $this->_process->removeCommand('-y');
            }
            $this->_output_path = 
            $this->_processing_path = $save_path;
            
//          check to see if we are extracting a segment
//          It is important that we are the extract commands before any segmenting, so that if we are extracting
//          a segment then spliting the file everything goes as expected.
            if(empty($this->_extract_segment) === false)
            {
                if(empty($this->_extract_segment['preseek']) === false)
                {
                    $this->_process->addPreInputCommand('-ss', $this->_extract_segment['preseek']->getTimecode('%hh:%mm:%ss.%ms', false));
                }
                if(empty($this->_extract_segment['seek']) === false)
                {
                    $this->_process->addCommand('-ss', $this->_extract_segment['seek']->getTimecode('%hh:%mm:%ss.%ms', false));
                }
                if(empty($this->_extract_segment['length']) === false)
                {
                    $this->_process->addCommand('-t', $this->_extract_segment['length']);
                }
            }
            
//          if we are splitting the output
            if(empty($this->_split_options) === false)
            {
//              if so check that a timecode or index has been set
                if($has_timecode_or_index === false)
                {
                    $pathinfo = pathinfo($save_path);
                    $save_path = 
                    $this->_output_path = $pathinfo['dirname'].DIRECTORY_SEPARATOR.$pathinfo['filename'].'-%timecode.'.$pathinfo['extension'];
                    $has_timecode_or_index = true;
                }
                
//              if we are splitting we need to add certain commands to make it work.
//              one of those is -map 0. Also note that video and audio objects additionally set their own
//              codecs if not supplied, in their related class function _savePreProcess
                // TODO this may need to be changed dependant on the number of mappings.
                $this->_process->addCommand('-map', '0');
                
                 // -acodec copy 
                 // -vcodec copy 
                
//              we must do this via add command rather than setFormat as it rejects the segment format.
                $this->_process->addCommand('-f', 'segment');
                foreach ($this->_split_options as $command => $arg)
                {
                    $this->_process->addCommand('-'.$command, $arg);
                }

//              get the output commands and augment with the final output options.
                $options = $output_format->getFormatOptions();

//              set the split format if an output format has already been set. and remove from the output format so that multiple "-f" formats are not given to the buffer
                if(empty($options['format']) === false)
                {
                    $this->_ignore_format = true;
                    $this->_process->addCommand('-segment_format', $options['format']);
                }
                
                // TODO add time delta and segment_list
            }
            
//          check to see if we have any global meta
            if(empty($this->_metadata) === false)
            {
                $meta_string = array();
                foreach ($this->_metadata as $key => $value)
                {
                    $this->_process->addCommand('-metadata:g', $key.'='.$value.'', true);
                }
            }

//          if we have a timecode or index based path we then have to supply a temporary processing path so that
//          we can perform the rename to timecode and index after they items have been transcoded by ffmpeg.
            if($has_timecode_or_index === true)
            {
                $processing_path = $this->_output_path;
                if($has_timecode === true)
                {
//                  we build the timecode and frame rate data into the output if we use %timecode
//                  that way we can always reconstruct the timecode even from another script or process.
                    
//                  get the frame rate of the export. we give priority to "-r" as this is the output of the object if already set somewhere,
//                  otherwise we revert to the output format setting,
//                  then fallback to the to the framerate of the current video component
                    if(($frame_rate = $this->_process->getCommand('-r')) === false)
                    {
                        $options = $output_format->getFormatOptions();
                        if(empty($options['video_frame_rate']) === false)
                        {
                            $frame_rate = $options['video_frame_rate'];
                        }
                        else
                        {
                            $data = $this->readVideoComponent();
                            if(isset($data['frames']) === true && isset($data['frames']['rate']) === true)
                            {
                                $frame_rate = $data['frames']['rate'];
                            }
                        }
                    }
                    if($frame_rate <= 0)
                    {
                        throw new \RuntimeException('Unable to access the output frame rate value and as a result we cannot generate a timecode based filename output.');
                    }
                    else if(preg_match('/[0-9]+\/[0-9]+/', $frame_rate) > 0)
                    {
                        $frame_rate = explode('/', $frame_rate);
                        $frame_rate = $frame_rate[0]/$frame_rate[1];
                    }
                    
//                  get the starting offset of the export
                    $offset = '0';
                    $stream_seek_input = $this->_process->getPreInputCommand('-ss');
                    if($stream_seek_input !== false)
                    {
                        $offset += Timecode::parseTimecode($stream_seek_input, '%hh:%mm:%ss.%ms');
                    }
                    $stream_seek_output = $this->_process->getCommand('-ss');
                    if($stream_seek_output !== false)
                    {
                        $offset += Timecode::parseTimecode($stream_seek_output, '%hh:%mm:%ss.%ms');
                    }
                    
//                  apply rounding to get a float of precise length
                    $offset = round($offset, 2);
                    
                    $processing_path = preg_replace('/%timecode/', '.%12d.'.$frame_rate.'_'.$offset.'._t.', $processing_path);
                }
                if($has_index === true)
                {
                    $processing_path = preg_replace('/%([0-9]*)index/', '.%$1d._i.', $processing_path);
                }
                
//              add a unique identifier to the processing path to prevent overwrites.
                $pathinfo = pathinfo($processing_path);
                $this->_processing_path = $pathinfo['dirname'].DIRECTORY_SEPARATOR.$pathinfo['filename'].'._u.'.Str::generateRandomString().'.u_.'.$pathinfo['extension'];
            }
        }
        
        /**
         * Process the output format just before the it is compiled into commands.
         *
         * @access protected
         * @author Oliver Lillie
         * @param Format &$output_format 
         * @return void
         */
        protected function _processOutputFormat(Format &$output_format=null, &$save_path, $overwrite)
        {
//          check to see if we have been set and output format, if not generate an empty one.
            if($output_format === null)
            {
                $format = null;
                $ext = pathinfo($save_path, PATHINFO_EXTENSION);
                if(empty($ext) === false)
                {
                    $format = Extensions::toBestGuessFormat($ext);
                }
                $output_format = $this->getDefaultFormat(Format::OUTPUT, $format);
            }
            
//          set the media into the format object so that we can update the format options that
//          require a media object to process.
            $output_format->setMedia($this)
                          ->updateFormatOptions($save_path, $overwrite);
        }
        
        /**
         * Adds the output format commands from the Format object to the FfmpegProcess
         *
         * @access protected
         * @author Oliver Lillie
         * @param Format $output_format 
         * @return void
         */
        protected function _saveAddOutputFormatCommands(Format $output_format=null)
        {
            if($output_format !== null)
            {
                $commands = $output_format->getCommandsHash();
                if(empty($commands) === false)
                {
                    foreach ($commands as $key => $value)
                    {
                        // this is a special consideration as if the format is being segmented then any output
                        // format must be ignored as it is already being set to -segment_format.
                        if($this->_ignore_format === true && $key === '-f')
                        {
                            continue;
                        }
                        $this->_process->addCommand($key, $value);
                    }
                }
            }
        }
        
        /**
         * Adds the input format commands from the Format object to the FfmpegProcess
         *
         * @access protected
         * @author Oliver Lillie
         * @param Format $output_format 
         * @return void
         */
        protected function _saveAddInputFormatCommands(Format $input_format=null)
        {
            if($input_format !== null)
            {
                $commands = $input_format->getCommandsHash();
                if(empty($commands) === false)
                {
                    foreach ($commands as $key => $value)
                    {
                        $this->_process->addPreInputCommand($key, $value);
                    }
                }
            }
        }
        
//      The below functions override the MediaParser functions so to automatically provide
//      the media_file_path each time.
        
        /**
         * Returns the information about a specific media file.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return array
         */
        public function read($read_from_cache=true)
        {
            return parent::getFileInformation($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns the files duration as a Timecode object if available otherwise returns false.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns a Timecode object if the duration is found, otherwise returns null.
         */
        public function readDuration($read_from_cache=true)
        {
            return parent::getFileDuration($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns the files duration as a Timecode object if available otherwise returns false.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns a Timecode object if the duration is found, otherwise returns null.
         */
        public function readGlobalMetaData($read_from_cache=true)
        {
            return parent::getFileGlobalMetadata($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns the files bitrate if available otherwise returns -1.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns the bitrate as an integer if available otherwise returns -1.
         */
        public function readBitrate($read_from_cache=true)
        {
            return parent::getFileBitrate($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns the start point of the file as a Timecode object if available, otherwise returns null.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns a Timecode object if the start point is found, otherwise returns null.
         */
        public function readStart($read_from_cache=true)
        {
            return parent::getFileStart($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns the start point of the file as a Timecode object if available, otherwise returns null.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns a string 'audio' or 'video' if media is audio or video, otherwise returns null.
         */
        public function readType($read_from_cache=true)
        {
            return parent::getFileType($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns any video information about the file if available.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns an array of found data, otherwise returns null.
         */
        public function readVideoComponent($read_from_cache=true)
        {
            return parent::getFileVideoComponent($this->_media_file_path, $read_from_cache);
        }

        /**
         * Returns mean and max volume information of the file.
         *
         * @access public
         * @author Samar Rizvi
         * @param boolean $read_from_cache
         * @return mixed Returns an array of found data, otherwise returns null.
         */
        public function readVolumeComponent($read_from_cache=true)
        {
            return parent::getFileVolumeComponent($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns any audio information about the file if available.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns an array of found data, otherwise returns null.
         */
        public function readAudioComponent($read_from_cache=true)
        {
            return parent::getFileAudioComponent($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns a boolean value determined by the media having an audio channel.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return boolean
         */
        public function readHasAudio($read_from_cache=true)
        {
            return parent::getFileHasAudio($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns a boolean value determined by the media having a video channel.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return boolean
         */
        public function readHasVideo($read_from_cache=true)
        {
            return parent::getFileHasVideo($this->_media_file_path, $read_from_cache);
        }
        
        /**
         * Returns the raw data provided by ffmpeg about a file.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $read_from_cache 
         * @return mixed Returns false if no data is returned, otherwise returns the raw data as a string.
         */
        public function readRawInformation($read_from_cache=true)
        {
            return parent::getFileRawInformation($this->_media_file_path, $read_from_cache);
        }
    }
