<?php
/**
 * Abstracts operations against the Perforce protections table.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */

namespace P4\Spec;

class Protections extends SingularAbstract
{
    const SPEC_TYPE         = 'protect';

    protected $fields       = array(
        'Protections'   => array(
            'accessor'  => 'getProtections',
            'mutator'   => 'setProtections'
        )
    );

    /**
     * Get protections in array form. Format of array is as follows:
     *
     *    array (
     *      array (
     *        'mode' => 'super',
     *        'type' => 'user',
     *        'name' => '*',
     *        'host' => '*',
     *        'path' => '//...'
     *      )
     *    )
     *
     * @return  array   array of protect entries.
     */
    public function getProtections()
    {
        $protections = array();
        foreach ((array) $this->getRawValue('Protections') as $line) {
            $protections[] = $this->toProtectArray($line);
        }

        return $protections;
    }

    /**
     * Set protections table entries.
     *
     * Individual protection entries may be specified in array or
     * raw string format for convienence. See getProtections() for format.
     *
     * @param   array   $protections    array of protect entries in array or raw string format.
     * @return  Protections     provides a fluent interface.
     */
    public function setProtections($protections)
    {
        if (!is_array($protections)) {
            throw new \InvalidArgumentException(
                'Protections must be passed as an array'
            );
        }

        $strings = array();
        foreach ($protections as $protect) {
            // Normalize protection entries to array format for validation
            if (is_string($protect)) {
                $protect = $this->toProtectArray($protect);
            }

            $strings[] = $this->fromProtectArray($protect);
        }

        $this->setRawValue('Protections', $strings);

        return $this;
    }

    /**
     * Add a protection table entry.
     *
     * @param   string  $mode   the access level (e.g. read, write, super, etc.)
     * @param   string  $type   the type of protection (user or group)
     * @param   string  $name   the user or group name
     * @param   string  $host   the host restriction
     * @param   string  $path   the path to apply the protection to.
     * @return  Protections     provides a fluent interface.
     * @throws  \InvalidArgumentException   if any inputs are invalid.
     */
    public function addProtection($mode, $type, $name, $host, $path)
    {
        if (!is_string($mode)
            || !is_string($type)
            || !is_string($name)
            || !is_string($host)
            || !is_string($path)
        ) {
            throw new \InvalidArgumentException(
                "Cannot add protection. All parameters must be in string form."
            );
        }

        // add to protections array.
        $protections   = $this->getRawValue('Protections');
        $protections[] = $this->fromProtectArray(
            array(
                'mode' => $mode,
                'type' => $type,
                'name' => $name,
                'host' => $host,
                'path' => $path,
            )
        );

        $this->setRawValue('Protections', $protections);

        return $this;
    }

    /**
     * Remove a protection table entry.
     *
     * @param   string  $mode   the access level (e.g. read, write, super, etc.)
     * @param   string  $type   the type of protection (user or group)
     * @param   string  $name   the user or group name
     * @param   string  $host   the host restriction
     * @param   string  $path   the path to apply the protection to.
     * @return  Protections     provides a fluent interface.
     * @throws  \InvalidArgumentException   if any inputs are invalid.
     */
    public function removeProtection($mode, $type, $name, $host, $path)
    {
        if (!is_string($mode)
            || !is_string($type)
            || !is_string($name)
            || !is_string($host)
            || !is_string($path)
        ) {
            throw new \InvalidArgumentException(
                "Cannot add protection. All parameters must be in string form."
            );
        }

        $removeArray  = array(
            'mode' => $mode,
            'type' => $type,
            'name' => $name,
            'host' => $host,
            'path' => $path,
        );

        if (!$this->isValidProtectArray($removeArray)) {
            throw new \InvalidArgumentException(
                'Protection array entry is invalid.'
            );
        }

        // When retrieving the protects value from the depot, the path is not
        // encapsulated in quotes, so fromProtectArray cannot be used in this case.
        $remove = $removeArray['mode'] ." ".
                  $removeArray['type'] ." ".
                  $removeArray['name'] ." ".
                  $removeArray['host'] ." ".
                  $removeArray['path'] ;

        $protections = $this->getRawValue('Protections');
        $key = array_search($remove, $protections);
        if ($key !== false) {
            unset($protections[$key]);
        }

        $this->setRawValue('Protections', $protections);

        return $this;
    }

    /**
     * Convert a raw protections string (single entry) into an array,
     * see getProtections for format.
     *
     * @param   string  $entry  A single protection entry in string format
     * @return  array   A single protect entry array
     * @throws  \InvalidArgumentException   If passed string is unparsable
     */
    protected function toProtectArray($entry)
    {
        $keys       = array('mode', 'type', 'name', 'host', 'path');
        $protection = str_getcsv($entry, ' ');

        if (count($protection) != count($keys)) {
            throw new \InvalidArgumentException(
                'Protection entry with missing field(s) encountered'
            );
        }

        return array_combine($keys, $protection);
    }

    /**
     * Convert a protection array (single entry) into a string, see
     * getProtections for format. Will validate input array and throw
     * on errors.
     *
     * @param   array   $array  The single protection entry to validate and convert to string
     * @return  string  A single protections entry in string format
     * @throws  \InvalidArgumentException   If input is poorly formatted
     */
    protected function fromProtectArray($array)
    {
        // Validate the array, will throw if invalid
        if (!$this->isValidProtectArray($array)) {
            throw new \InvalidArgumentException(
                'Protection array entry is invalid.'
            );
        }

        $protect =          $array['mode'] ." ".
                            $array['type'] ." ".
                            $array['name'] ." ".
                            $array['host'] ." ".
                    '"'.    $array['path'] .'"';

        return $protect;
    }

    /**
     * Validates a single protection entry in array format, see getProtections
     * for format details.
     *
     * @param   array   $array  A single protect entry in array format
     * @return  bool    True - Valid, False - Error(s) found
     */
    protected function isValidProtectArray($array)
    {
        if (!is_array($array)) {
            return false;
        }

        // Validate all 'word' fields are present and don't contain spaces
        $fields  = array('mode', 'type', 'name', 'host');
        foreach ($fields as $key) {
            if (!array_key_exists($key, $array) ||
                trim($array[$key]) === '' ||
                preg_match('/\s/', $array[$key])) {
                return false;
            }
        }

        // Validate 'path' field is present, spaces are permitted
        if (!array_key_exists('path', $array) || trim($array['path']) === '') {
            return false;
        }

        return true;
    }
}