<?php
/**
 *	pluginbuddy_zbdir Class
 *
 *  Provides a directory class for zipbuddy for building a directory tree for backup
 *	
 *	Version: 1.0.0
 *	Author:
 *	Author URI:
 *
 *	@param		$parent		object		Optional parent object which can provide functions for reporting, etc.
 *	@return		null
 *
 */
if ( !class_exists( "pluginbuddy_zbdir" ) ) {

	class pluginbuddy_zbdir {
	
		// status method type parameter values - would like a class for this
		const STATUS_TYPE_DETAILS = 'details';
		const NORM_DIRECTORY_SEPARATOR = '/';
		const DIRECTORY_SEPARATORS = '/\\';

		public $_version = '1.0';


        /**
         * The path of this directory node
         * 
         * @var path string
         */
        protected $_path = "";
        
        /**
         * The absolute paths to be excluded, must be / terminated
         * 
         * @var paths_to_exclude array of string
         */
        protected $_paths_to_exclude = array();

        /**
         * The directory listing items to be ignored
         * 
         * @var items_to_ignore array of string
         */
        protected $_items_to_ignore = array( ".", "..", ".DS_Store" );

        /**
         * The items that are terminals and we can add directly for this directory (absolute paths)
         * 
         * @var terminals array of string
         */
        protected $_terminals = array();

        /**
         * The branch nodes of subordinate directories that are on an exclusion path
         * 
         * @var branches array of string => pluginbuddy_zbdir
         */
        protected $_branches = array();

        /**
         * Whether or not we can call a status calback
         * 
         * @var have_status_callback bool
         */
		protected $_have_status_callback = false;
		
        /**
         * Object->method array for status function
         * 
         * @var status_callback array
         */
		protected $_status_callback = array();
		
		/**
		 *	__construct()
		 *	
		 *	Default constructor.
		 *	
		 *	@param		string		$path			The path to form a node for
		 *	@param		array		$excludes		The list of dirs/files to exclude (absolute paths with / terminator for dirs)
		 *	@return		null
		 *
		 */
		public function __construct( $path, $excludes = array() ) {

			// Normalize the trailing directory separator on the path
			$path = rtrim( $path, self::DIRECTORY_SEPARATORS ) . self::NORM_DIRECTORY_SEPARATOR;
			
			// Normalize platform specific directory separators in path
			$this->_path = str_replace( DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $path );
			
			$this->_paths_to_exclude = $excludes;
			
			$content = scandir( $this->_path ); // Get the directory content, will be simple names
			
			// Process each item for ignoring, treating as a terminal or as a branch
			foreach ( $content as &$item ) {

				// Initially check the simple name
				if ( in_array( $item, $this->_items_to_ignore ) ) {

					// This is just fluff in the directory listing
					continue;
					
				} elseif ( is_dir( ( $this->_path . $item ) ) ) {

					// It's a directory, check for matching exclusion or being prefix of exclusion
					if ( in_array( ( $this->_path . $item . self::NORM_DIRECTORY_SEPARATOR ), $this->_paths_to_exclude ) ) {
					
						// Exact match to an exclusion, exclude this directory completely
						continue;
						
					} elseif ( $this->in_array_prefix( ( $this->_path . $item . self::NORM_DIRECTORY_SEPARATOR ), $this->_paths_to_exclude ) ) {

						// Need a new node, add to the node array (absolute dir path is key)
						$this->_branches[ ( $this->_path . $item ) ] = new pluginbuddy_zbdir( ( $this->_path . $item . self::NORM_DIRECTORY_SEPARATOR ), $this->_paths_to_exclude, $this );
						
					} else {
					
						// Neither exclusion nor exclusion prefix so well treat it as a terminal
						$this->_terminals[] = ( $this->_path . $item );
					
					}
					
				} else {

					// Assume it's a file, check for matching exclusion
					if ( in_array( ( $this->_path . $item ), $this->_paths_to_exclude ) ) {
					
						// Exact match to an exclusion, exclude this file completely
						continue;
						
					} else {
					
						// Not an exclusion so it's a terminal
						$this->_terminals[] = ( $this->_path . $item );
						
					}
					
				}	
			}
							
		}
		
		/**
		 *	__destruct()
		 *	
		 *	Default destructor.
		 *	
		 *	@return		null
		 *
		 */
		public function __destruct( ) {

		}
				
		/**
		 *	set_status_callback()
		 *
		 *	Sets a reference to the function to call for each status update.
		 *  Argument must at least be a non-empty array with 2 elements
		 *
		 *	@param		array 	$callback	Object->method to call for status updates.
		 *	@return		null
		 *
		 */
		public function set_status_callback( $callback = array() ) {
		
			if ( is_array( $callback ) && !empty( $callback ) && ( 2 == count( $callback ) ) ) {
			
				$this->_status_callback = $callback;
				$this->_have_status_callback = true;

			}
			
		}
		
		/**
		 *	status()
		 *	
		 *	Invoke status method of parent if it exists
		 *  Must be at least one parameter otherwise ignore the call
		 *	
		 *	@param		string		$type		(Expected) Status message type.
		 *	@param		string		$message	(Expected) Status message.
		 *	@return		null
		 *
		 */
		public function status() {
		
			if ( $this->_have_status_callback && ( func_num_args() > 0 ) ) {

				$args = func_get_args();
				call_user_func_array( $this->_status_callback, $args );
				
			}
			
		}
		
		/**
		 *	get_terminals()
		 *	
		 *	Returns the array of terminals from this dir plus subordinates
		 *	
		 *	@return		array	Flat array of terminal filenames and directory names
		 *
		 */
		public function get_terminals( ) {
		
			// Minimum is our terminals
			$all_terminals = $this->_terminals;
			
			// Now add terminals from each subordinate
			foreach ( $this->_branches as $branch ) {
			
				$all_terminals = array_merge( $all_terminals, $branch->get_terminals() );
				
			}
			
			return $all_terminals;
			
		}
		
		/**
		 *	get_relative_excludes()
		 *	
		 *	Returns the array of exclusions with optional directory prefix removed
		 *	
		 *	@param		string	The base directory prefix to be removed
		 *	@return		array	Flat array of relative (to site root) excluded filenames and directory names
		 *
		 */
		public function get_relative_excludes( $base = '' ) {
		
			// The basedir must have a trailing normalized directory separator
			$basedir = ( rtrim( trim( $base ), self::DIRECTORY_SEPARATORS ) ) . self::NORM_DIRECTORY_SEPARATOR;
		
			// Normalize platform specific directory separators in path
			$basedir = str_replace( DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $basedir );
			
			$relative_excludes = $this->_paths_to_exclude;
			
			foreach ( $relative_excludes as &$exclude ) {
			
				// Remove base prefix but leave leading normalized directory separator
				$exclude = str_replace( rtrim( $basedir, self::NORM_DIRECTORY_SEPARATOR ), '', $exclude );
			
			}
			
			return $relative_excludes;
			
		}
		
		/**
		 *	in_array_prefix()
		 *	
		 *	Check if the given string is a prefix of any string in the given array
		 *	
		 *  @param		string	$prefix		The prefix string
		 *  @param		array	$candidates	The array of strings
		 *	@return		bool	true if the string is a prefix, false otherwise
		 *
		 */
		public function in_array_prefix( $prefix, array $candidates ) {

			foreach ( $candidates as $candidate ) {
			
				if ( !( false === strpos( $candidate, $prefix ) ) ) {

					// We found the prefix
					return true;
					
				}
				
			}
			
			// Got this far so not a prefix
			return false;
			
		}
		
	} // end pluginbuddy_zbdir class.	
	
}
?>