<?php


namespace CMSMSExt;

use CMSMSExt\Exceptions\FileSystemException;

/**
 * Class FS
 * a class for filesystem utils
 *
 * @package CMSMSExt
 */
class FS
{
  /**
   * Recursively delete a directory
   *
   * @param string $directory The absolute path to the directory to be removed.
   *
   * @return bool
   */
  public static function dir_recursive_delete(string $directory)
  {
    if('/' === \substr($directory, -1))
    {
      $directory = \substr($directory, 0, -1);
    }
    
    if(!\file_exists($directory) || !\is_dir($directory)) { return FALSE; }
  
    if(\is_readable($directory))
    {
      $handle = \opendir($directory);
      
      while(FALSE !== ($item = \readdir($handle)))
      {
        if('.' !== $item && '..' !== $item)
        {
          $path = $directory . '/' . $item;
          
          if(\is_dir($path))
          {
            self::dir_recursive_delete($path);
          }
          else
          {
            \unlink($path);
          }
        }
      }
      
      \closedir($handle);
      
      if( !\rmdir($directory) ) { return FALSE; }
    }
  
    return TRUE;
  }
  
  
  /**
   * Return a list of all of the directories inside a parent
   * This method is NOT recursive
   *
   * @param string $parent The absolute path to the directory to search
   * @return array An array of directories directly below the parent.
   */
  public static function dir_list(string $parent)
  {
    if(empty($parent)) { return []; }
    
    if(!\is_dir($parent)) { return []; }
    
    $dh = \opendir($parent);
    
    if(!$dh) { return []; }
    
    $results = [];
    while(FALSE !== ($file = \readdir($dh)))
    {
      if('.' === $file || '..' === $file) { continue; }
      
      if(\startswith($file, '.')) { continue; }
      
      if(\is_dir($parent . '/' . $file))
      {
        $results[] = $file;
      }
    }
    
    \closedir($dh);
    
    return $results;
  }
  
  /**
   * Recursively create a subdirectory till the path exists.
   *
   * @param string   $pathname The path name to create
   * @param int      $mode     The octal permission for the newly created subdirectories
   *
   * @return bool
   */
  public static function mkdir_r(string $pathname, int $mode = 0755) : bool
  {
    // Check if directory already exists
    if(empty($pathname) || \is_dir($pathname)) { return TRUE; }
    
    // Ensure a file does not already exist with the same name
    if(\is_file($pathname)) { return TRUE; }
    
    // Crawl up the directory tree
    $next_pathname = \dirname($pathname);
    
    if(self::mkdir_r($next_pathname, $mode) && !\is_file($pathname))
    {
      return \mkdir($pathname, $mode);
    }
    
    return FALSE;
  }
  
  /**
   * Given a directory, and a list of extensions, return a list of matching files.
   * This method will not return any directories.
   * This method is not recursive.
   *
   * @param string $dir The search directory
   * @param string $extensions A comma delimited list of extensions to return
   * @param bool $sorted Whether the output should be sorted (natural case) or not.
   * @return string[]
   */
  public static function get_file_list(string $dir, string $extensions, bool $sorted = TRUE)
  {
    $file_types = [];
    
    if( !empty($extensions) )
    {
      $file_types = \explode(',', $extensions);
    }
    
    $tmp = [];
    
    foreach($file_types as $one)
    {
      $one = \strtolower( \trim($one) );
      
      if($one) { $tmp[] = $one; }
    }
    
    $file_types = NULL;
    
    if( \count($tmp) ) { $file_types = $tmp; }
    
    $files = [];
    $dh    = \opendir($dir);
    
    if($dh)
    {
      while( FALSE !== ( $file = \readdir($dh) ) )
      {
        if('.' === $file || '..' === $file) { continue; }
        
        $full_path = \cms_join_path($dir, $file);
        
        if( \is_dir($full_path) ) { continue; }
        
        $ext = \substr(\strrchr($file, '.'), 1);
        
        if( !empty($file_types) && !\in_array($ext, $file_types, TRUE) ) { continue; }
        
        $files[$file] = $file;
      }
      
      \closedir($dh);
    }
    
    if($sorted) { \natcasesort($files); }
    
    return $files;
  }
  
  /**
   * Given a filename, test if it matches the specified pattern
   *
   * @param string $filename The test filename (string) or an array of strings
   * @param mixed  $pattern An array of string patterns, or a single string.
   * @param bool   $case_sensitive Whether the pattern(s) is/are case sensitive
   * @param bool   $allow_empty Whether or not an empty pattern is allowed (to match anything) (deprecated)
   *
   * @return bool
   */
  public static function file_matches_pattern(
    string $filename,
    $pattern,
    bool $case_sensitive = TRUE,
    bool $allow_empty = TRUE
  )
  {
    if(!\is_array($pattern))
    {
      if(!$pattern && $allow_empty) { return TRUE; } // no pattern = match everything
      
      $pattern = [$pattern];
    }
  
    foreach($pattern as $iValue)
    {
      $f = $filename;
      $p = $iValue;
      
      if(!$p) { continue; }
      
      if(!$case_sensitive)
      {
        $f = \strtolower($filename);
        $p = \strtolower($iValue);
      }
      if( \fnmatch($p, $f) ) { return TRUE;}
    }
  
    return FALSE;
  }
  
  /**
   * Similar to the PHP glob method this function will search all files in the directory (and below it) and return all
   * files that match the pattern.
   *
   * @param string $path           The full path to the input directory
   * @param mixed  $pattern        An array of string patterns.  Or a single string
   * @param string $mode           Either FILES, DIRS, or FULL.  If 'FILES', only files matching the pattern are
   *                               returned.  If 'DIRS' only directories are returned.  IF 'FULL', any file or
   *                               directory matching the pattern is returned.
   * @param mixed  $excludepattern An array of string patterns to exclude.  Or a simple string
   * @param int    $maxdepth       The maximum directory level to search.  Default is infinite.
   * @param int    $d              Internal
   *
   * @return array|void
   */
  public static function recursive_glob(
    string $path,
    $pattern = '*',
    string $mode = 'FULL',
    $excludepattern = '',
    int $maxdepth = -1,
    int $d = 0
  )
  {
    if( !\endswith($path, '/') ) { $path .= '/'; }
    
    $dir_list = [];
    
    if(
      ('FILES' !== $mode) && self::file_matches_pattern(\basename($path), $pattern) &&
      !self::file_matches_pattern(\basename($path), $excludepattern)
    ){ $dir_list[] = $path; }
    
    if( !\is_dir($path) ) { return; }
    
    if( $handle = \opendir($path) )
    {
      $tmp = [];
      while( FALSE !== ( $file = \readdir($handle) ) )
      {
        if('.' === $file || '..' === $file) { continue; }
        
        $fs = $path . $file;
        
        if( !@\is_dir($fs) && !\startswith($file, '.') && !\startswith($file, '_') )
        {
          if(
            'DIRS' !== $mode && self::file_matches_pattern($file, $pattern) &&
            !self::file_matches_pattern($file, $excludepattern, FALSE, FALSE)
          )
          { $dir_list[] = $fs; }
        }
        else if(
          $d >= 0 && ($d < $maxdepth || $maxdepth < 0) && !\startswith($file, '.') && !\startswith($file, '_') &&
          !self::file_matches_pattern($file, $excludepattern, TRUE, FALSE)
        )
        {
          $tmp[]     = self::recursive_glob($fs . '/', $pattern, $mode, $excludepattern, $maxdepth, $d + 1);
        }
      }
  
      $dir_list = \array_merge($dir_list,...$tmp);
      
      \closedir($handle);
    }
    
    return $dir_list;
  }
  
  /**
   * @param array  $list
   * @param string $dir
   * @param null   $filter
   * @param null   $exclude
   */
  public static function findFiles(&$list = [], $dir = '.', $filter = NULL, $exclude = NULL)
  {
    if( !\is_array($filter) ) { $filter = [$filter]; }
    if( empty($exclude) ) { $exclude = []; }
    if(!\is_array($exclude)) { $exclude = [$exclude]; }
    
    $tmp = [];
    foreach($exclude as $exclusion)
    {
      $tmp[] = '(^\\' . $exclusion . '$)';
    }
    
    $exclude_regex = \implode('|', $tmp);
    
    $iter = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS);
    
    foreach(
      new \RecursiveIteratorIterator(
        $iter,
        \RecursiveIteratorIterator::SELF_FIRST
      )
      as $f
    )
    {
      if( \preg_match( '/' . $exclude_regex . '/i', $iter->getFilename() ) ) { continue; }
      
      //one last check
      if( \in_array($iter->getFilename(), $exclude) ) { continue; }
      
      $extended_dir_array = \explode(\DIRECTORY_SEPARATOR, $f->getPathname() );
      
      # the following is all failing... and seems redundant anyway :S JM
      $cont = FALSE;

      foreach($extended_dir_array as $test_dir)
      {
        if( \preg_match('/' . $exclude_regex . '/i', $test_dir ) ) { $cont = TRUE; }

        //one last check
        if( \in_array($f->getFilename(), $exclude, FALSE) ) { $cont = TRUE; }
      }

      if($cont) { continue; }
      
      if($f->isDir())
      {
        if( \preg_match('/' . $exclude_regex . '/i', $f->getFilename() ) ) { continue; }
        
        //one last check
        if( \in_array($f->getFilename(), $exclude, FALSE) ) { continue; }
        
      }
      else
      {
        $current_file = \explode('.', $f->getBasename());
        
        if( \preg_match('/' . $exclude_regex . '/i', $current_file[0]) ) { continue; }
        if( \in_array($current_file[0], $exclude) ) { continue; }
        if( \preg_match( '/' . $exclude_regex . '/i', '.' . $f->getExtension() ) ) { continue; }
        if( \in_array('.' . $f->getExtension(), $exclude) ) { continue; }
      }
      
      if(!empty($filter))
      {
        if( \in_array('.' . $f->getExtension(), $filter) )
        {
          $list[] = $f->getPathname();
        }
        
        // otherwise nothing is added
      }
      else
      {
        $list[] = $f->getPathname();
      }
    }
  }
  
  static function copy_r(string $source_dir, string $target_dir, array $excludes = [])
  {
    //NOTE KEY_AS_FILENAME flag does not work as such - always get path here
    $iter = new \RecursiveIteratorIterator(
      new \RecursiveDirectoryIterator($source_dir,
                                \FilesystemIterator::KEY_AS_FILENAME |
                                \FilesystemIterator::CURRENT_AS_PATHNAME |
                                \FilesystemIterator::UNIX_PATHS |
                                \FilesystemIterator::FOLLOW_SYMLINKS
      ),
      \RecursiveIteratorIterator::SELF_FIRST);
    
    $len = \strlen($source_dir . \DIRECTORY_SEPARATOR);
    
    $matches = null;
  
    foreach($iter as $fn => $fp)
    {
      foreach($excludes as $excl)
      {
        if(\preg_match($excl, $fp, $matches, 0, $len))
        {
          $relpath = \substr($fp, $len);
          continue 2;
        }
      }
      
      $relpath = \substr($fp, $len);
  
      if('.' === $fn)
      {
        $tp = \joinpath($target_dir, $relpath);
    
        if(!\mkdir( $concurrentDirectory = \dirname($tp), 0771, TRUE) && !\is_dir($concurrentDirectory) )
        {
          throw new FileSystemException( \sprintf('Directory "%s" was not created', $concurrentDirectory) );
        }
      }
      else if('..' !== $fn)
      {
        $tp = \joinpath($target_dir, $relpath);
        if( !\mkdir($concurrentDirectory = \dirname($tp), 0771, TRUE) && !\is_dir($concurrentDirectory) )
        {
          throw new FileSystemException( \sprintf('Directory "%s" was not created', $concurrentDirectory) );
        }
        
        @\copy($fp, $tp);
      }
    }
  }
  
}

?>