<?php

use \EE_CLI\Utils;
use \EE_CLI\Dispatcher;
use \EE_CLI\FileCache;
use \EE_CLI\Process;
use \EE_CLI\WpHttpCacheManager;

/**
 * Various utilities for EE-CLI commands.
 */
class EE_CLI {

	private static $configurator;

	private static $logger;

	private static $hooks = array(), $hooks_passed = array();

	/**
	 * Set the logger instance.
	 *
	 * @param object $logger
	 */
	public static function set_logger( $logger ) {
		self::$logger = $logger;
	}

	/**
	 * Get the Configurator instance
	 *
	 * @return \EE_CLI\Configurator
	 */
	public static function get_configurator() {
		static $configurator;

		if ( !$configurator ) {
			$configurator = new EE_CLI\Configurator( EE_CLI_ROOT . '/php/config-spec.php' );
		}

		return $configurator;
	}

	public static function get_root_command() {
		static $root;

		if ( !$root ) {
			$root = new Dispatcher\RootCommand;
		}

		return $root;
	}

	public static function get_runner() {
		static $runner;

		if ( !$runner ) {
			$runner = new EE_CLI\Runner;
		}

		return $runner;
	}

	/**
	 * @return FileCache
	 */
	public static function get_cache() {
		static $cache;

		if ( !$cache ) {
			$home = getenv( 'HOME' );
			if ( !$home ) {
				// sometime in windows $HOME is not defined
				$home = getenv( 'HOMEDRIVE' ) . getenv( 'HOMEPATH' );
			}
			$dir = getenv( 'EE_CLI_CACHE_DIR' ) ? : "$home/.EE-CLI/cache";

			// 6 months, 300mb
			$cache = new FileCache( $dir, 15552000, 314572800 );

			// clean older files on shutdown with 1/50 probability
			if ( 0 === mt_rand( 0, 50 ) ) {
				register_shutdown_function( function () use ( $cache ) {
					$cache->clean();
				} );
			}
		}

		return $cache;
	}

	/**
	 * Set the context in which EE-CLI should be run
	 */
	public static function set_url( $url ) {
		EE_CLI::debug( 'Set URL: ' . $url );
		$url_parts = Utils\parse_url( $url );
		self::set_url_params( $url_parts );
	}

	private static function set_url_params( $url_parts ) {
		$f = function( $key ) use ( $url_parts ) {
			return \EE_CLI\Utils\get_flag_value( $url_parts, $key, '' );
		};

		if ( isset( $url_parts['host'] ) ) {
			if ( isset( $url_parts['scheme'] ) && 'https' === strtolower( $url_parts['scheme'] ) ) {
				$_SERVER['HTTPS'] = 'on';
			}

			$_SERVER['HTTP_HOST'] = $url_parts['host'];
			if ( isset( $url_parts['port'] ) ) {
				$_SERVER['HTTP_HOST'] .= ':' . $url_parts['port'];
			}

			$_SERVER['SERVER_NAME'] = $url_parts['host'];
		}

		$_SERVER['REQUEST_URI'] = $f('path') . ( isset( $url_parts['query'] ) ? '?' . $url_parts['query'] : '' );
		$_SERVER['SERVER_PORT'] = \EE_CLI\Utils\get_flag_value( $url_parts, 'port', '80' );
		$_SERVER['QUERY_STRING'] = $f('query');
	}

	/**
	 * @return WpHttpCacheManager
	 */
	public static function get_http_cache_manager() {
		static $http_cacher;

		if ( !$http_cacher ) {
			$http_cacher = new WpHttpCacheManager( self::get_cache() );
		}

		return $http_cacher;
	}

	public static function colorize( $string ) {
		return \cli\Colors::colorize( $string, self::get_runner()->in_color() );
	}

	/**
	 * Schedule a callback to be executed at a certain point (before WP is loaded).
	 */
	public static function add_hook( $when, $callback ) {
		if ( in_array( $when, self::$hooks_passed ) )
			call_user_func( $callback );

		self::$hooks[ $when ][] = $callback;
	}

	/**
	 * Execute registered callbacks.
	 */
	public static function do_hook( $when ) {
		self::$hooks_passed[] = $when;

		if ( !isset( self::$hooks[ $when ] ) )
			return;

		foreach ( self::$hooks[ $when ] as $callback ) {
			call_user_func( $callback );
		}
	}

	/**
	 * Add a command to the EE-CLI list of commands
	 *
	 * @param string $name The name of the command that will be used in the CLI
	 * @param string $class The command implementation
	 * @param array $args An associative array with additional parameters:
	 *   'before_invoke' => callback to execute before invoking the command
	 */
	public static function add_command( $name, $class, $args = array() ) {
		if ( is_string( $class ) && ! class_exists( (string) $class ) ) {
			EE_CLI::error( sprintf( "Class '%s' does not exist.", $class ) );
		}

		if ( isset( $args['before_invoke'] ) ) {
			self::add_hook( "before_invoke:$name", $args['before_invoke'] );
		}

		$path = preg_split( '/\s+/', $name );

		$leaf_name = array_pop( $path );
		$full_path = $path;

		$command = self::get_root_command();

		while ( !empty( $path ) ) {
			$subcommand_name = $path[0];
			$subcommand = $command->find_subcommand( $path );

			// create an empty container
			if ( !$subcommand ) {
				$subcommand = new Dispatcher\CompositeCommand( $command, $subcommand_name,
					new \EE_CLI\DocParser( '' ) );
				$command->add_subcommand( $subcommand_name, $subcommand );
			}

			$command = $subcommand;
		}

		$leaf_command = Dispatcher\CommandFactory::create( $leaf_name, $class, $command );

		if ( ! $command->can_have_subcommands() ) {
			throw new Exception( sprintf( "'%s' can't have subcommands.",
				implode( ' ' , Dispatcher\get_path( $command ) ) ) );
		}

		$command->add_subcommand( $leaf_name, $leaf_command );
	}

	/**
	 * Display a message in the CLI and end with a newline
	 *
	 * @param string $message
	 */
	public static function line( $message = '' ) {
		echo $message . "\n";
	}

	/**
	 * Log an informational message.
	 *
	 * @param string $message
	 */
	public static function log( $message ) {
		self::$logger->info( $message );
	}

	/**
	 * Display a success in the CLI and end with a newline
	 *
	 * @param string $message
	 */
	public static function success( $message ) {
		self::$logger->success( $message );
	}

	/**
	 * Log debug information
	 *
	 * @param string $message
	 */
	public static function debug( $message ) {
		self::$logger->debug( self::error_to_string( $message ) );
	}

	/**
	 * Display a warning in the CLI and end with a newline
	 *
	 * @param string $message
	 */
	public static function warning( $message ) {
		self::$logger->warning( self::error_to_string( $message ) );
	}

	/**
	 * Display an error in the CLI and end with a newline
	 *
	 * @param string|EE_Error $message
	 * @param bool            $exit    if true, the script will exit()
	 */
	public static function error( $message, $exit = true ) {
		if ( ! isset( self::get_runner()->assoc_args[ 'completions' ] ) ) {
			self::$logger->error( self::error_to_string( $message ) );
		}

		if ( $exit ) {
			exit(1);
		}
	}

	/**
	 * Display an error in the CLI and end with a newline
	 *
	 * @param array $message  each element from the array will be printed on its own line
	 */
	public static function error_multi_line( $message_lines ) {
		if ( ! isset( self::get_runner()->assoc_args[ 'completions' ] ) && is_array( $message_lines ) ) {
			self::$logger->error_multi_line( array_map( array( __CLASS__, 'error_to_string' ), $message_lines ) );
		}
	}

	/**
	 * Ask for confirmation before running a destructive operation.
	 */
	public static function confirm( $question, $assoc_args = array() ) {
		if ( ! \EE_CLI\Utils\get_flag_value( $assoc_args, 'yes' ) ) {
			fwrite( STDOUT, $question . " [y/n] " );

			$answer = trim( fgets( STDIN ) );

			if ( 'y' != $answer )
				exit;
		}
	}

	/**
	 * Read value from a positional argument or from STDIN.
	 *
	 * @param array $args The list of positional arguments.
	 * @param int $index At which position to check for the value.
	 *
	 * @return string
	 */
	public static function get_value_from_arg_or_stdin( $args, $index ) {
		if ( isset( $args[ $index ] ) ) {
			$raw_value = $args[ $index ];
		} else {
			// We don't use file_get_contents() here because it doesn't handle
			// Ctrl-D properly, when typing in the value interactively.
			$raw_value = '';
			while ( ( $line = fgets( STDIN ) ) !== false ) {
				$raw_value .= $line;
			}
		}

		return $raw_value;
	}

	/**
	 * Read a value, from various formats.
	 *
	 * @param mixed $value
	 * @param array $assoc_args
	 */
	public static function read_value( $raw_value, $assoc_args = array() ) {
		if ( \EE_CLI\Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) {
			$value = json_decode( $raw_value, true );
			if ( null === $value ) {
				EE_CLI::error( sprintf( 'Invalid JSON: %s', $raw_value ) );
			}
		} else {
			$value = $raw_value;
		}

		return $value;
	}

	/**
	 * Display a value, in various formats
	 *
	 * @param mixed $value
	 * @param array $assoc_args
	 */
	public static function print_value( $value, $assoc_args = array() ) {
		if ( \EE_CLI\Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) {
			$value = json_encode( $value );
		} elseif ( is_array( $value ) || is_object( $value ) ) {
			$value = var_export( $value );
		}

		echo $value . "\n";
	}

	/**
	 * Convert a ee_error into a string
	 *
	 * @param mixed $errors
	 * @return string
	 */
	public static function error_to_string( $errors ) {
		if ( is_string( $errors ) ) {
			return $errors;
		}

		if ( is_object( $errors ) && is_a( $errors, 'EE_Error' ) ) {
			foreach ( $errors->get_error_messages() as $message ) {
				if ( $errors->get_error_data() )
					return $message . ' ' . $errors->get_error_data();
				else
					return $message;
			}
		}
	}

	/**
	 * Launch an external process that takes over I/O.
	 *
	 * @param string Command to call
	 * @param bool Whether to exit if the command returns an error status
	 * @param bool Whether to return an exit status (default) or detailed execution results
	 *
	 * @return int|ProcessRun The command exit status, or a ProcessRun instance
	 */
	public static function launch( $command, $exit_on_error = true, $return_detailed = false ) {

		$proc = Process::create( $command );
		$results = $proc->run();

		if ( $results->return_code && $exit_on_error )
			exit( $results->return_code );

		if ( $return_detailed ) {
			return $results;
		} else {
			return $results->return_code;
		}
	}

	/**
	 * Launch another EE-CLI command using the runtime arguments for the current process
	 *
	 * @param string Command to call
	 * @param array $args Positional arguments to use
	 * @param array $assoc_args Associative arguments to use
	 * @param bool Whether to exit if the command returns an error status
	 * @param bool Whether to return an exit status (default) or detailed execution results
	 * @param array $runtime_args Override one or more global args (path,url,user,allow-root)
	 *
	 * @return int|ProcessRun The command exit status, or a ProcessRun instance
	 */
	public static function launch_self( $command, $args = array(), $assoc_args = array(), $exit_on_error = true, $return_detailed = false, $runtime_args = array() ) {
		$reused_runtime_args = array(
			'path',
			'url',
			'user',
			'allow-root',
		);

		foreach ( $reused_runtime_args as $key ) {
			if ( isset( $runtime_args[ $key ] ) ) {
				$assoc_args[ $key ] = $runtime_args[ $key ];
			} else if ( $value = self::get_runner()->config[ $key ] )
				$assoc_args[ $key ] = $value;
		}

		$php_bin = self::get_php_binary();

		$script_path = $GLOBALS['argv'][0];

		$args = implode( ' ', array_map( 'escapeshellarg', $args ) );
		$assoc_args = \EE_CLI\Utils\assoc_args_to_str( $assoc_args );

		$full_command = "{$php_bin} {$script_path} {$command} {$args} {$assoc_args}";

		return self::launch( $full_command, $exit_on_error, $return_detailed );
	}

	/**
	 * Get the path to the PHP binary used when executing EE-CLI.
	 * Environment values permit specific binaries to be indicated.
	 *
	 * @return string
	 */
	public static function get_php_binary() {
		if ( defined( 'PHP_BINARY' ) )
			return PHP_BINARY;

		if ( getenv( 'EE_CLI_PHP_USED' ) )
			return getenv( 'EE_CLI_PHP_USED' );

		if ( getenv( 'EE_CLI_PHP' ) )
			return getenv( 'EE_CLI_PHP' );

		return 'php';
	}

	public static function get_config( $key = null ) {
		if ( null === $key ) {
			return self::get_runner()->config;
		}

		if ( !isset( self::get_runner()->config[ $key ] ) ) {
			self::warning( "Unknown config option '$key'." );
			return null;
		}

		return self::get_runner()->config[ $key ];
	}

	/**
	 * Run a given command within the current process using the same global parameters.
	 *
	 * To run a command using a new process with the same global parameters, use EE_CLI::launch_self()
	 * To run a command using a new process with different global parameters, use EE_CLI::launch()
	 *
	 * @param array
	 * @param array
	 */
	public static function run_command( $args, $assoc_args = array() ) {
		self::get_runner()->run_command( $args, $assoc_args );
	}



	// DEPRECATED STUFF

	public static function add_man_dir() {
		trigger_error( 'EE_CLI::add_man_dir() is deprecated. Add docs inline.', E_USER_WARNING );
	}

	// back-compat
	public static function out( $str ) {
		fwrite( STDOUT, $str );
	}

	// back-compat
	public static function addCommand( $name, $class ) {
		trigger_error( sprintf( 'ee %s: %s is deprecated. use EE_CLI::add_command() instead.',
			$name, __FUNCTION__ ), E_USER_WARNING );
		self::add_command( $name, $class );
	}
}