<?php
/**
 * @copyright (c) Kufer Software GmbH, Oderstraße 7, D-84453 Mühldorf
 */

if (!defined('DEPENDENCIES_MAIN')) {
  define('DEPENDENCIES_MAIN', BASEDIR . 'webtools' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'dependencies.json');
}

if (!defined('DEPENDENCIES_EXTRA')) {
  define('DEPENDENCIES_EXTRA', BASEDIR . 'webtools' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'extraDependencies.json');
}


class AutoLoader
{
  
  private $mainDependencies;
  
  private $extraDependencies;

  
  private $alreadyGenerated = false;

  
  private $notFoundClassNames = array();

  
  private $kuferToolsFallbackDependencies;

  private function __construct()
  {
    if (basename(($_SERVER['SCRIPT_NAME'])) === 'generateDependencies.php' && !file_exists(DEPENDENCIES_MAIN)) {
      $this->generateMainDependencies();
    }
  }

  private function readKuferToolsFallbackDependencies()
  {
    if (is_null($this->kuferToolsFallbackDependencies)) {
      $this->kuferToolsFallbackDependencies = json_decode(file_get_contents(
        \FSOperations::path(BASEDIR, 'webtools', 'config', 'kufertoolsFallbackDependencies.json')
      ), true);
    }

    return $this->kuferToolsFallbackDependencies;
  }

  
  public static function getInstance()
  {
    static $instance = null;
    if ($instance == null) {
      $instance = new AutoLoader();
    }

    return $instance;
  }

  
  private function getDependenciesFromFile($pPath)
  {
    if (!file_exists($pPath)) {
      return array();
    }

    return json_decode(file_get_contents($pPath), true);
  }

  
  private function getMainDependencies()
  {
    if ($this->mainDependencies == null) {
      $this->mainDependencies = $this->getDependenciesFromFile(DEPENDENCIES_MAIN);
    }

    return $this->mainDependencies;
  }

  
  private function saveMainDependencies($pDependencies)
  {
    file_put_contents(DEPENDENCIES_MAIN, json_encode($pDependencies));
    $this->mainDependencies = $pDependencies;
  }

  
  private function getExtraDependencies()
  {
    if ($this->extraDependencies == null) {
      $this->extraDependencies = $this->getDependenciesFromFile(DEPENDENCIES_EXTRA);
    }

    return $this->extraDependencies;
  }

  
  private function saveExtraDependencies($pDependencies)
  {
    file_put_contents(DEPENDENCIES_EXTRA, json_encode($pDependencies));
    $this->extraDependencies = $pDependencies;
  }

  
  public function load($className)
  {
    if (isset($this->notFoundClassNames[$className]) && $this->notFoundClassNames[$className]) {
      return;
    }

    $path = $this->getPath4Class($className, $this->getMainDependencies());

    if ($path === false) {
      $path = $this->getPath4Class($className, $this->getExtraDependencies());
      if ($path === false && !$this->alreadyGenerated) {
        $this->generateExtraDependencies();
        $path = $this->getPath4Class($className, $this->getExtraDependencies());
        $this->alreadyGenerated = true;
      }
    }

    if ($path !== false) {
      try {
        if ((@include_once BASEDIR . $path) === false) {
          $found = false;

          
          if (strpos($className, 'Kufer\\Web\\KuferTools\\') === 0) {
            $fallback = $this->readKuferToolsFallbackDependencies();
            $lcClassName = strtolower($className);
            foreach ($this->readKuferToolsFallbackDependencies() as $key => $val) {
              if (strpos($lcClassName, $key . '\\') === 0) {
                $path = $val . substr($path, strlen($val));
                if ((@include_once BASEDIR . $path) !== false) {
                  $found = true;
                  break;
                }
              }
            }
          } elseif (basename($path) !== strtolower(basename($path))) {
            $lcpath = dirname($path) . DIRECTORY_SEPARATOR . strtolower(basename($path));
            if ((@include_once BASEDIR . $lcpath) !== false) {
              $found = true;
            }
          }

          if (!$found) {
            throw new \Error("Datei bei \"$path\" für Klasse \"$className\" existiert nicht");
          }
        }
      } catch (\Error $error) {
        try {
          
          
          require_once BASEDIR . 'classes/logger/Logger.php';
          require_once BASEDIR . 'classes/logger/DateiLogger.php';
          require_once BASEDIR . 'classes/logger/MaxFileSizeLogger.php';
          require_once BASEDIR . 'classes/addfunctions/ConstantOperations.php';
          require_once BASEDIR . 'classes/addfunctions/Path.php';
          require_once BASEDIR . 'classes/addfunctions/DebugOperations.php';
          require_once BASEDIR . 'classes/addfunctions/StringOperations.php';
          \Logger::loggException($error, 400);
        } catch (\Throwable $ex) {
          throw $error;
        }

        throw $error;
      }
    } else {
      $this->notFoundClassNames[$className] = true;
      
      
      
    }
  }

  
  private function getPath4Class($pClassName, $pDependencies)
  {
    $className = strtolower($pClassName);
    if (isset($pDependencies[$className])) {
      return $pDependencies[$className];
    } elseif (!is_null($pathFromPsr4 = $this->getPathWithoutBasedirFromPsr4LikeType($pClassName))) {
      return $pathFromPsr4;
    } else {
      return false;
    }
  }

  
  private function generateMainDependencies()
  {
    $basedir = rtrim(BASEDIR, DIRECTORY_SEPARATOR);
    $dependencies = $this->getDependencies($basedir, array());
    $this->saveMainDependencies($dependencies);
  }

  
  private function generateExtraDependencies()
  {
    $result = array();
    foreach ($this->getDirsToLookUpOnError() as $dir) {
      $dependencies = $this->getDependencies(BASEDIR . $dir, array());
      $result = array_merge($result, $dependencies);
    }
    $this->saveExtraDependencies($result);
  }

  
  private function getDependencies($pPath, $pDependencies)
  {
    if (is_dir($pPath)) {
      return $this->handleGetDependencies4Dir($pPath, $pDependencies);
    } else {
      return $this->handleGetDependencies4File($pPath, $pDependencies);
    }
  }

  
  private function handleGetDependencies4Dir($pPath, $pDependencies)
  {
    $dir = dir($pPath);
    $dirsToIgnore = $this->getDirsToIgnore();
    while (false !== ($entry = $dir->read())) {
      if (in_array($entry, $dirsToIgnore)) {
        continue;
      }

      $pDependencies = $this->getDependencies($pPath . DIRECTORY_SEPARATOR . $entry, $pDependencies);
    }
    $dir->close();

    return $pDependencies;
  }

  
  private function handleGetDependencies4File($pPath, $pDependencies)
  {
    if (is_file($pPath) && (pathinfo($pPath, PATHINFO_EXTENSION) == 'php')) {
      $types = $this->_getTypesFromFile($pPath);

      
      $basedirlength = strlen(BASEDIR);
      $pathWithoutBaseDir = substr($pPath, $basedirlength);

      
      $pathWithoutBaseDir = str_replace('\\', '/', $pathWithoutBaseDir);

      foreach ($types as $type) {
        
        $typelow = strtolower($type);

        if (array_key_exists($typelow, $pDependencies)) {
          
          $message = "Typ '{$typelow}' wurde in " . $pDependencies[$typelow] . " definiert und soll nun in $pathWithoutBaseDir wieder definiert werden.";

          throw new Exception($message);
        }

        if ($this->getPathWithoutBasedirFromPsr4LikeType($type) !== $pathWithoutBaseDir) {
          $pDependencies[$typelow] = $pathWithoutBaseDir;
        }
      }
    }

    return $pDependencies;
  }

  private function getPathWithoutBasedirFromPsr4LikeType(string $typeWithNamespace): ?string
  {
    require_once BASEDIR . implode('/', array('classes', 'addfunctions', 'StringOperations.php'));
    require_once BASEDIR . implode('/', array('classes', 'addfunctions', 'CodingOperations.php'));
    require_once BASEDIR . implode('/', array('classes', 'addfunctions', 'System.php'));

    $lastNamespacePos = \StringOperations::strrpos($typeWithNamespace, '\\');
    if ($lastNamespacePos === false) {
      return null;
    }

    $lcNamespace = \StringOperations::strtolower(\StringOperations::substr($typeWithNamespace, 0, $lastNamespacePos));
    $typename = \StringOperations::substr($typeWithNamespace, $lastNamespacePos + 1);

    if (\StringOperations::startsWith($lcNamespace, 'kufer\\web\\webbasys\\includee')) {
      list(4 => $withoutFirstPart) = explode('\\', $lcNamespace, 5);

      return str_replace('\\', '/', 'webbasys\\include\\' . $withoutFirstPart . '\\' . $typename . '.php');
    }
    if (\StringOperations::startsWith($lcNamespace, 'kufer\\web\\')) {
      list(2 => $withoutFirstPart) = explode('\\', $lcNamespace, 3);

      return str_replace('\\', '/', $withoutFirstPart . '\\' . $typename . '.php');
    }

    return null;
  }

  
  private function _getTypesFromFile($pPath)
  {
    $tokens = $this->getTokensFromFile($pPath);

    return $this->getTypesFromTokens($tokens);
  }

  
  private function getTokensFromFile($pPath)
  {
    $code = php_strip_whitespace($pPath);

    return token_get_all($code);
  }

  
  private function getTypesFromTokens($pTokens)
  {
    $types = array();
    $namespace = '';
    $namespaceMode = false;

    $skipNext = 0;

    foreach ($pTokens as $index => $token) {
      
      
      if (!is_array($token)) {
        $token = array($token);
      }

      if ($skipNext > 0) {
        $skipNext--;

        continue;
      }

      
      if ($namespaceMode) {
        if ($token[0] == ';' || $token[0] == '{') {
          $namespace .= '\\';
          $namespaceMode = false;
        } elseif ($token[0] == T_STRING || $token[0] == T_NS_SEPARATOR) {
          $namespace .= $token[1];
        } elseif ((defined('T_NAME_QUALIFIED') && $token[0] == T_NAME_QUALIFIED)
               || (defined('T_NAME_FULLY_QUALIFIED') && $token[0] == T_NAME_FULLY_QUALIFIED)
               || (defined('T_NAME_RELATIVE') && $token[0] == T_NAME_RELATIVE)) {
          $namespace .= $token[1];
        }
      } elseif ($token[0] == T_DOUBLE_COLON && $pTokens[$index + 1][0] == T_CLASS) {
        $skipNext = 2;
      } elseif ($token[0] == T_NEW && $pTokens[$index + 2][0] == T_CLASS) {
        
        $skipNext = 2;
      } elseif ($token[0] == T_CLASS || $token[0] == T_INTERFACE || $token[0] == T_TRAIT) {
        
        
        
        $types[] = $namespace . $pTokens[$index + 2][1];
      } elseif ($token[0] == T_NAMESPACE) {
        $namespace = '';
        $namespaceMode = true;
      }
    }

    return $types;
  }

  
  private function getDirsToIgnore(): array
  {
    return array(
      '.',
      '..',
      '.svn',
      '.git',
      'jquery',
      'TestLocal',
      'unittests',
      'integrationtests',
      'composer',
      'plugins',
      'phpunit'
    );
  }

  
  private function getDirsToLookUpOnError()
  {
    return array(
      'classes/validation/KtoPruef',
      'plugins'
    );
  }
}