<?php
namespace Drush\Config;

use Composer\Autoload\ClassLoader;

use Drush\Drush;
use Drush\Utils\FsUtils;
use Webmozart\PathUtil\Path;

/**
 * Store information about the environment
 */
class Environment
{
    protected $homeDir;
    protected $originalCwd;
    protected $etcPrefix;
    protected $sharePrefix;
    protected $drushBasePath;
    protected $vendorDir;

    protected $docPrefix;
    protected $configFileVariant;

    protected $loader;
    protected $siteLoader;

    /**
     * Environment constructor
     * @param string $homeDir User home directory.
     * @param string $cwd The current working directory at the time Drush was called.
     * @param string $autoloadFile Path to the autoload.php file.
     */
    public function __construct($homeDir, $cwd, $autoloadFile)
    {
        $this->homeDir = $homeDir;
        $this->originalCwd = Path::canonicalize(FsUtils::realpath($cwd));
        $this->etcPrefix = '';
        $this->sharePrefix = '';
        $this->drushBasePath = Path::canonicalize(dirname(dirname(__DIR__)));
        $this->vendorDir = FsUtils::realpath(dirname($autoloadFile));
    }

    /**
     * Load the autoloader for the selected Drupal site
     *
     * @param string $root
     * @return ClassLoader
     */
    public function loadSiteAutoloader($root)
    {
        $autloadFilePath = "$root/autoload.php";
        if (!file_exists($autloadFilePath)) {
            return $this->loader;
        }

        if ($this->siteLoader) {
            return $this->siteLoader;
        }

        $this->siteLoader = require $autloadFilePath;
        if ($this->siteLoader === false) {
            // Nothing more to do. See https://github.com/drush-ops/drush/issues/3741.
            return $this->loader;
        }
        if ($this->siteLoader === true) {
            // The autoloader was already required. Assume that Drush and Drupal share an autoloader per
            // "Point autoload.php to the proper vendor directory" - https://www.drupal.org/node/2404989
            $this->siteLoader = $this->loader;
        }

        // Ensure that the site's autoloader has highest priority. Usually,
        // the first classloader registered gets the first shot at loading classes.
        // We want Drupal's classloader to be used first when a class is loaded,
        // and have Drush's classloader only be called as a fallback measure.
        $this->siteLoader->unregister();
        $this->siteLoader->register(true);

        return $this->siteLoader;
    }

    /**
     * Return the name of the user running drush.
     *
     * @return string
     */
    protected function getUsername()
    {
        $name = null;
        if (!$name = getenv("username")) { // Windows
            if (!$name = getenv("USER")) {
                // If USER not defined, use posix
                if (function_exists('posix_getpwuid')) {
                    $processUser = posix_getpwuid(posix_geteuid());
                    $name = $processUser['name'];
                }
            }
        }
        return $name;
    }

    protected function getTmp()
    {
        $directories = [];

        // Get user specific and operating system temp folders from system environment variables.
        // See http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/ntcmds_shelloverview.mspx?mfr=true
        $tempdir = getenv('TEMP');
        if (!empty($tempdir)) {
            $directories[] = $tempdir;
        }
        $tmpdir = getenv('TMP');
        if (!empty($tmpdir)) {
            $directories[] = $tmpdir;
        }
        // Operating system specific dirs.
        if (self::isWindows()) {
            $windir = getenv('WINDIR');
            if (isset($windir)) {
                // WINDIR itself is not writable, but it always contains a /Temp dir,
                // which is the system-wide temporary directory on older versions. Newer
                // versions only allow system processes to use it.
                $directories[] = Path::join($windir, 'Temp');
            }
        } else {
            $directories[] = Path::canonicalize('/tmp');
        }
        $directories[] = Path::canonicalize(sys_get_temp_dir());

        foreach ($directories as $directory) {
            if (is_dir($directory) && is_writable($directory)) {
                $temporary_directory = $directory;
                break;
            }
        }

        if (empty($temporary_directory)) {
            // If no directory has been found, create one in cwd.
            $temporary_directory = Path::join(Drush::config()->cwd(), 'tmp');
            drush_mkdir($temporary_directory, true);
            if (!is_dir($temporary_directory)) {
                throw new \Exception(dt("Unable to create a temporary directory."));
            }
            // Function not available yet - this is not likely to get reached anyway.
            // drush_register_file_for_deletion($temporary_directory);
        }
        return $temporary_directory;
    }

    /**
     * Convert the environment object into an exported configuration
     * array.
     *
     * @see PreflightArgs::applyToConfig(), which also exports information to config.
     *
     * @return array Nested associative array that is overlayed on configuration.
     */
    public function exportConfigData()
    {
        return [
            // Information about the environment presented to Drush
            'env' => [
                'cwd' => $this->cwd(),
                'home' => $this->homeDir(),
                'user' => $this->getUsername(),
                'is-windows' => $this->isWindows(),
                'tmp' => $this->getTmp(),
            ],
            // These values are available as global options, and
            // will be passed in to the FormatterOptions et. al.
            'options' => [
                'width' => $this->calculateColumns(),
            ],
            // Information about the directories where Drush found assets, etc.
            'drush' => [
                'base-dir' => $this->drushBasePath,
                'vendor-dir' => $this->vendorPath(),
                'docs-dir' => $this->docsPath(),
                'user-dir' => $this->userConfigPath(),
                'system-dir' => $this->systemConfigPath(),
                'system-command-dir' => $this->systemCommandFilePath(),
            ],
            'runtime' => [
                'site-file-previous' => $this->getSiteSetAliasFilePath('drush-drupal-prev-site-'),
                'site-file-current' => $this->getSiteSetAliasFilePath(),
            ],
        ];
    }

    /**
     * The base directory of the Drush application itself
     * (where composer.json et.al. are found)
     *
     * @return string
     */
    public function drushBasePath()
    {
        return $this->drushBasePath;
    }

    /**
     * Get the site:set alias from the current site:set file path.
     *
     * @return bool|string
     */
    public function getSiteSetAliasName()
    {
        $site_filename = $this->getSiteSetAliasFilePath();
        if (file_exists($site_filename)) {
            $site = file_get_contents($site_filename);
            if ($site) {
                return $site;
            }
        }
        return false;
    }

    /**
     * User's home directory
     *
     * @return string
     */
    public function homeDir()
    {
        return $this->homeDir;
    }

    /**
     * The user's Drush configuration directory, ~/.drush
     *
     * @return string
     */
    public function userConfigPath()
    {
        return $this->homeDir() . '/.drush';
    }

    public function setConfigFileVariant($variant)
    {
        $this->configFileVariant = $variant;
    }

    /**
     * Get the config file variant -- defined to be
     * the Drush major version number. This is for
     * loading drush.yml and drush9.yml, etc.
     */
    public function getConfigFileVariant()
    {
        return $this->configFileVariant;
    }

    /**
     * The original working directory
     *
     * @return string
     */
    public function cwd()
    {
        return $this->originalCwd;
    }

    /**
     * Return the path to Drush's vendor directory
     *
     * @return string
     */
    public function vendorPath()
    {
        return $this->vendorDir;
    }

    /**
     * The class loader returned when the autoload.php file is included.
     *
     * @return \Composer\Autoload\ClassLoader
     */
    public function loader()
    {
        return $this->loader;
    }

    /**
     * Set the class loader from the autload.php file, if available.
     *
     * @param \Composer\Autoload\ClassLoader $loader
     */
    public function setLoader(ClassLoader $loader)
    {
        $this->loader = $loader;
    }

    /**
     * Alter our default locations based on the value of environment variables
     *
     * @return $this
     */
    public function applyEnvironment()
    {
        // Copy ETC_PREFIX and SHARE_PREFIX from environment variables if available.
        // This alters where we check for server-wide config and alias files.
        // Used by unit test suite to provide a clean environment.
        $this->setEtcPrefix(getenv('ETC_PREFIX'));
        $this->setSharePrefix(getenv('SHARE_PREFIX'));

        return $this;
    }

    /**
     * Set the directory prefix to locate the directory that Drush will
     * use as /etc (e.g. during the functional tests)
     *
     * @param string $etcPrefix
     * @return $this
     */
    public function setEtcPrefix($etcPrefix)
    {
        if (isset($etcPrefix)) {
            $this->etcPrefix = $etcPrefix;
        }
        return $this;
    }

    /**
     * Set the directory prefix to locate the directory that Drush will
     * use as /user/share (e.g. during the functional tests)
     * @param string $sharePrefix
     * @return $this
     */
    public function setSharePrefix($sharePrefix)
    {
        if (isset($sharePrefix)) {
            $this->sharePrefix = $sharePrefix;
            $this->docPrefix = null;
        }
        return $this;
    }

    /**
     * Return the directory where Drush's documentation is stored. Usually
     * this is within the Drush application, but some Drush RPM distributions
     * & c. for Linux platforms slice-and-dice the contents and put the docs
     * elsewhere.
     *
     * @return string
     */
    public function docsPath()
    {
        if (!$this->docPrefix) {
            $this->docPrefix = $this->findDocsPath($this->drushBasePath);
        }
        return $this->docPrefix;
    }

    /**
     * Locate the Drush documentation. This is recalculated whenever the
     * share prefix is changed.
     *
     * @param string $drushBasePath
     * @return string
     */
    protected function findDocsPath($drushBasePath)
    {
        $candidates = [
            "$drushBasePath/README.md",
            static::systemPathPrefix($this->sharePrefix, '/usr') . '/share/docs/drush/README.md',
        ];
        return $this->findFromCandidates($candidates);
    }

    /**
     * Check a list of directories and return the first one that exists.
     *
     * @param array $candidates
     * @return string|boolean
     */
    protected function findFromCandidates($candidates)
    {
        foreach ($candidates as $candidate) {
            if (file_exists($candidate)) {
                return dirname($candidate);
            }
        }
        return false;
    }

    /**
     * Return the appropriate system path prefix, unless an override is provided.
     * @param string $override
     * @param string $defaultPrefix
     * @return string
     */
    protected static function systemPathPrefix($override = '', $defaultPrefix = '')
    {
        if ($override) {
            return $override;
        }
        return static::isWindows() ? Path::join(getenv('ALLUSERSPROFILE'), 'Drush') : $defaultPrefix;
    }

    /**
     * Return the system configuration path (default: /etc/drush)
     *
     * @return string
     */
    public function systemConfigPath()
    {
        return static::systemPathPrefix($this->etcPrefix, '') . '/etc/drush';
    }

    /**
     * Return the system shared commandfile path (default: /usr/share/drush/commands)
     *
     * @return string
     */
    public function systemCommandFilePath()
    {
        return static::systemPathPrefix($this->sharePrefix, '/usr') . '/share/drush/commands';
    }

    /**
     * Determine whether current OS is a Windows variant.
     *
     * @return boolean
     */
    public static function isWindows($os = null)
    {
        return strtoupper(substr($os ?: PHP_OS, 0, 3)) === 'WIN';
    }

    /**
     * Verify that we are running PHP through the command line interface.
     *
     * @return boolean
     *   A boolean value that is true when PHP is being run through the command line,
     *   and false if being run through cgi or mod_php.
     */
    public function verifyCLI()
    {
        return (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0));
    }

    /**
     * Calculate the terminal width used for wrapping table output.
     * Normally this is exported using tput in the drush script.
     * If this is not present we do an additional check using stty here.
     * On Windows in CMD and PowerShell is this exported using mode con.
     *
     * @return integer
     */
    public function calculateColumns()
    {
        if ($columns = getenv('COLUMNS')) {
            return $columns;
        }

        // Trying to export the columns using stty.
        exec('stty size 2>&1', $columns_output, $columns_status);
        $matched = false;
        if (!$columns_status && $matched = preg_match('/^\d+\s(\d+)$/', $columns_output[0], $matches, 0)) {
            $columns = $matches[1];
        }

        // If stty fails and Drush is running on Windows are we trying with mode con.
        if (($columns_status || !$matched) && static::isWindows()) {
            $columns_output = [];
            exec('mode con', $columns_output, $columns_status);
            if (!$columns_status && is_array($columns_output)) {
                $columns = (int)preg_replace('/\D/', '', $columns_output[4], -1, $columns_count);
            }
            // TODO: else { 'Drush could not detect the console window width. Set a Windows Environment Variable of COLUMNS to the desired width.'
        }

        // Failling back to default columns value
        if (empty($columns)) {
            $columns = 80;
        }

        // TODO: should we deal with reserve-margin here, or adjust it later?
        return $columns;
    }

    /**
     * Returns the filename for the file that stores the DRUPAL_SITE variable.
     *
     * @param string $filename_prefix
     *   An arbitrary string to prefix the filename with.
     *
     * @return string|false
     *   Returns the full path to temp file if possible, or FALSE if not.
     */
    protected function getSiteSetAliasFilePath($filename_prefix = 'drush-drupal-site-')
    {
        $shell_pid = getenv('DRUSH_SHELL_PID');
        if (!$shell_pid && function_exists('posix_getppid')) {
            $shell_pid = posix_getppid();
        }
        if (!$shell_pid) {
            return false;
        }

        // The env variables below must match the variables in example.prompt.sh
        $tmp = getenv('TMPDIR') ? getenv('TMPDIR') : '/tmp';
        $username = $this->getUsername();

        return "{$tmp}/drush-env-{$username}/{$filename_prefix}" . $shell_pid;
    }
}
