<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Framework\Data;

use Magento\Framework\DataObject;

/**
 * Class AbstractCriteria
 */
abstract class AbstractCriteria implements \Magento\Framework\Api\CriteriaInterface
{
    /**
     * @var array
     */
    protected $data = [
        self::PART_FIELDS => ['list' => []],
        self::PART_FILTERS => ['list' => []],
        self::PART_ORDERS => ['list' => []],
        self::PART_CRITERIA_LIST => ['list' => []],
    ];

    /**
     * @var string
     */
    protected $mapperInterfaceName;

    /**
     * Get associated Mapper Interface name
     *
     * @throws \Exception
     * @return string
     */
    public function getMapperInterfaceName()
    {
        if (!$this->mapperInterfaceName) {
            throw new \Exception(
                (string)new \Magento\Framework\Phrase(
                    'Missed Mapper Interface for Criteria Interface: %1',
                    [get_class($this)]
                )
            );
        }
        return $this->mapperInterfaceName;
    }

    /**
     * Add field to select
     *
     * @param string|array $field
     * @param string|null $alias
     * @return void
     */
    public function addField($field, $alias = null)
    {
        if ($field === '*') {
            $this->data[self::PART_FIELDS]['list'] = [$field];
        } else {
            if (is_array($field)) {
                foreach ($field as $key => $value) {
                    $this->addField($value, is_string($key) ? $key : null);
                }
            } else {
                if ($alias === null) {
                    $this->data[self::PART_FIELDS]['list'][$field] = $field;
                } else {
                    $this->data[self::PART_FIELDS]['list'][$alias] = $field;
                }
            }
        }
    }

    /**
     * Add field filter to collection
     *
     * If $condition integer or string - exact value will be filtered ('eq' condition)
     *
     * If $condition is array - one of the following structures is expected:
     * <pre>
     * - ["from" => $fromValue, "to" => $toValue]
     * - ["eq" => $equalValue]
     * - ["neq" => $notEqualValue]
     * - ["like" => $likeValue]
     * - ["in" => [$inValues]]
     * - ["nin" => [$notInValues]]
     * - ["notnull" => $valueIsNotNull]
     * - ["null" => $valueIsNull]
     * - ["moreq" => $moreOrEqualValue]
     * - ["gt" => $greaterValue]
     * - ["lt" => $lessValue]
     * - ["gteq" => $greaterOrEqualValue]
     * - ["lteq" => $lessOrEqualValue]
     * - ["finset" => $valueInSet]
     * </pre>
     *
     * If non matched - sequential parallel arrays are expected and OR conditions
     * will be built using above mentioned structure.
     *
     * Example:
     * <pre>
     * $field = ['age', 'name'];
     * $condition = [42, ['like' => 'Mage']];
     * $type = 'or';
     * </pre>
     * The above would find where age equal to 42 OR name like %Mage%.
     *
     * @param string $name
     * @param string|array $field
     * @param string|int|array $condition
     * @param string $type
     * @throws \Exception
     * @return void
     */
    public function addFilter($name, $field, $condition = null, $type = 'and')
    {
        if (isset($this->data[self::PART_FILTERS]['list'][$name])) {
            throw new \Exception(
                (string)new \Magento\Framework\Phrase(
                    'Filter already exists in Criteria object: %1',
                    [$name]
                )
            );
        }
        $filter = new DataObject();
        // implements ArrayAccess
        $filter['name'] = $name;
        $filter['field'] = $field;
        $filter['condition'] = $condition;
        $filter['type'] = strtolower($type);
        $this->data[self::PART_FILTERS]['list'][$name] = $filter;
    }

    /**
     * self::setOrder() alias
     *
     * @param string $field
     * @param string $direction
     * @param bool $unShift
     * @return void
     */
    public function addOrder($field, $direction = self::SORT_ORDER_DESC, $unShift = false)
    {
        $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC;
        unset($this->data[self::PART_ORDERS]['list'][$field]);
        // avoid ordering by the same field twice
        if ($unShift) {
            $orders = [$field => $direction];
            foreach ($this->data[self::PART_ORDERS]['list'] as $key => $dir) {
                $orders[$key] = $dir;
            }
            $this->data[self::PART_ORDERS]['list'] = $orders;
        } else {
            $this->data[self::PART_ORDERS]['list'][$field] = $direction;
        }
    }

    /**
     * Set Query limit
     *
     * @param int $offset
     * @param int $size
     * @return void
     */
    public function setLimit($offset, $size)
    {
        $this->data[self::PART_LIMIT] = [$offset, $size];
    }

    /**
     * Removes field from select
     *
     * @param string|null $field
     * @param bool $isAlias Alias identifier
     * @return void
     */
    public function removeField($field, $isAlias = false)
    {
        if ($isAlias) {
            if (isset($this->data[self::PART_FIELDS]['list'][$field])) {
                unset($this->data[self::PART_FIELDS]['list'][$field]);
            }
        } else {
            foreach ($this->data[self::PART_FIELDS]['list'] as $key => $value) {
                if ($value === $field) {
                    unset($this->data[self::PART_FIELDS]['list'][$key]);
                    break;
                }
            }
        }
    }

    /**
     * Removes all fields from select
     *
     * @return void
     */
    public function removeAllFields()
    {
        $this->data[self::PART_FIELDS]['list'] = [];
    }

    /**
     * Removes filter by name
     *
     * @param string $name
     * @return void
     */
    public function removeFilter($name)
    {
        if (isset($this->data[self::PART_FILTERS]['list'][$name])) {
            unset($this->data[self::PART_FILTERS]['list'][$name]);
        }
    }

    /**
     * Removes all filters
     *
     * @return void
     */
    public function removeAllFilters()
    {
        $this->data[self::PART_FILTERS]['list'] = [];
    }

    /**
     * Get Criteria objects added to current Composite Criteria
     *
     * @return array
     */
    public function getCriteriaList()
    {
        return $this->data[self::PART_CRITERIA_LIST]['list'];
    }

    /**
     * Get list of filters
     *
     * @return array
     */
    public function getFilters()
    {
        return $this->data[self::PART_FILTERS]['list'];
    }

    /**
     * Get ordering criteria
     *
     * @return array
     */
    public function getOrders()
    {
        return $this->data[self::PART_ORDERS]['list'];
    }

    /**
     * Get limit
     * (['offset', 'page'])
     *
     * @return array
     */
    public function getLimit()
    {
        return $this->data[self::PART_LIMIT];
    }

    /**
     * Retrieve criteria part
     *
     * @param string $name
     * @param mixed $default
     * @return mixed
     */
    public function getPart($name, $default = null)
    {
        return $this->data[$name] ?? $default;
    }

    /**
     * Return all criteria parts as array
     *
     * @return array
     */
    public function toArray()
    {
        return $this->data;
    }

    /**
     * Reset criteria
     *
     * @return void
     */
    public function reset()
    {
        $this->data = [
            self::PART_FIELDS => ['list' => []],
            self::PART_FILTERS => ['list' => []],
            self::PART_ORDERS => ['list' => []],
            self::PART_CRITERIA_LIST => ['list' => []],
        ];
    }
}
