<?php
/**
 * Abstracts operations against Perforce keys.
 * We just extend the counter abstract, tweak the type and implement a set
 * and a delete method which don't accept the force parameter.
 *
 * This class is somewhat unique as calling set will immediately write the new value
 * to perforce; no separate save step is required.
 * When reading values out we do attempt to use cached results, to ensure you read
 * out the value directly from perforce set $force to true when calling get.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */

namespace P4\Key;

use P4\Connection\ConnectionInterface;
use P4\Counter\AbstractCounter;
use P4\Model\Connected;

class Key extends AbstractCounter
{
    const   FETCH_BY_IDS        = 'ids';

    // ensure -u is included to all p4 counter and p4 counters calls
    protected static $flags     = array('-u');

    /**
     * Get all Counters from Perforce.
     *
     * @param   array   $options    optional - array of options to augment fetch behavior.
     *                              supported options are:
     *                                   FETCH_MAXIMUM - set to integer value to limit to the first
     *                                                   'max' number of entries.
     *                                                   Note: Max limit is imposed client side on <2013.1.
     *                                   FETCH_BY_NAME - set to string value to limit to counters
     *                                                   matching the given name/pattern.
     *                                    FETCH_BY_IDS - provide an array of ids to fetch.
     *                                                   not compatible with FETCH_BY_NAME or FETCH_AFTER.
     *                                     FETCH_AFTER - set to an id _after_ which we start collecting
     * @param   ConnectionInterface     $connection  optional - a specific connection to use.
     * @return  Connected\Iterator      all counters matching passed option(s).
     */
    public static function fetchAll($options = array(), ConnectionInterface $connection = null)
    {
        // normalize options to make our lives a bit easier
        $options += array(
            static::FETCH_BY_IDS  => null,
            static::FETCH_AFTER   => null,
            static::FETCH_BY_NAME => null,
            static::FETCH_MAXIMUM => null
        );

        // if fetch by ids wasn't passed just let parent handle it.
        if (!is_array($options[static::FETCH_BY_IDS])) {
            return parent::fetchAll($options, $connection);
        }

        // if no ids were specified just return an empty iterator
        // continuing on would otherwise return everything.
        if (empty($options[static::FETCH_BY_IDS])) {
            return new Connected\Iterator;
        }

        // if the included fetch after or fetch by name blow up, we don't support it.
        if ($options[static::FETCH_AFTER] || $options[static::FETCH_BY_NAME]) {
            throw new \InvalidArgumentException(
                'It is not valid to pass fetch by ids and also specify fetch after or fetch by name.'
            );
        }

        $connection = $connection ?: static::getDefaultConnection();
        $max        = (int) $options[static::FETCH_MAXIMUM];
        $ids        = (array) $options[static::FETCH_BY_IDS];
        $keys       = new Connected\Iterator;
        $params     = static::$flags;

        // Older servers (<13.1) do not support multiple -e args. So we issue one command per ID.
        // Because IDs can contain wildcards we need to enforce max limiting here (client side).
        if (!$connection->isServerMinVersion('2013.1')) {
            $seen = 0;
            foreach ($ids as $id) {
                // populate a key and add it to the iterator
                try {
                    $keys->merge(
                        static::fetchAll(
                            array(
                                static::FETCH_BY_NAME => $id,
                                static::FETCH_MAXIMUM => $max ? $max - $seen : $max
                            ),
                            $connection
                        )
                    );
                } catch (\Exception $e) {
                    // assume id was invalid or key doesn't exist, ignore
                }

                // if max is enabled and we've seen enough; we're done
                $seen = count($keys);
                if ($max && $seen >= $max) {
                    break;
                }
            }

            return $keys;
        }

        // if we made it here our p4d server supports specifying multiple -e args
        // start by setting up all of the args so we can batch them
        $args = array();
        foreach ($ids as $id) {
            $args[] = '-e';
            $args[] = $id;
        }

        // add max to our prefix args if a value has been specified
        if ($max) {
            $params[] = '-m';
            $params[] = $max;
        }

        // run each batch of arguments
        $batches = $connection->batchArgs($args, $params, null, 2);
        foreach ($batches as $batch) {
            // if we have a max update the batch's limit as we progress
            if ($max) {
                $key = array_search('-m', $batch) + 1;
                $batch[$key] = $max - count($keys);
            }

            // execute this batch and process results into keys
            $result = $connection->run('counters', $batch);
            foreach ($result->getData() as $data) {
                // populate a key and add it to the iterator
                try {
                    $key = new static($connection);
                    $key->setId($data['counter']);
                    $key->value = $data['value'];
                } catch (\InvalidArgumentException $e) {
                    // assume id was invalid - ignore.
                    continue;
                }

                $keys[] = $key;
            }
        }

        return $keys;
    }

    /**
     * Set key's value. The value will be immediately written to perforce.
     *
     * @param   mixed   $value  the value to set in the key.
     * @return  Key             provides a fluent interface
     * @throws  Exception       if no Id has been set
     */
    public function set($value)
    {
        return parent::doSet($value);
    }

    /**
     * Delete this key entry.
     *
     * @return  Key             provides a fluent interface
     * @throws  Exception       if no id has been set.
     */
    public function delete()
    {
        return parent::doDelete();
    }
}