<?php
/**
 * Filters an array and extracts and validates command line arguments
 *
 * @author Patrick Forget <patforg at webtrendi.com>
 */

namespace Clapp;

/**
 * Filters an array and extracts and validates command line arguments
 *
 * @author Patrick Forget <patforg at webtrendi.com>
 */
class CommandArgumentFilter
{
    /**
     * Command line arguments
     * @var array
     */
    private $arguments = array();
    
    /**
     * Definition of allowed parameters
     * @var \Clapp\CommandLineArgumentDefinition
     */
    private $definitions = null;

    /**
     * Flag if arguments have been parsed in to params
     * @var boolean
     */
    private $parsed = false;

    /**
     * Parsed params
     * @var array
     */
    private $params = array();

    /**
     * Trailing values
     * @var string
     */
    private $trailingValues = "";
    
    /**
     * program name
     * @var string
     */
    private $programName = "";

    /**
     * class constructor
     *
     * @author Patrick Forget <patforg at webtrendi.com>
     *
     * @param \Clapp\CommandLineDefinition $definitions contains list of allowed parameters
     * @param array $args list of arguments to filter.
     */
    public function __construct(\Clapp\CommandLineArgumentDefinition $definitions, $args)
    {
        if (is_array($args)) {
            $this->arguments = $args;
        } //if

        $this->definitions = $definitions;
    } // __construct()

    /**
     * returns parameter matching provided name
     *
     * @author Patrick Forget <patforg at webtrendi.com>
     *
     * @param string name of the paramter to retreive
     *
     * @return mixed if param the param appears only once the method will
     *     return 1 if the parameter doesn't take a value. The specified value
     *     for that param will returned if it does take value.
     *
     *     If many occurence of the param appear the number of occurences will
     *     be returned for params that do not take values. An array of values
     *     will be returned for the parameters that do take values.
     *
     *     If the parameter is not present null if it takes a value and false if
     *     it's not present and doesn't allow values
     */
    public function getParam($name)
    {
        if (!$this->parsed) {
            $this->parseParams();
        } //if

        $longName = strlen($name) === 1 ? $this->definitions->getLongName($name) : $name;
        if (isset($this->params[$longName])) {
            return $this->params[$longName];
        } else {
            if ($this->definitions->allowsValue($longName)) {
                return null;
            } else {
                return false;
            } //if
        } //if

    } // getParam()

    /**
     * retreive the program name
     *
     * @author Patrick Forget <patforg at webtrendi.com>
     */
    public function getProgramName()
    {
        if (!$this->parsed) {
            $this->parseParams();
        } //if

        return $this->programName;
    } // getProgramName()
    
    /**
     * retreive the trailing values
     *
     * @author Patrick Forget <patforg at webtrendi.com>
     */
    public function getTrailingValues()
    {
        if (!$this->parsed) {
            $this->parseParams();
        } //if

        return $this->trailingValues;
    } // getTrailingValues()

    /**
     * extracts params from arguments
     *
     * @author Patrick Forget <patforg at webtrendi.com>
     */
    protected function parseParams()
    {

        $argumentStack = $this->arguments;

        $expectingValue = false;
        $currentLongName = null;
        $currentValue = null;
        $trailingValues = "";
        $endOfDashedArguments = false;
        $addParam = false;
        $argumentsLeft = sizeof($argumentStack);
        $multiShortParams = array();

        $this->programName = array_shift($argumentStack); // remove first argument which is the program name

        while ($currentArgument = array_shift($argumentStack)) {
            $argumentsLeft--;
            $currentArgumentLength = strlen($currentArgument);

            // arguments that don't start with a dash
            if (substr($currentArgument, 0, 1) !== '-') {
                if ($expectingValue) {
                    $currentValue = $currentArgument;
                    $addParam = true;
                } else {
                    $trailingValues .= " ". $currentArgument;
                    $endOfDashedArguments = true;
                } //if

            // double dash detected
            } elseif (substr($currentArgument, 1, 1)  === '-') {
                if ($expectingValue) {
                    throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values");
                } //if

                /* stop parsing arguments if double dash
                   only param is encountered  */
                if ($currentArgumentLength == 2) {
                    if ($trailingValues !== "") {
                        throw new \UnexpectedValueException("Trailing values must appear after double dash");
                    } //if

                    $trailingValues = " ". implode(" ", $argumentStack);
                    $argumentStack = array();
                    $endOfDashedArguments = true;
                    break;
                } //if

                $longNameParts = explode("=", substr($currentArgument, 2), 2);

                $currentLongName = $longNameParts[0];

                if (sizeof($longNameParts) > 1) {
                    $currentValue = $longNameParts[1];
                    $addParam = true;
                } elseif ($this->definitions->allowsValue($currentLongName)) {
                    $expectingValue = true;
                } else {
                    $addParam = true;
                } //if

            // single dash
            } else {
                if ($expectingValue) {
                    throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values");
                } //if

                $shortNameParts = explode("=", substr($currentArgument, 1), 2);

                $shortName = $shortNameParts[0];

                if (strlen($shortName) <= 1) {
                    $currentLongName = $this->definitions->getLongName($shortName);

                    if ($currentLongName === null) {
                        throw new \InvalidArgumentException("Unable to find name with ".
                            "provided parameter ({$shortName})");
                    } //if

                    if (sizeof($shortNameParts) > 1) {
                        $currentValue = $shortNameParts[1];
                        $addParam = true;
                    } elseif ($this->definitions->allowsValue($currentLongName)) {
                        $expectingValue = true;
                    } else {
                        $addParam = true;
                    } //if

                } else {
                    $multiShortParams = str_split($shortName);

                    /* process the last one (which is the only one that can have a value) */
                    $lastParam = array_pop($multiShortParams);
                    $currentLongName = $this->definitions->getLongName($lastParam);
                    if (sizeof($shortNameParts) > 1) {
                        $currentValue = $shortNameParts[1];
                        $addParam = true;
                    } elseif ($this->definitions->allowsValue($lastParam)) {
                        $expectingValue = true;
                    } else {
                        $addParam = true;
                    } //if

                } //if
                
            } //if

            if ($addParam) {
                if ($endOfDashedArguments) {
                    throw new \UnexpectedValueException("Unexpected argument after undashed values");
                } //if

                /* Not sure how this could happen */
                // @codeCoverageIgnoreStart
                if ($currentLongName === false || $currentLongName === null) {
                    throw new \UnexpectedValueException("Missing argument name");
                } //if
                // @codeCoverageIgnoreEnd

                if (!$this->definitions->paramExists($currentLongName)) {
                    throw new \InvalidArgumentException("Invalid argument name");
                } //if

                $allowsMultiple = $this->definitions->allowsMultiple($currentLongName);
                $allowsValue = $this->definitions->allowsValue($currentLongName);

                if (isset($this->params[$currentLongName]) && !$allowsMultiple) {
                    throw new \UnexpectedValueException("Multiple instace of parameter {$currentLongName} not allowed");
                } //if

                if ($allowsValue) {
                    /* Missing values should always be detected before addParam is true */
                    // @codeCoverageIgnoreStart
                    if ($currentValue === null) {
                        throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values");
                    } //if
                    // @codeCoverageIgnoreEnd

                } elseif ($currentValue !== null) {
                    throw new \UnexpectedValueException("Parameter {$currentLongName} does not accept values");

                } else {
                    $currentValue = true;
                } //if

                if ($allowsMultiple) {
                    if ($allowsValue) {
                        if (!isset($this->params[$currentLongName])) {
                            $this->params[$currentLongName] = array();
                        } //if

                        $this->params[$currentLongName][] = $currentValue;

                    } else {
                        if (!isset($this->params[$currentLongName])) {
                            $this->params[$currentLongName] = 0;
                        } //if

                        $this->params[$currentLongName]++;

                    } //if

                } else {
                    $this->params[$currentLongName] = $currentValue;
                } //if

                foreach ($multiShortParams as $shortName) {
                    $argumentStack[] = "-{$shortName}";
                    $argumentsLeft++;
                } //foreach

                /* reset stuff for next param */
                $expectingValue = false;
                $currentLongName = null;
                $currentValue = null;
                $addParam = false;
                $multiShortParams = array();

            } //if

        } //while

        if ($expectingValue !== false) {
            throw new \UnexpectedValueException("Parameter {$currentLongName} expects a values");
        } //if

        /* Not sure how this could happen */
        // @codeCoverageIgnoreStart
        if ($currentLongName !== null ||
            $addParam !== false ||
            $currentValue !== null ||
            sizeof($multiShortParams) !== 0) {
            throw new \UnexpectedValueException("Unable to process some parameters");
        } //if
        // @codeCoverageIgnoreEnd

        if ($trailingValues !== "") {
            $this->trailingValues = substr($trailingValues, 1); // remove extra space at the begging
        } //if

        $this->parsed = true;
    } // parseParams()
}
