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

use Magento\Framework\Exception\LocalizedException;

/**
 * System configuration structure.
 *
 * All paths are declared in module's system.xml.
 *
 * ```xml
 * <section id="section_id">
 *      <group id="group_id" ...>
 *          <field id="field_one_id" ...>
 *              <label>Field One</label>
 *              ...
 *          </field>
 *          <field id="field_two_id" ...>
 *              <label>Field Two</label>
 *              <config_path>section/group/field</config_path>
 *              ...
 *          </field>
 *      </group>
 * </section>
 * ```
 *
 * Structure path is the nested path of node ids (section, group, field).
 *
 * Config path is the path which is declared in <config_path> node.
 * If this node is not provided then config path is the same as structure path.
 *
 * With the example above you can see that the field <field id="field_one_id"> has the next paths:
 *  - the structure path section_id/group_id/field_one_id
 *  - the configuration path section_id/group_id/field_one_id
 *
 * Also you can see that the field <field id="field_two_id"> has the next paths:
 * - the structure path section_id/group_id/field_two_id
 * - the configuration path section/group/field
 *
 * @api
 * @since 100.0.2
 */
class Structure implements \Magento\Config\Model\Config\Structure\SearchInterface
{
    /**
     * Key that contains field type in structure array
     */
    public const TYPE_KEY = '_elementType';

    /**
     * Configuration structure represented as tree
     *
     * @var array
     */
    protected $_data;

    /**
     * Config tab iterator
     *
     * @var \Magento\Config\Model\Config\Structure\Element\Iterator\Tab
     */
    protected $_tabIterator;

    /**
     * Pool of config element flyweight objects
     *
     * @var \Magento\Config\Model\Config\Structure\Element\FlyweightFactory
     */
    protected $_flyweightFactory;

    /**
     * Provider of current config scope
     *
     * @var ScopeDefiner
     */
    protected $_scopeDefiner;

    /**
     * List of cached elements
     *
     * @var \Magento\Config\Model\Config\Structure\ElementInterface[]
     */
    protected $_elements;

    /**
     * List of config sections
     *
     * @var array
     * @since 100.1.0
     */
    protected $sectionList;

    /**
     * Collects config paths and their structure paths from configuration files
     *
     * For example:
     * ```php
     * [
     *  'section_id/group_id/field_one_id' => [
     *      'section_id/group_id/field_one_id'
     *  ],
     * 'section/group/field' => [
     *      'section_id/group_id/field_two_id'
     * ]
     * ```
     *
     * @var array
     */
    private $mappedPaths;

    /**
     * @param \Magento\Config\Model\Config\Structure\Data $structureData
     * @param \Magento\Config\Model\Config\Structure\Element\Iterator\Tab $tabIterator
     * @param \Magento\Config\Model\Config\Structure\Element\FlyweightFactory $flyweightFactory
     * @param ScopeDefiner $scopeDefiner
     */
    public function __construct(
        \Magento\Config\Model\Config\Structure\Data $structureData,
        \Magento\Config\Model\Config\Structure\Element\Iterator\Tab $tabIterator,
        \Magento\Config\Model\Config\Structure\Element\FlyweightFactory $flyweightFactory,
        ScopeDefiner $scopeDefiner
    ) {
        $this->_data = $structureData->get();
        $this->_tabIterator = $tabIterator;
        $this->_flyweightFactory = $flyweightFactory;
        $this->_scopeDefiner = $scopeDefiner;
    }

    /**
     * Retrieve tab iterator
     *
     * @return \Magento\Config\Model\Config\Structure\Element\Iterator
     */
    public function getTabs()
    {
        if (isset($this->_data['sections'])) {
            foreach ($this->_data['sections'] as $sectionId => $section) {
                if (isset($section['tab']) && $section['tab']) {
                    $this->_data['tabs'][$section['tab']]['children'][$sectionId] = $section;
                }
            }
            $this->_tabIterator->setElements($this->_data['tabs'], $this->_scopeDefiner->getScope());
        }
        return $this->_tabIterator;
    }

    /**
     * Retrieve config section list
     *
     * @return array
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
     * @since 100.1.0
     */
    public function getSectionList()
    {
        if (empty($this->sectionList)) {
            foreach ($this->_data['sections'] as $sectionId => $section) {
                if (array_key_exists('children', $section) && is_array($section['children'])) {
                    foreach ($section['children'] as $childId => $child) {
                        $this->sectionList[$sectionId . '_' . $childId] = true;
                    }
                }
            }
        }
        return $this->sectionList;
    }

    /**
     * Find element by structure path
     *
     * @param string $path The structure path
     * @return \Magento\Config\Model\Config\Structure\ElementInterface|null
     */
    public function getElement($path)
    {
        $parts = $path !== null ? explode('/', $path) : [];
        return $this->getElementByPathParts($parts);
    }

    /**
     * Find element by config path
     *
     * @param string $path The configuration path
     * @return \Magento\Config\Model\Config\Structure\ElementInterface|null
     * @since 101.0.0
     */
    public function getElementByConfigPath($path)
    {
        $allPaths = $this->getFieldPaths();

        if (isset($allPaths[$path]) && is_array($allPaths[$path])) {
            $path = array_shift($allPaths[$path]);
        }

        $parts = $path !== null ? explode('/', $path) : [];
        return $this->getElementByPathParts($parts);
    }

    /**
     * Retrieve first available section in config structure
     *
     * @return Structure\ElementInterface
     * @throws LocalizedException
     */
    public function getFirstSection()
    {
        $tabs = $this->getTabs();
        $tabs->rewind();
        /** @var $tab \Magento\Config\Model\Config\Structure\Element\Tab */
        $tab = $tabs->current();
        $tab->getChildren()->rewind();
        if (!$tab->getChildren()->current()->isVisible()) {
            throw new LocalizedException(__('Visible section not found.'));
        }

        return $tab->getChildren()->current();
    }

    /**
     * Find element by path parts
     *
     * @param string[] $pathParts
     * @return \Magento\Config\Model\Config\Structure\ElementInterface|null
     */
    public function getElementByPathParts(array $pathParts)
    {
        $path = implode('_', $pathParts);
        if (isset($this->_elements[$path])) {
            return $this->_elements[$path];
        }
        $children = [];
        if ($this->_data) {
            $children = $this->_data['sections'];
        }
        $child = [];
        foreach ($pathParts as $pathPart) {
            if ($children && (array_key_exists($pathPart, $children))) {
                $child = $children[$pathPart];
                $children = array_key_exists('children', $child) ? $child['children'] : [];
            } else {
                $child = $this->_createEmptyElement($pathParts);
                break;
            }
        }
        $this->_elements[$path] = $this->_flyweightFactory->create($child['_elementType']);
        $this->_elements[$path]->setData($child, $this->_scopeDefiner->getScope());
        return $this->_elements[$path];
    }

    /**
     * Create empty element data
     *
     * @param string[] $pathParts
     * @return array
     */
    protected function _createEmptyElement(array $pathParts)
    {
        switch (count($pathParts)) {
            case 1:
                $elementType = 'section';
                break;
            case 2:
                $elementType = 'group';
                break;
            default:
                $elementType = 'field';
        }
        $elementId = array_pop($pathParts);
        return ['id' => $elementId, 'path' => implode('/', $pathParts), '_elementType' => $elementType];
    }

    /**
     * Retrieve paths of fields that have provided attributes with provided values
     *
     * @param string $attributeName
     * @param mixed $attributeValue
     * @return array
     */
    public function getFieldPathsByAttribute($attributeName, $attributeValue)
    {
        $result = [];
        if (empty($this->_data['sections'])) {
            return $result;
        }

        foreach ($this->_data['sections'] as $section) {
            if (!isset($section['children'])) {
                continue;
            }
            foreach ($section['children'] as $group) {
                if (isset($group['children'])) {
                    $path = $section['id'] . '/' . $group['id'];
                    $result[] = $this->_getGroupFieldPathsByAttribute(
                        $group['children'],
                        $path,
                        $attributeName,
                        $attributeValue
                    );
                }
            }
        }
        return array_merge([], ...$result);
    }

    /**
     * Find group fields with specified attribute and attribute value
     *
     * @param array $fields
     * @param string $parentPath
     * @param string $attributeName
     * @param mixed $attributeValue
     * @return array
     */
    protected function _getGroupFieldPathsByAttribute(array $fields, $parentPath, $attributeName, $attributeValue)
    {
        $result = [];
        foreach ($fields as $field) {
            if (isset($field['children'])) {
                $result += $this->_getGroupFieldPathsByAttribute(
                    $field['children'],
                    $parentPath . '/' . $field['id'],
                    $attributeName,
                    $attributeValue
                );
            } elseif (isset($field[$attributeName]) && $field[$attributeName] == $attributeValue) {
                $result[] = $parentPath . '/' . $field['id'];
            }
        }
        return $result;
    }

    /**
     * Collects config paths and their structure paths from configuration files.
     * Returns the map of config paths and their structure paths.
     * All paths are declared in module's system.xml.
     *
     * ```xml
     * <section id="section_id">
     *      <group id="group_id" ...>
     *          <field id="field_one_id" ...>
     *              <label>Field One</label>
     *              ...
     *          </field>
     *          <field id="field_two_id" ...>
     *              <label>Field Two</label>
     *              <config_path>section/group/field</config_path>
     *              ...
     *          </field>
     *      </group>
     * </section>
     * ```
     * If <config_path> node does not exist, then config path duplicates structure path.
     * The result of this example will be:
     *
     * ```php
     * [
     *  'section_id/group_id/field_one_id' => [
     *      'section_id/group_id/field_one_id'
     *  ],
     * 'section/group/field' => [
     *      'section_id/group_id/field_two_id'
     * ]
     * ```
     *
     * @return array An array of config path to config structure path map
     * @since 100.1.12
     */
    public function getFieldPaths()
    {
        $sections = !empty($this->_data['sections']) ? $this->_data['sections'] : [];

        if (!$this->mappedPaths) {
            $this->mappedPaths = $this->getFieldsRecursively($sections);
        }

        return $this->mappedPaths;
    }

    /**
     * Iteration that collects config field paths recursively from config files.
     *
     * @param array $elements The elements to be parsed
     * @return array An array of config path to config structure path map
     */
    private function getFieldsRecursively(array $elements = [])
    {
        $result = [];

        foreach ($elements as $element) {
            if (isset($element['children'])) {
                $result = array_merge_recursive(
                    $result,
                    $this->getFieldsRecursively($element['children'])
                );
            } else {
                if ($element['_elementType'] === 'field') {
                    $structurePath = (isset($element['path']) ? $element['path'] . '/' : '') . $element['id'];
                    $configPath = isset($element['config_path']) ? $element['config_path'] : $structurePath;

                    if (!isset($result[$configPath])) {
                        $result[$configPath] = [];
                    }

                    $result[$configPath][] = $structurePath;
                }
            }
        }

        return $result;
    }
}
