<?php
/*   _            _          _ ____   ___  _____
 *  | |          | |        | |___ \ / _ \| ____|
 *  | |      __ _| |__   ___| | __) | | | | |__
 *  | |     / _` | '_ \ / _ \ ||__ <|  -  |___ \
 *  | |____| (_| | |_) |  __/ |___) |     |___) |
 *  |______|\__,_|_.__/ \___|_|____/ \___/|____/
 *
 *  Copyright Label305 B.V. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Label305\AujaLaravel\Config;

use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Log;
use Label305\AujaLaravel\Database\DatabaseHelper;

/**
 * A class which determines properties and relations for models.
 *
 * @package Label305\AujaLaravel
 */
class AujaConfigurator {

    const ID_SUFFIX = '_id';

    /**
     * @var Application
     */
    private $app;

    /**
     * @var DatabaseHelper the DatabaseRepository which provides information about the database.
     */
    private $databaseRepository;

    /**
     * @var array a key-value pair of model names and the Model instances.
     */
    private $models = [];

    /**
     * @var Relation[][] A key-value pair of model names and an array of their relations.
     */
    private $relations = [];

    /**
     * @var ModelConfig[] A key-value pair of model names and their generated configs.
     */
    private $configs = [];

    /**
     * Creates a new AujaConfigurator.
     *
     * @param Application    $app
     * @param DatabaseHelper $databaseHelper
     */
    public function __construct(Application $app, DatabaseHelper $databaseHelper) {
        $this->app = $app;
        $this->databaseRepository = $databaseHelper;
    }

    /**
     * Defines Models, Columns and Relations between the Models.
     * This method should be called before using any other methods.
     *
     * @param  String[] $modelConfigurations an array of full namespaces of ModelConfig objects of Eloquent objects
     */
    public function configure(array $modelConfigurations) {
        if (empty($modelConfigurations)) {
            throw new \LogicException('Supply at least one model or model configuration!');
        }

        /* First define the models and their columns. */
        foreach ($modelConfigurations as $className) {

            $modelClass = $className;

            if (is_subclass_of($className, 'Illuminate\Database\Eloquent\Model')) {
                $config = new ModelConfig();
                $config->setModelClass($modelClass);
            }
            else if (is_subclass_of($className, 'Label305\AujaLaravel\Config\ModelConfig')) {
                $config = new $className();
                $modelClass = $config->getModelClass();
            }
            else {
                throw new \InvalidArgumentException(
                    "Model configuration should be class name string of either a ModelConfig or Eloquent subclass."
                );
            }

            $model = new Model($modelClass);
            $this->models[$modelClass] = $model;
            $this->relations[$modelClass] = [];

            $configResolver = new ConfigResolver($config, $model);
            $this->configs[$modelClass] = $configResolver->resolve();
            $this->findColumns($this->models[$modelClass]);
            $this->configs[$modelClass] = $configResolver->resolve(); // TODO: Find a workaround for doing this twice.
        }

        /* Find relations */
        $this->findRelations(array_values($this->models));
    }

    /**
     * @return Model[] the array of Model instances.
     */
    public function getModels() {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        return array_values($this->models);
    }

    /**
     * @param $modelName String the name of the model.
     *
     * @return Model the Model corresponding to given Model name.
     */
    public function getModel($modelName) {
        if (!isset($this->models[$modelName])) {
            throw new \LogicException(sprintf('Model for name %s doesn\'t exist!', $modelName));
        }

        return $this->models[$modelName];
    }

    /**
     * @return Relation[] A key-value pair of model names and an array of their relations.
     */
    public function getRelations() {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        return $this->relations;
    }

    /**
     * Returns an array of `Relations` for given `Model`.
     *
     * @param Model $model The `Model`.
     *
     * @return Relation[] The `Relations`.
     */
    public function getRelationsForModel(Model $model) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        return !is_null($model) && isset($this->relations[$model->getName()]) ? $this->relations[$model->getName()] : [];
    }

    /**
     * Finds the name of the table for given Model, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model  The `Model` to find the table name for.
     * @param ModelConfig $config The `ModelConfig` to use for retrieving the table name.
     *
     * @return String The name of table for given Model.
     */
    public function getTableName(Model $model, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = null;
        if ($config != null && $config->getTableName() != null) {
            $result = $config->getTableName();
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->getTableName();
        }

        return $result;
    }

    /**
     * Finds which display name to use for given Model, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model  The `Model` to find the display name for.
     * @param ModelConfig $config The `ModelConfig` to use for retrieving the display name.
     *
     * @return String The name to display for the `Model`.
     */
    public function getDisplayName(Model $model, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = null;
        if ($config != null && $config->getDisplayName() != null) {
            $result = $config->getDisplayName();
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->getDisplayName();
        }
        return $result;
    }

    /**
     * Finds which display name to use for given Model, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model      The model to find the display name for.
     * @param String      $columnName The name of the column to find the display name for.
     * @param ModelConfig $config     The `ModelConfig` to use for retrieving the display name.
     *
     * @return String The name to display for the `Model`.
     */
    public function getColumnDisplayName(Model $model, $columnName, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = null;
        if ($config != null && $config->getColumnDisplayName($columnName) != $columnName) {
            $result = $config->getColumnDisplayName($columnName);
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->getColumnDisplayName($columnName);
        }
        return $result;
    }

    /**
     * Finds which display field to use for given Model, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model  The `Model` to find the display field for.
     * @param ModelConfig $config The `ModelConfig` to use for retrieving the display field.
     *
     * @return String The name of the field to use for displaying the `Model`.
     */
    public function getDisplayField(Model $model, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = null;
        if ($config != null && $config->getDisplayField() != null) {
            $result = $config->getDisplayField();
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->getDisplayField();
        }
        return $result;
    }

    /**
     * Finds the icon to use for given `Model`, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model  The `Model` to find the icon for.
     * @param ModelConfig $config The `ModelConfig` to use for retrieving the icon.
     *
     * @return String The name of the icon.
     */
    public function getIcon(Model $model, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = null;
        if ($config != null && $config->getIcon() != null) {
            $result = $config->getIcon();
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->getIcon();
        }
        return $result;
    }

    /**
     * Finds the visible fields to show for given `Model`, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model  The `Model` to find the visible fields for.
     * @param modelConfig $config The `ModelConfig` to use for retrieving the visible fields.
     *
     * @return \String[] An array of names of the visible fields.
     */
    public function getVisibleFields(Model $model, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = null;
        if ($config != null && $config->getVisibleFields() != null) {
            $result = $config->getVisibleFields();
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->getVisibleFields();
        }
        return $result;
    }

    /**
     * Returns whether given model should be included in the main view, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model  The `Model` to check.
     * @param ModelConfig $config The `ModelConfig` to use for checking.
     *
     * @return bool `true` if the model should be included in main.
     */
    public function shouldSmartIncludeInMain(Model $model, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = false;
        if ($config != null) {
            $result = $config->getSmartIncludeInMain();
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->getSmartIncludeInMain();
        }
        return $result;
    }

    /**
     * Returns whether given model is searchable, using the `ModelConfig` class for the `Model`.
     * Uses the overridden value if present, or falls back to the generated value.
     *
     * @param Model       $model  The `Model` to check.
     * @param ModelConfig $config The `ModelConfig` to use for checking.
     *
     * @return bool `true` if the model is searchable.
     */
    public function isSearchable(Model $model, ModelConfig $config = null) {
        if (empty($this->models)) {
            throw new \LogicException('AujaConfigurator not configured yet! Call configure first.');
        }

        if (!isset($this->configs[$model->getName()])) {
            throw new \LogicException(sprintf('AujaConfigurator not configured for model %s', $model->getName()));
        }

        $result = false;
        if ($config != null) {
            $result = $config->isSearchable();
        } else {
            $modelConfig = $this->configs[$model->getName()];
            $result = $modelConfig->isSearchable();
        }
        return $result;
    }

    /**
     * Finds and configures the `Columns` for given `Model`.
     *
     * @param Model $model the Model to find the `Columns` for.
     */
    private function findColumns(Model $model) {
        //$this->app['log']->debug('Finding columns for model ' . $model->getName());
        $tableName = $this->getTableName($model);

        if (!$this->databaseRepository->hasTable($tableName)) {
            throw new \InvalidArgumentException(sprintf('Table %s for %s does not exist!', $tableName, $model->getName()));
        }

        $columns = $this->databaseRepository->getColumnListing($tableName);
        foreach ($columns as $columnName) {
            //$this->app['log']->debug(sprintf('Adding column %s to %s', $columnName, $model->getName()));
            $columnType = $this->databaseRepository->getColumnType($tableName, $columnName);
            $model->addColumn(new Column($columnName, $columnType));
        }
    }

    /**
     * @param Model[] $models Finds Relations for given Models.
     */
    private function findRelations(array $models) {
        foreach ($models as $model) {
            $this->findSimpleRelations($model);
        }

        // TODO: Can this be prettier?
        /* Find BELONGS_TO relations which do not have a BELONGS_TO reverse relation. These need to have a HAS_MANY reverse relation. */
        foreach ($this->relations as $modelRelations) {
            foreach ($modelRelations as $relation) {
                if ($relation->getType() == Relation::BELONGS_TO) {
                    /* Try to find a matching reversed relation */
                    $otherModelRelations = &$this->relations[$relation->getRight()->getName()];
                    $hasReverseRelation = false;
                    foreach ($otherModelRelations as $otherModelRelation) {
                        if ($otherModelRelation->getRight() == $relation->getLeft()) {
                            $hasReverseRelation = true;
                        }
                    }
                    if (!$hasReverseRelation) {
                        /* There is no one-to-one relation. Define a hasMany relation. */
                        $otherModelRelations[] = new Relation($relation->getRight(), $relation->getLeft(), Relation::HAS_MANY);
                    }
                }
            }
        }

        $this->findManyToManyRelations(array_values($this->models));
    }

    /**
     * Finds and defines one-to-one and one-to-many relations for given model.
     *
     * @param $model Model the Model to find the relations for.
     */
    private function findSimpleRelations(Model $model) {
        //$this->app['log']->debug(sprintf('Finding relations for %s', $model->getName()));
        foreach ($model->getColumns() as $column) {
            if (ends_with($column->getName(), self::ID_SUFFIX)) {
                $this->defineRelation($model, $column->getName());
            }
        }
    }

    /**
     * Defines the relation between given model and the model corresponding to the column name.
     * Does nothing if the other model was not declared in init().
     *
     * @param $model      Model the model which has given columnName.
     * @param $columnName String the column name, which corresponds to another model.
     */
    private function defineRelation(Model $model, $columnName) {
        $otherModelName = ucfirst(camel_case(substr($columnName, 0, strpos($columnName, self::ID_SUFFIX)))); // TODO: prettify

        if (!in_array($otherModelName, array_keys($this->models))) {
            //$this->app['log']->warning(sprintf('Found foreign id %s in model %s, but no model with name %s was registered', $columnName, $model->getName(), $otherModelName));
            return;
        }

        //$this->app['log']->info(sprintf('%s has a %s', $model->getName(), $otherModelName));

        $this->relations[$model->getName()][] = new Relation($model, $this->models[$otherModelName], Relation::BELONGS_TO);
    }

    /**
     * Finds and defines many to many relations between models in given array.
     *
     * @param $models Model[] the model names to look for relations for.
     */
    private function findManyToManyRelations(array $models) {
        //$this->app['log']->debug('Finding many to many relations');
        for ($i = 0; $i < sizeof($models); $i++) {
            for ($j = $i + 1; $j < sizeof($models); $j++) {
                $model1 = $models[$i];
                $model2 = $models[$j];

                /* We assume names of pivot tables are alphabetically ordered */
                if (strcasecmp($model1->getName(), $model2->getName()) < 0) {
                    $tableName = strtolower($model1->getName()) . '_' . strtolower($model2->getName());
                } else {
                    $tableName = strtolower($model2->getName()) . '_' . strtolower($model1->getName());
                }

                if ($this->databaseRepository->hasTable($tableName)) {
                    $this->defineManyToManyRelation($model1, $model2);
                }
            }
        }
    }

    /**
     * Defines a many-to-many relation between given models.
     *
     * @param $model1 Model the first model.
     * @param $model2 Model the second model.
     */
    private function defineManyToManyRelation(Model $model1, Model $model2) {
        //$this->app['log']->info(sprintf('%s has and belongs to many %s', $model1->getName(), str_plural($model2->getName())));
        $this->relations[$model1->getName()][] = new Relation($model1, $model2, Relation::HAS_AND_BELONGS_TO);
        $this->relations[$model2->getName()][] = new Relation($model2, $model1, Relation::HAS_AND_BELONGS_TO);
    }
}


<?php

namespace Koine;

use Koine\Parameters\ParameterMissingException;
use Koine\Parameters\UnpermittedParameterException;

/**
 * @author Marcelo Jacobus <marcelo.jacobus@gmail.com>
 */
class Parameters extends Hash
{
    /**
     * If new created params should throw exceptions or ignore unpermitted params
     * @var boolean
     */
    public static $throwExceptions = true;

    /**
     * If should throw exceptions or ignore unpermitted params
     * @var boolean
     */
    protected $throw = true;

    /**
     * Makes sure a parameter was passed
     *
     * @param  string                    $key the parameter key
     * @return Parameters
     * @throws ParameterMissingException when parameter is missing
     */
    public function requireParam($key)
    {
        $param = $this->fetch($key, function ($key) {
            throw new ParameterMissingException("Missing param '$key'");
        });

        if ($this->valueIsEmpty($param)) {
            throw new ParameterMissingException("Missing param '$key'");
        }

        return $param;
    }

    /**
     * Filters unwanted params
     * @param  array                         $permittedParams
     * @return Parameters
     * @throws UnpermittedParameterException when parameters are set to throw
     *                                                       exception on unpermitted params
     */
    public function permit(array $permittedParams)
    {
        $params = clone $this;

        $this->filter($params, $permittedParams);

        return $params;
    }

    /**
     * Filter out or throws exception according to the permitted params
     * @param  Parameter                     $params
     * @param  array                         $permitted
     * @throws UnpermittedParameterException when params not permitted are passed in
     */
    public function filter(Parameters $params, array $permitted = array())
    {
        $this->cleanUnwanted($params, $permitted);
        $this->handleArrays($params, $permitted);
        $this->handleCollections($params, $permitted);
    }

    /**
     * Handle Parameters that have only integer indexes
     * @param Parameter $params
     * @param array     $permitted
     */
    private function handleCollections(Parameters $params, array $permitted = array())
    {
        // if is empty, any value is allowed
        if (empty($permitted)) {
            return;
        }

        $keys = $params->keys();
        $intKeys = $keys->select(function ($value) {
            return is_int($value);
        });

        if ($keys->count() === $intKeys->count()) {
            foreach ($keys as $key) {
                $value = $params[$key];

                if ($value instanceof Parameters) {
                    $this->filter($value, $permitted);
                }
            }
        }
    }

    /**
     * Handle permissions that are given in the hash form
     * @param Parameter $params
     * @param array     $permitted
     */
    private function handleArrays(Parameters $params, array $permitted = array())
    {
        foreach ($permitted as $key => $allowed) {
            if (is_array($allowed) && $params->hasKey($key)) {
                $value = $params[$key];

                if ($value instanceof Parameters) {
                    $this->filter($value, $allowed);
                } else {
                    $this->handleUnpermittedParam($key, $params);
                }
            }
        }
    }

    /**
     * Filters out or throws exception when parameters are neigher keys nor values
     * in the permitted array
     * @param  Parameter                 $params
     * @param  array                     $permitted
     * @throws ParameterMissingException when parameter is missing
     */
    private function cleanUnwanted(Parameters $params, $permitted)
    {
        foreach ($params->toArray() as $key => $value) {
            if (is_array($value) && !is_int($key)) {
                if (!array_key_exists($key, $permitted)) {
                    $this->handleUnpermittedParam($key, $params);
                }
            } elseif (!is_int($key) && !in_array($key, $permitted) && !array_key_exists($key, $permitted)) {
                $this->handleUnpermittedParam($key, $params);
            }
        }
    }

    /**
     * Get the flag throw
     *
     * @return boolean;
     */
    public function getThrowExceptions()
    {
        return static::$throwExceptions;
    }

    /**
     * Empty Hash or empty array?
     * @return boolean
     */
    protected function valueIsEmpty($value)
    {
        return (
            is_object($value) &&
            $value instanceof Parameters &&
            $value->isEmpty()
        ) || (is_array($value) && !count($value));
    }

    /**
     * Handle the unpermitted param either by removing it or throwing an exception
     * @param  string                    $key
     * @param  Parameters                $params
     * @throws ParameterMissingException when parameter is missing
     */
    protected function handleUnpermittedParam($key, $params)
    {
        if ($this->getThrowExceptions()) {
            $message = "Parameter '$key' is not allowed";
            throw new UnpermittedParameterException($message);
        }

        $params->delete($key);
    }
}


<?php

namespace PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;

use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;

class Column
{
    const AUTOFILTER_FILTERTYPE_FILTER = 'filters';
    const AUTOFILTER_FILTERTYPE_CUSTOMFILTER = 'customFilters';
    //    Supports no more than 2 rules, with an And/Or join criteria
    //        if more than 1 rule is defined
    const AUTOFILTER_FILTERTYPE_DYNAMICFILTER = 'dynamicFilter';
    //    Even though the filter rule is constant, the filtered data can vary
    //        e.g. filtered by date = TODAY
    const AUTOFILTER_FILTERTYPE_TOPTENFILTER = 'top10';

    /**
     * Types of autofilter rules.
     *
     * @var string[]
     */
    private static $filterTypes = [
        //    Currently we're not handling
        //        colorFilter
        //        extLst
        //        iconFilter
        self::AUTOFILTER_FILTERTYPE_FILTER,
        self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER,
        self::AUTOFILTER_FILTERTYPE_DYNAMICFILTER,
        self::AUTOFILTER_FILTERTYPE_TOPTENFILTER,
    ];

    // Multiple Rule Connections
    const AUTOFILTER_COLUMN_JOIN_AND = 'and';
    const AUTOFILTER_COLUMN_JOIN_OR = 'or';

    /**
     * Join options for autofilter rules.
     *
     * @var string[]
     */
    private static $ruleJoins = [
        self::AUTOFILTER_COLUMN_JOIN_AND,
        self::AUTOFILTER_COLUMN_JOIN_OR,
    ];

    /**
     * Autofilter.
     *
     * @var AutoFilter
     */
    private $parent;

    /**
     * Autofilter Column Index.
     *
     * @var string
     */
    private $columnIndex = '';

    /**
     * Autofilter Column Filter Type.
     *
     * @var string
     */
    private $filterType = self::AUTOFILTER_FILTERTYPE_FILTER;

    /**
     * Autofilter Multiple Rules And/Or.
     *
     * @var string
     */
    private $join = self::AUTOFILTER_COLUMN_JOIN_OR;

    /**
     * Autofilter Column Rules.
     *
     * @var array of Column\Rule
     */
    private $ruleset = [];

    /**
     * Autofilter Column Dynamic Attributes.
     *
     * @var array of mixed
     */
    private $attributes = [];

    /**
     * Create a new Column.
     *
     * @param string $pColumn Column (e.g. A)
     * @param AutoFilter $pParent Autofilter for this column
     */
    public function __construct($pColumn, AutoFilter $pParent = null)
    {
        $this->columnIndex = $pColumn;
        $this->parent = $pParent;
    }

    /**
     * Get AutoFilter Column Index.
     *
     * @return string
     */
    public function getColumnIndex()
    {
        return $this->columnIndex;
    }

    /**
     * Set AutoFilter Column Index.
     *
     * @param string $pColumn Column (e.g. A)
     *
     * @throws PhpSpreadsheetException
     *
     * @return Column
     */
    public function setColumnIndex($pColumn)
    {
        // Uppercase coordinate
        $pColumn = strtoupper($pColumn);
        if ($this->parent !== null) {
            $this->parent->testColumnInRange($pColumn);
        }

        $this->columnIndex = $pColumn;

        return $this;
    }

    /**
     * Get this Column's AutoFilter Parent.
     *
     * @return AutoFilter
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * Set this Column's AutoFilter Parent.
     *
     * @param AutoFilter $pParent
     *
     * @return Column
     */
    public function setParent(AutoFilter $pParent = null)
    {
        $this->parent = $pParent;

        return $this;
    }

    /**
     * Get AutoFilter Type.
     *
     * @return string
     */
    public function getFilterType()
    {
        return $this->filterType;
    }

    /**
     * Set AutoFilter Type.
     *
     * @param string $pFilterType
     *
     * @throws PhpSpreadsheetException
     *
     * @return Column
     */
    public function setFilterType($pFilterType)
    {
        if (!in_array($pFilterType, self::$filterTypes)) {
            throw new PhpSpreadsheetException('Invalid filter type for column AutoFilter.');
        }

        $this->filterType = $pFilterType;

        return $this;
    }

    /**
     * Get AutoFilter Multiple Rules And/Or Join.
     *
     * @return string
     */
    public function getJoin()
    {
        return $this->join;
    }

    /**
     * Set AutoFilter Multiple Rules And/Or.
     *
     * @param string $pJoin And/Or
     *
     * @throws PhpSpreadsheetException
     *
     * @return Column
     */
    public function setJoin($pJoin)
    {
        // Lowercase And/Or
        $pJoin = strtolower($pJoin);
        if (!in_array($pJoin, self::$ruleJoins)) {
            throw new PhpSpreadsheetException('Invalid rule connection for column AutoFilter.');
        }

        $this->join = $pJoin;

        return $this;
    }

    /**
     * Set AutoFilter Attributes.
     *
     * @param string[] $attributes
     *
     * @return Column
     */
    public function setAttributes(array $attributes)
    {
        $this->attributes = $attributes;

        return $this;
    }

    /**
     * Set An AutoFilter Attribute.
     *
     * @param string $pName Attribute Name
     * @param string $pValue Attribute Value
     *
     * @return Column
     */
    public function setAttribute($pName, $pValue)
    {
        $this->attributes[$pName] = $pValue;

        return $this;
    }

    /**
     * Get AutoFilter Column Attributes.
     *
     * @return string[]
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Get specific AutoFilter Column Attribute.
     *
     * @param string $pName Attribute Name
     *
     * @return string
     */
    public function getAttribute($pName)
    {
        if (isset($this->attributes[$pName])) {
            return $this->attributes[$pName];
        }

        return null;
    }

    /**
     * Get all AutoFilter Column Rules.
     *
     * @return Column\Rule[]
     */
    public function getRules()
    {
        return $this->ruleset;
    }

    /**
     * Get a specified AutoFilter Column Rule.
     *
     * @param int $pIndex Rule index in the ruleset array
     *
     * @return Column\Rule
     */
    public function getRule($pIndex)
    {
        if (!isset($this->ruleset[$pIndex])) {
            $this->ruleset[$pIndex] = new Column\Rule($this);
        }

        return $this->ruleset[$pIndex];
    }

    /**
     * Create a new AutoFilter Column Rule in the ruleset.
     *
     * @return Column\Rule
     */
    public function createRule()
    {
        $this->ruleset[] = new Column\Rule($this);

        return end($this->ruleset);
    }

    /**
     * Add a new AutoFilter Column Rule to the ruleset.
     *
     * @param Column\Rule $pRule
     *
     * @return Column
     */
    public function addRule(Column\Rule $pRule)
    {
        $pRule->setParent($this);
        $this->ruleset[] = $pRule;

        return $this;
    }

    /**
     * Delete a specified AutoFilter Column Rule
     * If the number of rules is reduced to 1, then we reset And/Or logic to Or.
     *
     * @param int $pIndex Rule index in the ruleset array
     *
     * @return Column
     */
    public function deleteRule($pIndex)
    {
        if (isset($this->ruleset[$pIndex])) {
            unset($this->ruleset[$pIndex]);
            //    If we've just deleted down to a single rule, then reset And/Or joining to Or
            if (count($this->ruleset) <= 1) {
                $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR);
            }
        }

        return $this;
    }

    /**
     * Delete all AutoFilter Column Rules.
     *
     * @return Column
     */
    public function clearRules()
    {
        $this->ruleset = [];
        $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR);

        return $this;
    }

    /**
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
     */
    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if ($key === 'parent') {
                // Detach from autofilter parent
                $this->parent = null;
            } elseif ($key === 'ruleset') {
                // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
                $this->ruleset = [];
                foreach ($value as $k => $v) {
                    $cloned = clone $v;
                    $cloned->setParent($this); // attach the new cloned Rule to this new cloned Autofilter Cloned object
                    $this->ruleset[$k] = $cloned;
                }
            } elseif (is_object($value)) {
                $this->$key = clone $value;
            } else {
                $this->$key = $value;
            }
        }
    }
}


<?php

/*
 * This file is part of Symplify
 * Copyright (c) 2016 Tomas Votruba (http://tomasvotruba.cz).
 */

namespace Symplify\PHP7_CodeSniffer\Console\Command;

use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symplify\PHP7_CodeSniffer\Application\Application;
use Symplify\PHP7_CodeSniffer\Application\Command\RunApplicationCommand;
use Symplify\PHP7_CodeSniffer\Console\ExitCode;
use Symplify\PHP7_CodeSniffer\Console\Style\CodeSnifferStyle;
use Symplify\PHP7_CodeSniffer\Console\Output\InfoMessagePrinter;
use Symplify\PHP7_CodeSniffer\Report\ErrorDataCollector;
use Throwable;

final class RunCommand extends Command
{
    /**
     * @var CodeSnifferStyle
     */
    private $codeSnifferStyle;

    /**
     * @var ErrorDataCollector
     */
    private $errorDataCollector;

    /**
     * @var Application
     */
    private $application;

    /**
     * @var InfoMessagePrinter
     */
    private $infoMessagePrinter;

    public function __construct(
        CodeSnifferStyle $codeSnifferStyle,
        ErrorDataCollector $reportCollector,
        Application $application,
        InfoMessagePrinter $infoMessagePrinter
    ) {
        $this->codeSnifferStyle = $codeSnifferStyle;
        $this->errorDataCollector = $reportCollector;
        $this->application = $application;
        $this->infoMessagePrinter = $infoMessagePrinter;

        parent::__construct();
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName('run');
        $this->setDescription('Checks code against coding standard.');
        $this->addArgument(
            'source',
            InputArgument::REQUIRED | InputArgument::IS_ARRAY,
            'Files or directories to process.'
        );
        $this->addOption('fix', null, null, 'Fix all fixable errors.');
        $this->addArrayOption('standards', 'List of coding standards to use.');
        $this->addArrayOption('sniffs', 'List of sniff codes to use.');
        $this->addArrayOption('exclude-sniffs', 'List of sniff codes to be excluded.');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {
            $this->application->runCommand($this->createCommandFromInput($input));

            if ($this->errorDataCollector->getErrorCount()) {
                $this->infoMessagePrinter->printFoundErrorsStatus($input->getOption('fix'));

                return ExitCode::ERROR;
            }

            $this->codeSnifferStyle->success(
                'Great job! Your code is completely fine. Take a break and look around you.'
            );

            return ExitCode::SUCCESS;
        } catch (Throwable $throwable) {
            $this->codeSnifferStyle->error($throwable->getMessage());

            return ExitCode::ERROR;
        }
    }

    private function addArrayOption(string $name, string $description)
    {
        $this->addOption($name, null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, $description);
    }

    private function createCommandFromInput(InputInterface $input) : RunApplicationCommand
    {
        return new RunApplicationCommand(
            $input->getArgument('source'),
            $input->getOption('standards'),
            $input->getOption('sniffs'),
            $input->getOption('exclude-sniffs'),
            $input->getOption('fix')
        );
    }
}


<?php

namespace PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column;

use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column;

class Rule
{
    const AUTOFILTER_RULETYPE_FILTER = 'filter';
    const AUTOFILTER_RULETYPE_DATEGROUP = 'dateGroupItem';
    const AUTOFILTER_RULETYPE_CUSTOMFILTER = 'customFilter';
    const AUTOFILTER_RULETYPE_DYNAMICFILTER = 'dynamicFilter';
    const AUTOFILTER_RULETYPE_TOPTENFILTER = 'top10Filter';

    private static $ruleTypes = [
        //    Currently we're not handling
        //        colorFilter
        //        extLst
        //        iconFilter
        self::AUTOFILTER_RULETYPE_FILTER,
        self::AUTOFILTER_RULETYPE_DATEGROUP,
        self::AUTOFILTER_RULETYPE_CUSTOMFILTER,
        self::AUTOFILTER_RULETYPE_DYNAMICFILTER,
        self::AUTOFILTER_RULETYPE_TOPTENFILTER,
    ];

    const AUTOFILTER_RULETYPE_DATEGROUP_YEAR = 'year';
    const AUTOFILTER_RULETYPE_DATEGROUP_MONTH = 'month';
    const AUTOFILTER_RULETYPE_DATEGROUP_DAY = 'day';
    const AUTOFILTER_RULETYPE_DATEGROUP_HOUR = 'hour';
    const AUTOFILTER_RULETYPE_DATEGROUP_MINUTE = 'minute';
    const AUTOFILTER_RULETYPE_DATEGROUP_SECOND = 'second';

    private static $dateTimeGroups = [
        self::AUTOFILTER_RULETYPE_DATEGROUP_YEAR,
        self::AUTOFILTER_RULETYPE_DATEGROUP_MONTH,
        self::AUTOFILTER_RULETYPE_DATEGROUP_DAY,
        self::AUTOFILTER_RULETYPE_DATEGROUP_HOUR,
        self::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE,
        self::AUTOFILTER_RULETYPE_DATEGROUP_SECOND,
    ];

    const AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY = 'yesterday';
    const AUTOFILTER_RULETYPE_DYNAMIC_TODAY = 'today';
    const AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW = 'tomorrow';
    const AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE = 'yearToDate';
    const AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR = 'thisYear';
    const AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER = 'thisQuarter';
    const AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH = 'thisMonth';
    const AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK = 'thisWeek';
    const AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR = 'lastYear';
    const AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER = 'lastQuarter';
    const AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH = 'lastMonth';
    const AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK = 'lastWeek';
    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR = 'nextYear';
    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER = 'nextQuarter';
    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH = 'nextMonth';
    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK = 'nextWeek';
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_1 = 'M1';
    const AUTOFILTER_RULETYPE_DYNAMIC_JANUARY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_1;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2 = 'M2';
    const AUTOFILTER_RULETYPE_DYNAMIC_FEBRUARY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_3 = 'M3';
    const AUTOFILTER_RULETYPE_DYNAMIC_MARCH = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_3;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_4 = 'M4';
    const AUTOFILTER_RULETYPE_DYNAMIC_APRIL = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_4;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_5 = 'M5';
    const AUTOFILTER_RULETYPE_DYNAMIC_MAY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_5;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_6 = 'M6';
    const AUTOFILTER_RULETYPE_DYNAMIC_JUNE = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_6;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_7 = 'M7';
    const AUTOFILTER_RULETYPE_DYNAMIC_JULY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_7;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_8 = 'M8';
    const AUTOFILTER_RULETYPE_DYNAMIC_AUGUST = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_8;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_9 = 'M9';
    const AUTOFILTER_RULETYPE_DYNAMIC_SEPTEMBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_9;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_10 = 'M10';
    const AUTOFILTER_RULETYPE_DYNAMIC_OCTOBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_10;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_11 = 'M11';
    const AUTOFILTER_RULETYPE_DYNAMIC_NOVEMBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_11;
    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_12 = 'M12';
    const AUTOFILTER_RULETYPE_DYNAMIC_DECEMBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_12;
    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_1 = 'Q1';
    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_2 = 'Q2';
    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_3 = 'Q3';
    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_4 = 'Q4';
    const AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE = 'aboveAverage';
    const AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE = 'belowAverage';

    private static $dynamicTypes = [
        self::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY,
        self::AUTOFILTER_RULETYPE_DYNAMIC_TODAY,
        self::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW,
        self::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE,
        self::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR,
        self::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER,
        self::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH,
        self::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK,
        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR,
        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER,
        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH,
        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK,
        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR,
        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER,
        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH,
        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_1,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_3,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_4,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_5,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_6,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_7,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_8,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_9,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_10,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_11,
        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_12,
        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_1,
        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_2,
        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_3,
        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_4,
        self::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE,
        self::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE,
    ];

    /*
     *    The only valid filter rule operators for filter and customFilter types are:
     *        <xsd:enumeration value="equal"/>
     *        <xsd:enumeration value="lessThan"/>
     *        <xsd:enumeration value="lessThanOrEqual"/>
     *        <xsd:enumeration value="notEqual"/>
     *        <xsd:enumeration value="greaterThanOrEqual"/>
     *        <xsd:enumeration value="greaterThan"/>
     */
    const AUTOFILTER_COLUMN_RULE_EQUAL = 'equal';
    const AUTOFILTER_COLUMN_RULE_NOTEQUAL = 'notEqual';
    const AUTOFILTER_COLUMN_RULE_GREATERTHAN = 'greaterThan';
    const AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL = 'greaterThanOrEqual';
    const AUTOFILTER_COLUMN_RULE_LESSTHAN = 'lessThan';
    const AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL = 'lessThanOrEqual';

    private static $operators = [
        self::AUTOFILTER_COLUMN_RULE_EQUAL,
        self::AUTOFILTER_COLUMN_RULE_NOTEQUAL,
        self::AUTOFILTER_COLUMN_RULE_GREATERTHAN,
        self::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL,
        self::AUTOFILTER_COLUMN_RULE_LESSTHAN,
        self::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL,
    ];

    const AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE = 'byValue';
    const AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT = 'byPercent';

    private static $topTenValue = [
        self::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE,
        self::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT,
    ];

    const AUTOFILTER_COLUMN_RULE_TOPTEN_TOP = 'top';
    const AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM = 'bottom';

    private static $topTenType = [
        self::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP,
        self::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM,
    ];

    // Rule Operators (Numeric, Boolean etc)
//    const AUTOFILTER_COLUMN_RULE_BETWEEN            = 'between';        //    greaterThanOrEqual 1 && lessThanOrEqual 2
    // Rule Operators (Numeric Special) which are translated to standard numeric operators with calculated values
//    const AUTOFILTER_COLUMN_RULE_TOPTEN                = 'topTen';            //    greaterThan calculated value
//    const AUTOFILTER_COLUMN_RULE_TOPTENPERCENT        = 'topTenPercent';    //    greaterThan calculated value
//    const AUTOFILTER_COLUMN_RULE_ABOVEAVERAGE        = 'aboveAverage';    //    Value is calculated as the average
//    const AUTOFILTER_COLUMN_RULE_BELOWAVERAGE        = 'belowAverage';    //    Value is calculated as the average
    // Rule Operators (String) which are set as wild-carded values
//    const AUTOFILTER_COLUMN_RULE_BEGINSWITH            = 'beginsWith';            // A*
//    const AUTOFILTER_COLUMN_RULE_ENDSWITH            = 'endsWith';            // *Z
//    const AUTOFILTER_COLUMN_RULE_CONTAINS            = 'contains';            // *B*
//    const AUTOFILTER_COLUMN_RULE_DOESNTCONTAIN        = 'notEqual';            //    notEqual *B*
    // Rule Operators (Date Special) which are translated to standard numeric operators with calculated values
//    const AUTOFILTER_COLUMN_RULE_BEFORE                = 'lessThan';
//    const AUTOFILTER_COLUMN_RULE_AFTER                = 'greaterThan';
//    const AUTOFILTER_COLUMN_RULE_YESTERDAY            = 'yesterday';
//    const AUTOFILTER_COLUMN_RULE_TODAY                = 'today';
//    const AUTOFILTER_COLUMN_RULE_TOMORROW            = 'tomorrow';
//    const AUTOFILTER_COLUMN_RULE_LASTWEEK            = 'lastWeek';
//    const AUTOFILTER_COLUMN_RULE_THISWEEK            = 'thisWeek';
//    const AUTOFILTER_COLUMN_RULE_NEXTWEEK            = 'nextWeek';
//    const AUTOFILTER_COLUMN_RULE_LASTMONTH            = 'lastMonth';
//    const AUTOFILTER_COLUMN_RULE_THISMONTH            = 'thisMonth';
//    const AUTOFILTER_COLUMN_RULE_NEXTMONTH            = 'nextMonth';
//    const AUTOFILTER_COLUMN_RULE_LASTQUARTER        = 'lastQuarter';
//    const AUTOFILTER_COLUMN_RULE_THISQUARTER        = 'thisQuarter';
//    const AUTOFILTER_COLUMN_RULE_NEXTQUARTER        = 'nextQuarter';
//    const AUTOFILTER_COLUMN_RULE_LASTYEAR            = 'lastYear';
//    const AUTOFILTER_COLUMN_RULE_THISYEAR            = 'thisYear';
//    const AUTOFILTER_COLUMN_RULE_NEXTYEAR            = 'nextYear';
//    const AUTOFILTER_COLUMN_RULE_YEARTODATE            = 'yearToDate';            //    <dynamicFilter val="40909" type="yearToDate" maxVal="41113"/>
//    const AUTOFILTER_COLUMN_RULE_ALLDATESINMONTH    = 'allDatesInMonth';    //    <dynamicFilter type="M2"/> for Month/February
//    const AUTOFILTER_COLUMN_RULE_ALLDATESINQUARTER    = 'allDatesInQuarter';    //    <dynamicFilter type="Q2"/> for Quarter 2

    /**
     * Autofilter Column.
     *
     * @var Column
     */
    private $parent;

    /**
     * Autofilter Rule Type.
     *
     * @var string
     */
    private $ruleType = self::AUTOFILTER_RULETYPE_FILTER;

    /**
     * Autofilter Rule Value.
     *
     * @var string
     */
    private $value = '';

    /**
     * Autofilter Rule Operator.
     *
     * @var string
     */
    private $operator = self::AUTOFILTER_COLUMN_RULE_EQUAL;

    /**
     * DateTimeGrouping Group Value.
     *
     * @var string
     */
    private $grouping = '';

    /**
     * Create a new Rule.
     *
     * @param Column $pParent
     */
    public function __construct(Column $pParent = null)
    {
        $this->parent = $pParent;
    }

    /**
     * Get AutoFilter Rule Type.
     *
     * @return string
     */
    public function getRuleType()
    {
        return $this->ruleType;
    }

    /**
     * Set AutoFilter Rule Type.
     *
     * @param string $pRuleType see self::AUTOFILTER_RULETYPE_*
     *
     * @throws PhpSpreadsheetException
     *
     * @return Rule
     */
    public function setRuleType($pRuleType)
    {
        if (!in_array($pRuleType, self::$ruleTypes)) {
            throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.');
        }

        $this->ruleType = $pRuleType;

        return $this;
    }

    /**
     * Get AutoFilter Rule Value.
     *
     * @return string
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * Set AutoFilter Rule Value.
     *
     * @param string|string[] $pValue
     *
     * @throws PhpSpreadsheetException
     *
     * @return Rule
     */
    public function setValue($pValue)
    {
        if (is_array($pValue)) {
            $grouping = -1;
            foreach ($pValue as $key => $value) {
                //    Validate array entries
                if (!in_array($key, self::$dateTimeGroups)) {
                    //    Remove any invalid entries from the value array
                    unset($pValue[$key]);
                } else {
                    //    Work out what the dateTime grouping will be
                    $grouping = max($grouping, array_search($key, self::$dateTimeGroups));
                }
            }
            if (count($pValue) == 0) {
                throw new PhpSpreadsheetException('Invalid rule value for column AutoFilter Rule.');
            }
            //    Set the dateTime grouping that we've anticipated
            $this->setGrouping(self::$dateTimeGroups[$grouping]);
        }
        $this->value = $pValue;

        return $this;
    }

    /**
     * Get AutoFilter Rule Operator.
     *
     * @return string
     */
    public function getOperator()
    {
        return $this->operator;
    }

    /**
     * Set AutoFilter Rule Operator.
     *
     * @param string $pOperator see self::AUTOFILTER_COLUMN_RULE_*
     *
     * @throws PhpSpreadsheetException
     *
     * @return Rule
     */
    public function setOperator($pOperator)
    {
        if (empty($pOperator)) {
            $pOperator = self::AUTOFILTER_COLUMN_RULE_EQUAL;
        }
        if ((!in_array($pOperator, self::$operators)) &&
            (!in_array($pOperator, self::$topTenValue))) {
            throw new PhpSpreadsheetException('Invalid operator for column AutoFilter Rule.');
        }
        $this->operator = $pOperator;

        return $this;
    }

    /**
     * Get AutoFilter Rule Grouping.
     *
     * @return string
     */
    public function getGrouping()
    {
        return $this->grouping;
    }

    /**
     * Set AutoFilter Rule Grouping.
     *
     * @param string $pGrouping
     *
     * @throws PhpSpreadsheetException
     *
     * @return Rule
     */
    public function setGrouping($pGrouping)
    {
        if (($pGrouping !== null) &&
            (!in_array($pGrouping, self::$dateTimeGroups)) &&
            (!in_array($pGrouping, self::$dynamicTypes)) &&
            (!in_array($pGrouping, self::$topTenType))) {
            throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.');
        }
        $this->grouping = $pGrouping;

        return $this;
    }

    /**
     * Set AutoFilter Rule.
     *
     * @param string $pOperator see self::AUTOFILTER_COLUMN_RULE_*
     * @param string|string[] $pValue
     * @param string $pGrouping
     *
     * @throws PhpSpreadsheetException
     *
     * @return Rule
     */
    public function setRule($pOperator, $pValue, $pGrouping = null)
    {
        $this->setOperator($pOperator);
        $this->setValue($pValue);
        //  Only set grouping if it's been passed in as a user-supplied argument,
        //      otherwise we're calculating it when we setValue() and don't want to overwrite that
        //      If the user supplies an argumnet for grouping, then on their own head be it
        if ($pGrouping !== null) {
            $this->setGrouping($pGrouping);
        }

        return $this;
    }

    /**
     * Get this Rule's AutoFilter Column Parent.
     *
     * @return Column
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * Set this Rule's AutoFilter Column Parent.
     *
     * @param Column $pParent
     *
     * @return Rule
     */
    public function setParent(Column $pParent = null)
    {
        $this->parent = $pParent;

        return $this;
    }

    /**
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
     */
    public function __clone()
    {
        $vars = get_object_vars($this);
        foreach ($vars as $key => $value) {
            if (is_object($value)) {
                if ($key == 'parent') {
                    //    Detach from autofilter column parent
                    $this->$key = null;
                } else {
                    $this->$key = clone $value;
                }
            } else {
                $this->$key = $value;
            }
        }
    }
}