<?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 Video extends Media
    {
        protected $_extracting_frames;
        protected $_extracting_frame;
        
        protected $_extracting_audio;
        
        public function __construct($video_file_path, Config $config=null, VideoFormat $video_input_format=null, $ensure_video_file=true)
        {
            parent::__construct($video_file_path, $config, $video_input_format);
            
//          validate this media file is a video file
            if($ensure_video_file === true && $this->_validateMedia('video') === false)
            {
                throw new \LogicException('You cannot use an instance of '.get_class($this).' for "'.$video_file_path.'" as the file is not a video file. It is reported to be a '.$this->readType());
            }
            
            $this->_extracting_frames = false;
            $this->_extracting_frame = false;
            
            $this->_extracting_audio = false;
        }
        
        public function getDefaultFormatClassName()
        {
            return 'VideoFormat';
        }
        
        public function extractAudio()
        {
            $this->_extracting_audio = true;
            
            return $this;
        }
        
        public function extractFrame(Timecode $timecode)
        {
            if($this->_extracting_frames === true)
            {
                throw new \LogicException('You cannot extract multiple frames and then extract a single frame in the same execution chain.');
            }
            
            $this->extractSegment($timecode, $timecode);
            $this->_extracting_frame = true;
            
            return $this;
        }
        
        public function extractFrames(Timecode $from_timecode=null, Timecode $to_timecode=null, $force_frame_rate=null)
        {
            if($this->_extracting_frame === true)
            {
                throw new \LogicException('You cannot a single frame and then extract multiple frames in the same execution chain.');
            }
            if($force_frame_rate !== null && is_int($force_frame_rate) === false && is_float($force_frame_rate) === false && (is_string($force_frame_rate) === true && preg_match('/[0-9]+\/[0-9]+/', $force_frame_rate) === 0))
            {
                throw new \InvalidArgumentException('If setting a forced frame rate please make sure it is either an integer, a float or a string in the "1/xxx" format (i.e. 1/60 = 1 frame every 60 seconds).');
            }
            
            if($from_timecode !== null || $to_timecode !== null)
            {
                $this->extractSegment($from_timecode, $to_timecode);
            }
            $this->_extracting_frames = $force_frame_rate === null ? true : $force_frame_rate;
            
            return $this;
        }
        
        protected function _savePreProcess(Format &$output_format=null, &$save_path, $overwrite, ProgressHandlerAbstract &$progress_handler=null)
        {
            parent::_savePreProcess($output_format, $save_path, $overwrite, $progress_handler);
            
//          if we are splitting the output
            if(empty($this->_split_options) === false)
            {
                $options = $output_format->getFormatOptions();
            
//              if we are splitting we need to add certain commands to make it work.
//              for video, we need to ensure that both audio and video codecs are set.
                if($this->readHasAudio() === true && empty($options['audio_codec']) === true)
                {
                    $data = $this->readAudioComponent(); 
                    // TODO checks for empty name
                    // TODO check for encode availability
                    $this->_process->addCommand('-acodec', $data['codec']['name']);
                }
                if(empty($options['video_codec']) === true)
                {
                    $data = $this->readVideoComponent();
                    // TODO checks for empty name
                    // TODO check for encode availability
                    $this->_process->addCommand('-vcodec', $data['codec']['name']);
                }
            }
        }
        
        /**
         * Process the output format just before the it is compiled into commands.
         *
         * @access public
         * @author Oliver Lillie
         * @param Format &$output_format 
         * @return void
         */
        protected function _processOutputFormat(Format &$output_format=null, &$save_path, $overwrite)
        {
            parent::_processOutputFormat($output_format, $save_path, $overwrite);
            
//          turn off the related options.
            if($this->_extracting_audio === true)
            {
                $output_format->disableVideo();
            }
            if($this->_extracting_frames !== false || $this->_extracting_frame === true)
            {
                $output_format->disableAudio();
            }
            
//          check for conflictions with having both audio and video disabled.
            $options = $output_format->getFormatOptions();
            if($options['disable_audio'] === true && $options['disable_video'] === true)
            {
                throw new \LogicException('Unable to process output format to send to ffmpeg as both audio and video are disabled.');
            }

//          process the frame extraction options onto the video output format.
            if($this->_extracting_frame === true)
            {
//              check for conflictions with a single frame extraction and a setting of the video frame rate output format
                if($options['video_frame_rate'] !== null && $options['video_frame_rate'] !== 1)
                {
                    // TODO change to log a warning instead
                    throw new \LogicException('You are attempting to extract a frame, however you have also specified a frame rate in the video output format. When extracting a frame you cannot set the frame rate of the output format. If you wish to extract multiple frames please use the extractFrames function instead.');
                }
//              check for conflictions with a max frames setting
                else if($options['video_frame_rate'] !== null && $options['video_frame_rate'] !== 1)
                {
                    // TODO change to log a warning instead
                    throw new \LogicException('You are attempting to extract a frame, however you have also specified a max frame limit in the video output format. When extracting a frame you cannot set the max frame limit of the output format. If you wish to extract multiple frames please use the extractFrames function instead.');
                }
                
                $output_format->setVideoFrameRate(1);
                $output_format->setVideoMaxFrames(1);
            }
//          check to see if we are extracting multiple frames. if so check save path and set related options.
            else if($this->_extracting_frames !== false)
            {
//              if the extracting frames value is not true, then it is a forced frame rate.
                if($this->_extracting_frames !== true)
                {
//                  check for conflictions with a an output format frame raete being set.
                    if($options['video_frame_rate'] !== null)
                    {
                        // TODO change to log a warning instead
                        throw new \LogicException('You are attempting to extract multiple frames and force a frame rate, however you have also specified a frame rate in the video output format. When extracting multiple frames whilst specifying a forced frame rate you cannot set the frame rate of the output format.');
                    }
                    
                    $output_format->setVideoFrameRate($this->_extracting_frames);
                }
            }

//          check to see if an aspect ratio is set, if it is correct the width and heights to reflect that aspect ratio.
//          This isn't strictly needed it is purely for informational purposes that this is done, because if the width is not
//          inline with what is should be according to the aspect ratio ffmpeg will report the wrong final width and height
//          when using it to lookup information about the file.
            if(isset($options['video_aspect_ratio']))
            {
                $aspect_ratio = $options['video_aspect_ratio'];
                if(empty($aspect_ratio) === false)
                {
                    $dimensions = $options['video_dimensions'];
                    if(empty($dimensions) === true)
                    {
                        $dimensions = $this->readDimensions();
                        if(empty($dimensions) === false)
                        {
                            $dimensions['auto_adjust_dimensions'] = $aspect_ratio['auto_adjust_dimensions'];
                            $dimensions['force_aspect'] = true;
                            $options['video_dimensions'] = $dimensions;
                        }
                    }
                    if(empty($dimensions) === false)
                    {
                        if(strpos($aspect_ratio['ratio'], ':') !== false)
                        {
                            $ratio = explode(':', $aspect_ratio['ratio']);
                            $ratio = $ratio[0] / $ratio[1];
                            $new_width = round($dimensions['height'] * $ratio);
//                          make sure new width is an even number
                            $ceiled = ceil($new_width);
                            $new_width = $ceiled % 2 !== 0 ? floor($new_width) : $ceiled;
                            if($new_width !== $dimensions['width'])
                            {
                                $output_format->setVideoDimensions($new_width, $dimensions['height'], $dimensions['auto_adjust_dimensions'], $dimensions['force_aspect']);
                                $options = $output_format->getFormatOptions();
                            }
                        }
                        else if(strpos($aspect_ratio['ratio'], '.') !== false)
                        {
                            $ratio = floatval($aspect_ratio['ratio']);
                            $new_width = $dimensions['height'] * $ratio;
//                          make sure new width is an even number
                            $ceiled = ceil($new_width);
                            $new_width = $ceiled % 2 !== 0 ? floor($new_width) : $ceiled;
                            if($new_width !== $dimensions['width'])
                            {
                                $output_format->setVideoDimensions($new_width, $dimensions['height'], $dimensions['auto_adjust_dimensions'], $dimensions['force_aspect']);
                                $options = $output_format->getFormatOptions();
                            }
                        }
                    }
                }
            }
            
//          check the video dimensions to see if we need to post process the dimensions
            if(isset($options['video_dimensions']))
            {
                $dimensions = $options['video_dimensions'];
                if(empty($dimensions) === false && $dimensions['auto_adjust_dimensions'] === true)
                {
//                  get the optimal dimensions for this video based on the aspect ratio
                    $optimal_dimensions = $this->getOptimalDimensions($dimensions['width'], $dimensions['height'], $dimensions['force_aspect']);
                    if($dimensions['width'] !== $optimal_dimensions['padded_width'] || $dimensions['height'] !== $optimal_dimensions['padded_height'])
                    {
                        $output_format->setVideoDimensions($optimal_dimensions['padded_width'], $optimal_dimensions['padded_height']);
                    }
                
//                  check to see if we have to apply any padding.
                    if($optimal_dimensions['pad_top'] > 0 || $optimal_dimensions['pad_right'] > 0 || $optimal_dimensions['pad_bottom'] > 0 || $optimal_dimensions['pad_left'] > 0)
                    {
                        $output_format->setVideoPadding($optimal_dimensions['pad_top'], $optimal_dimensions['pad_right'], $optimal_dimensions['pad_bottom'], $optimal_dimensions['pad_left'], $optimal_dimensions['padded_width'], $optimal_dimensions['padded_height']);
                    }
                }
            }
            
        }
        
        /**
         * Takes in a set of video dimensions - original and target - and returns the optimal conversion
         * dimensions.  It will always return the smaller of the original or target dimensions.
         * For example: original dimensions of 320x240 and target dimensions of 640x480.
         * The result will be 320x240 because converting to 640x480 would be a waste of disk
         * space, processing, and bandwidth (assuming these videos are to be downloaded).
         * 
         * @param $target_width:       The width of the video file which we will be converting to.
         * @param $target_height:      The height of the video file which we will be converting to.
         * @param $force_aspect:       Boolean value of whether or not to force conversion to the target's
         *                 aspect ratio using padding (so the video isn't stretched).  If false, the
         *                conversion dimensions will retain the aspect ratio of the original.
         *                Optional parameter.  Defaults to true.
         * @return: An array containing the size and padding information to be used for conversion.
         *       Format:
         *       Array
         *       (
         *           [width] => int
         *           [height] => int
         *           [pad_top] => int // top padding (if applicable)
         *           [pad_bottom] => int // bottom padding (if applicable)
         *           [pad_left] => int // left padding (if applicable)
         *           [pad_right] => int // right padding (if applicable)
         *       )
         *
         * @access public
         * @author Herr K
         * @link http://stackoverflow.com/q/3988011/194480
         * @return void
         */
        public function getOptimalDimensions($target_width, $target_height, $force_aspect=true)
        {
            $dimensions = $this->readDimensions();
            if(empty($dimensions) === true)
            {
                throw new \RuntimeException('Unable to read the videos dimensions.');
            }
            
            $original_width = $dimensions['width'];
            $original_height = $dimensions['height'];   
            
//          Array to be returned by this function
            $target = array(
                'padded_width' => $original_width,
                'padded_height' => $original_height,
                'video_width' => $original_width,
                'video_height' => $original_height,
                'pad_top' => 0,
                'pad_right' => 0,
                'pad_bottom' => 0,
                'pad_left' => 0,
            );

//          Target aspect ratio (width / height)
            $aspect = $target_width / $target_height;

//          Target reciprocal aspect ratio (height / width)
            $raspect = $target_height / $target_width;

//          Aspect ratio is different
            if($original_width/$original_height !== $aspect)
            {
//              Width is the greater of the two dimensions relative to the target dimensions
                if($original_width/$original_height > $aspect)
                {
//                  Original video is smaller.  Scale down dimensions for conversion
                    if($original_width < $target_width)
                    {
                        $target_width = $original_width;
                        $target_height = round($raspect * $target_width);
                    }
                    
//                  Calculate height from width
                    $original_height = round($original_height / $original_width * $target_width);
                    $original_width = $target_width;
                    if($force_aspect === true)
                    {
//                      Pad top and bottom
                        $dif = round(($target_height - $original_height) / 2);
                        $target['pad_top'] = $dif;
                        $target['pad_bottom'] = $dif;
                    }
                }
                else
                {
//                  Height is the greater of the two dimensions relative to the target dimensions
                    if($original_height < $target_height)
                    {
//                      Original video is smaller.  Scale down dimensions for conversion
                        $target_height = $original_height;
                        $target_width = round($aspect * $target_height);
                    }
                    
//                  Calculate width from height
                    $original_width = round($original_width / $original_height * $target_height);
                    $original_height = $target_height;
                    if($force_aspect === true)
                    {
//                      Pad left and right
                        $dif = round(($target_width - $original_width) / 2);
                        $target['pad_left'] = $dif;
                        $target['pad_right'] = $dif;
                    }
                }
            }
//          The aspect ratio is the same
            else
            {
                if($original_width !== $target_width)
                {
//                  The original video is smaller.  Use its resolution for conversion
                    if($original_width < $target_width)
                    {
                        $target_width = $original_width;
                        $target_height = $original_height;
                    }
//                  The original video is larger,  Use the target dimensions for conversion
                    else
                    {
                        $original_width = $target_width;
                        $original_height = $target_height;
                    }
                }
            }
            
//          Use the target_ vars because they contain dimensions relative to the target aspect ratio
            if($force_aspect === true)
            {
                $target['padded_width'] = $target_width;
                $target['padded_height'] = $target_height;
            }
            else
            {
                $target['padded_width'] = $original_width;
                $target['padded_height'] = $original_height;
            }
            
            return $target;
        }
        
        /**
         * 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 readDimensions($read_from_cache=true)
        {
            $video_data = parent::readVideoComponent($read_from_cache);
            return $video_data['dimensions'];
        }
        
        /**
         * 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 getFrameRate($read_from_cache=true)
        {
            $video_data = parent::readVideoComponent($read_from_cache);
            return $video_data['frames']['rate'];
        }
    }

