<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\AdminBundle\Admin;

use Knp\Menu\ItemInterface;
use Sonata\AdminBundle\BCLayer\BCHelper;
use Sonata\AdminBundle\Datagrid\DatagridInterface;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\DependencyInjection\Admin\AbstractTaggedAdmin;
use Sonata\AdminBundle\Exception\AdminClassNotFoundException;
use Sonata\AdminBundle\FieldDescription\FieldDescriptionCollection;
use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Form\Type\ModelHiddenType;
use Sonata\AdminBundle\Manipulator\ObjectManipulator;
use Sonata\AdminBundle\Model\ProxyResolverInterface;
use Sonata\AdminBundle\Object\Metadata;
use Sonata\AdminBundle\Object\MetadataInterface;
use Sonata\AdminBundle\Route\RouteCollection;
use Sonata\AdminBundle\Route\RouteCollectionInterface;
use Sonata\AdminBundle\Security\Acl\Permission\AdminPermissionMap;
use Sonata\AdminBundle\Security\Handler\AclSecurityHandlerInterface;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\AdminBundle\Util\Instantiator;
use Sonata\AdminBundle\Util\ParametersManipulator;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface as RoutingUrlGeneratorInterface;
use Symfony\Component\Security\Acl\Model\DomainObjectInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * @phpstan-template T of object
 * @phpstan-extends AbstractTaggedAdmin<T>
 * @phpstan-implements AdminInterface<T>
 */
abstract class AbstractAdmin extends AbstractTaggedAdmin implements AdminInterface, DomainObjectInterface, AdminTreeInterface
{
    // NEXT_MAJOR: Remove the CONTEXT constants.
    /** @deprecated */
    public const CONTEXT_MENU = 'menu';
    /** @deprecated */
    public const CONTEXT_DASHBOARD = 'dashboard';

    public const CLASS_REGEX =
        '@
        (?:([A-Za-z0-9]*)\\\)?        # vendor name / app name
        (Bundle\\\)?                  # optional bundle directory
        ([A-Za-z0-9]+?)(?:Bundle)?\\\ # bundle name, with optional suffix
        (
            Entity|Document|Model|PHPCR|CouchDocument|Phpcr|
            Doctrine\\\Orm|Doctrine\\\Phpcr|Doctrine\\\MongoDB|Doctrine\\\CouchDB
        )\\\(.*)@x';

    private const ACTION_TREE = 1;
    private const ACTION_SHOW = 2;
    private const ACTION_EDIT = 4;
    private const ACTION_DELETE = 8;
    private const ACTION_ACL = 16;
    private const ACTION_HISTORY = 32;
    private const ACTION_LIST = 64;
    private const ACTION_BATCH = 128;
    private const INTERNAL_ACTIONS = [
        'tree' => self::ACTION_TREE,
        'show' => self::ACTION_SHOW,
        'edit' => self::ACTION_EDIT,
        'delete' => self::ACTION_DELETE,
        'acl' => self::ACTION_ACL,
        'history' => self::ACTION_HISTORY,
        'list' => self::ACTION_LIST,
        'batch' => self::ACTION_BATCH,
    ];
    private const MASK_OF_ACTION_CREATE = self::ACTION_TREE | self::ACTION_SHOW | self::ACTION_EDIT | self::ACTION_DELETE | self::ACTION_LIST | self::ACTION_BATCH;
    private const MASK_OF_ACTION_SHOW = self::ACTION_EDIT | self::ACTION_HISTORY | self::ACTION_ACL;
    private const MASK_OF_ACTION_EDIT = self::ACTION_SHOW | self::ACTION_DELETE | self::ACTION_ACL | self::ACTION_HISTORY;
    private const MASK_OF_ACTION_HISTORY = self::ACTION_SHOW | self::ACTION_EDIT | self::ACTION_ACL;
    private const MASK_OF_ACTION_ACL = self::ACTION_EDIT | self::ACTION_HISTORY;
    private const MASK_OF_ACTION_LIST = self::ACTION_SHOW | self::ACTION_EDIT | self::ACTION_DELETE | self::ACTION_ACL | self::ACTION_BATCH;
    private const MASK_OF_ACTIONS_USING_OBJECT = self::MASK_OF_ACTION_SHOW | self::MASK_OF_ACTION_EDIT | self::MASK_OF_ACTION_HISTORY | self::MASK_OF_ACTION_ACL;

    private const DEFAULT_LIST_PER_PAGE_RESULTS = 25;
    private const DEFAULT_LIST_PER_PAGE_OPTIONS = [10, 25, 50, 100, 250];

    /**
     * @deprecated since sonata-project/admin-bundle 4.15, will be removed in 5.0.
     *
     * The base route name used to generate the routing information.
     *
     * @var string|null
     */
    protected $baseRouteName;

    /**
     * @deprecated since sonata-project/admin-bundle 4.15, will be removed in 5.0.
     *
     * The base route pattern used to generate the routing information.
     *
     * @var string|null
     */
    protected $baseRoutePattern;

    /**
     * The label class name  (used in the title/breadcrumb ...).
     *
     * @var string|null
     */
    protected $classnameLabel;

    /**
     * Setting to true will enable preview mode for
     * the entity and show a preview button in the
     * edit/create forms.
     *
     * @var bool
     */
    protected $supportsPreviewMode = false;

    /**
     * The list FieldDescription constructed from the configureListField method.
     *
     * @var array<string, FieldDescriptionInterface>
     */
    private array $listFieldDescriptions = [];

    /**
     * The show FieldDescription constructed from the configureShowFields method.
     *
     * @var FieldDescriptionInterface[]
     */
    private array $showFieldDescriptions = [];

    /**
     * The list FieldDescription constructed from the configureFormField method.
     *
     * @var FieldDescriptionInterface[]
     */
    private array $formFieldDescriptions = [];

    /**
     * The filter FieldDescription constructed from the configureFilterField method.
     *
     * @var FieldDescriptionInterface[]
     */
    private array $filterFieldDescriptions = [];

    /**
     * The maximum number of page numbers to display in the list.
     */
    private int $maxPageLinks = 25;

    /**
     * The translation domain to be used to translate messages.
     */
    private string $translationDomain = 'messages';

    /**
     * Array of routes related to this admin.
     */
    private ?RouteCollectionInterface $routes = null;

    /**
     * The subject only set in edit/update/create mode.
     *
     * @phpstan-var T|null
     */
    private ?object $subject = null;

    /**
     * Define a Collection of child admin, ie /admin/order/{id}/order-element/{childId}.
     *
     * @var array<string, AdminInterface<object>>
     */
    private array $children = [];

    /**
     * Reference the parent admin.
     *
     * @var AdminInterface<object>|null
     */
    private ?AdminInterface $parent = null;

    /**
     * Reference the parent FieldDescription related to this admin
     * only set for FieldDescription which is associated to an Sub Admin instance.
     */
    private ?FieldDescriptionInterface $parentFieldDescription = null;

    /**
     * If true then the current admin is part of the nested admin set (from the url).
     */
    private bool $currentChild = false;

    /**
     * The uniqId is used to avoid clashing with 2 admin related to the code
     * ie: a Block linked to a Block.
     */
    private ?string $uniqId = null;

    /**
     * The current request object.
     */
    private ?Request $request = null;

    /**
     * @phpstan-var DatagridInterface<ProxyQueryInterface<T>>|null
     */
    private ?DatagridInterface $datagrid = null;

    private ?ItemInterface $menu = null;

    /**
     * @var string[]
     */
    private array $formTheme = [];

    /**
     * @var string[]
     */
    private array $filterTheme = [];

    /**
     * @var AdminExtensionInterface[]
     *
     * @phpstan-var array<AdminExtensionInterface<T>>
     */
    private array $extensions = [];

    /**
     * @var array<string, bool>
     */
    private array $cacheIsGranted = [];

    /**
     * @var array<string, string|null>
     */
    private array $parentAssociationMapping = [];

    /**
     * The subclasses supported by the admin class.
     *
     * @var string[]
     *
     * @phpstan-var array<string, class-string<T>>
     */
    private array $subClasses = [];

    /**
     * The list collection.
     *
     * @var FieldDescriptionCollection<FieldDescriptionInterface>|null
     */
    private ?FieldDescriptionCollection $list = null;

    /**
     * @var FieldDescriptionCollection<FieldDescriptionInterface>|null
     */
    private ?FieldDescriptionCollection $show = null;

    private ?FormInterface $form = null;

    /**
     * The cached base route name.
     */
    private ?string $cachedBaseRouteName = null;

    /**
     * The cached base route pattern.
     */
    private ?string $cachedBaseRoutePattern = null;

    /**
     * The form group disposition.
     *
     * @var array<string, array<string, mixed>>
     */
    private array $formGroups = [];

    /**
     * The form tabs disposition.
     *
     * @var array<string, array<string, mixed>>
     */
    private array $formTabs = [];

    /**
     * The view group disposition.
     *
     * @var array<string, array<string, mixed>>
     */
    private array $showGroups = [];

    /**
     * The view tab disposition.
     *
     * @var array<string, array<string, mixed>>
     */
    private array $showTabs = [];

    /**
     * @var array<string, bool>
     */
    private array $loaded = [
        'routes' => false,
        'tab_menu' => false,
        'show' => false,
        'list' => false,
        'form' => false,
        'datagrid' => false,
    ];

    public function getExportFormats(): array
    {
        return [];
    }

    final public function getExportFields(): array
    {
        $fields = $this->configureExportFields();

        foreach ($this->getExtensions() as $extension) {
            $fields = $extension->configureExportFields($this, $fields);
        }

        return $fields;
    }

    final public function getDataSourceIterator(): \Iterator
    {
        $datagrid = $this->getDatagrid();
        $datagrid->buildPager();

        $fields = [];

        foreach ($this->getExportFields() as $key => $field) {
            if (!\is_string($key)) {
                $label = $this->getTranslationLabel($field, 'export', 'label');
                $key = $this->getTranslator()->trans($label, [], $this->getTranslationDomain());
            }

            $fields[$key] = $field;
        }

        $query = $datagrid->getQuery();

        return $this->getDataSource()->createIterator($query, $fields);
    }

    final public function initialize(): void
    {
        if (null === $this->classnameLabel) {
            $namespaceSeparatorPos = strrpos($this->getClass(), '\\');
            $this->classnameLabel = false !== $namespaceSeparatorPos
                ? substr($this->getClass(), $namespaceSeparatorPos + 1)
                : $this->getClass();
        }

        $this->configure();

        foreach ($this->getExtensions() as $extension) {
            $extension->configure($this);
        }
    }

    final public function update(object $object): object
    {
        $this->preUpdate($object);
        foreach ($this->getExtensions() as $extension) {
            $extension->preUpdate($this, $object);
        }

        $this->getModelManager()->update($object);

        $this->postUpdate($object);
        foreach ($this->getExtensions() as $extension) {
            $extension->postUpdate($this, $object);
        }

        return $object;
    }

    final public function create(object $object): object
    {
        $this->prePersist($object);
        foreach ($this->getExtensions() as $extension) {
            $extension->prePersist($this, $object);
        }

        $this->getModelManager()->create($object);

        $this->postPersist($object);
        foreach ($this->getExtensions() as $extension) {
            $extension->postPersist($this, $object);
        }

        $this->createObjectSecurity($object);

        return $object;
    }

    final public function delete(object $object): void
    {
        $this->preRemove($object);
        foreach ($this->getExtensions() as $extension) {
            $extension->preRemove($this, $object);
        }

        $this->getSecurityHandler()->deleteObjectSecurity($this, $object);
        $this->getModelManager()->delete($object);

        $this->postRemove($object);
        foreach ($this->getExtensions() as $extension) {
            $extension->postRemove($this, $object);
        }
    }

    public function preBatchAction(string $actionName, ProxyQueryInterface $query, array &$idx, bool $allElements = false): void
    {
    }

    final public function getDefaultFilterParameters(): array
    {
        return array_merge(
            $this->getDefaultSortValues(),
            $this->getDefaultFilterValues()
        );
    }

    final public function getFilterParameters(): array
    {
        $parameters = $this->getDefaultFilterParameters();

        // build the values array
        if ($this->hasRequest()) {
            /** @var InputBag|ParameterBag $bag */
            $bag = $this->getRequest()->query;
            if ($bag instanceof InputBag) {
                // symfony 5.1+
                $filters = $bag->all('filter');
            } else {
                $filters = $bag->get('filter', []);
                \assert(\is_array($filters));
            }

            if (isset($filters[DatagridInterface::PAGE])) {
                $filters[DatagridInterface::PAGE] = (int) $filters[DatagridInterface::PAGE];
            }
            if (isset($filters[DatagridInterface::PER_PAGE])) {
                $filters[DatagridInterface::PER_PAGE] = (int) $filters[DatagridInterface::PER_PAGE];
            }

            // if filter persistence is configured
            if ($this->hasFilterPersister()) {
                // if reset filters is asked, remove from storage
                if ('reset' === $this->getRequest()->query->get('filters')) {
                    $this->getFilterPersister()->reset($this->getCode());
                }

                // if no filters, fetch from storage
                // otherwise save to storage
                if ([] === $filters) {
                    $filters = $this->getFilterPersister()->get($this->getCode());
                } else {
                    $this->getFilterPersister()->set($this->getCode(), $filters);
                }
            }

            $parameters = ParametersManipulator::merge($parameters, $filters);

            // always force the parent value
            if ($this->isChild()) {
                $parentAssociationMapping = $this->getParentAssociationMapping();
                if (null !== $parentAssociationMapping) {
                    $name = str_replace('.', '__', $parentAssociationMapping);
                    $parameters[$name] = ['value' => $this->getRequest()->get($this->getParent()->getIdParameter())];
                }
            }
        }

        if (
            !isset($parameters[DatagridInterface::PER_PAGE])
            || !\is_int($parameters[DatagridInterface::PER_PAGE])
            || !$this->determinedPerPageValue($parameters[DatagridInterface::PER_PAGE])
        ) {
            $parameters[DatagridInterface::PER_PAGE] = $this->getMaxPerPage();
        }

        $parameters = $this->configureFilterParameters($parameters);

        foreach ($this->getExtensions() as $extension) {
            $parameters = $extension->configureFilterParameters($this, $parameters);
        }

        return $parameters;
    }

    /**
     * Returns the name of the parent related field, so the field can be use to set the default
     * value (ie the parent object) or to filter the object.
     *
     * @throws \LogicException
     */
    final public function getParentAssociationMapping(): ?string
    {
        if (!$this->isChild()) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no parent.',
                static::class
            ));
        }

        $parent = $this->getParent()->getCode();

        return $this->parentAssociationMapping[$parent];
    }

    final public function getBaseRoutePattern(): string
    {
        if (null !== $this->cachedBaseRoutePattern) {
            return $this->cachedBaseRoutePattern;
        }

        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route pattern
            $this->cachedBaseRoutePattern = sprintf(
                '%s/%s/%s',
                $this->getParent()->getBaseRoutePattern(),
                $this->getParent()->getRouterIdParameter(),
                $this->generateBaseRoutePattern(true)
            );
        } else {
            $this->cachedBaseRoutePattern = $this->generateBaseRoutePattern();
        }

        return $this->cachedBaseRoutePattern;
    }

    /**
     * Returns the baseRouteName used to generate the routing information.
     *
     * @return string the baseRouteName used to generate the routing information
     */
    final public function getBaseRouteName(): string
    {
        if (null !== $this->cachedBaseRouteName) {
            return $this->cachedBaseRouteName;
        }

        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route name
            $this->cachedBaseRouteName = sprintf(
                '%s_%s',
                $this->getParent()->getBaseRouteName(),
                $this->generateBaseRouteName(true)
            );
        } else {
            $this->cachedBaseRouteName = $this->generateBaseRouteName();
        }

        return $this->cachedBaseRouteName;
    }

    final public function getClass(): string
    {
        if ($this->hasActiveSubClass()) {
            if ($this->hasParentFieldDescription()) {
                throw new \LogicException('Feature not implemented: an embedded admin cannot have subclass');
            }

            $subClass = $this->getRequest()->query->get('subclass');
            \assert(\is_string($subClass));

            if (!$this->hasSubClass($subClass)) {
                throw new \LogicException(sprintf('Subclass "%s" is not defined.', $subClass));
            }

            return $this->getSubClass($subClass);
        }

        // Do not use `$this->hasSubject()` and `$this->getSubject()` here to avoid infinite loop.
        // `getSubject` use `hasSubject()` which use `getObject()` which use `getClass()`.
        if (null !== $this->subject) {
            $modelManager = $this->getModelManager();
            /** @phpstan-var class-string<T> $class */
            $class = $modelManager instanceof ProxyResolverInterface
                ? $modelManager->getRealClass($this->subject)
                // NEXT_MAJOR: Change to `\get_class($this->subject)` instead
                : BCHelper::getClass($this->subject);

            return $class;
        }

        return $this->getModelClass();
    }

    final public function getSubClasses(): array
    {
        return $this->subClasses;
    }

    final public function setSubClasses(array $subClasses): void
    {
        $this->subClasses = $subClasses;
    }

    final public function hasSubClass(string $name): bool
    {
        return isset($this->subClasses[$name]);
    }

    final public function hasActiveSubClass(): bool
    {
        if (\count($this->subClasses) > 0 && $this->hasRequest()) {
            return \is_string($this->getRequest()->query->get('subclass'));
        }

        return false;
    }

    final public function getActiveSubClass(): string
    {
        if (!$this->hasActiveSubClass()) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no active subclass.',
                static::class
            ));
        }

        return $this->getSubClass($this->getActiveSubclassCode());
    }

    final public function getActiveSubclassCode(): string
    {
        if (!$this->hasActiveSubClass()) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no active subclass.',
                static::class
            ));
        }

        $subClass = (string) $this->getRequest()->query->get('subclass');

        if (!$this->hasSubClass($subClass)) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no active subclass.',
                static::class
            ));
        }

        return $subClass;
    }

    final public function getBatchActions(): array
    {
        if (!$this->hasRoute('batch')) {
            return [];
        }

        $actions = [];

        if ($this->hasRoute('delete') && $this->hasAccess('delete')) {
            $actions['delete'] = [
                'label' => 'action_delete',
                'translation_domain' => 'SonataAdminBundle',
                'ask_confirmation' => true, // by default always true
            ];
        }

        $actions = $this->configureBatchActions($actions);

        foreach ($this->getExtensions() as $extension) {
            $actions = $extension->configureBatchActions($this, $actions);
        }

        foreach ($actions as $name => &$action) {
            if (!\array_key_exists('label', $action)) {
                $action['label'] = $this->getTranslationLabel($name, 'batch', 'label');
            }

            if (!\array_key_exists('translation_domain', $action)) {
                $action['translation_domain'] = $this->getTranslationDomain();
            }
        }

        return $actions;
    }

    final public function getRoutes(): RouteCollectionInterface
    {
        $routes = $this->buildRoutes();
        if (null === $routes) {
            throw new \LogicException('Cannot access routes during the building process.');
        }

        return $routes;
    }

    public function getRouterIdParameter(): string
    {
        return sprintf('{%s}', $this->getIdParameter());
    }

    public function getIdParameter(): string
    {
        $parameter = 'id';

        for ($i = 0; $i < $this->getChildDepth(); ++$i) {
            $parameter = sprintf('child%s', ucfirst($parameter));
        }

        return $parameter;
    }

    final public function hasRoute(string $name): bool
    {
        return $this->getRouteGenerator()->hasAdminRoute($this, $name);
    }

    final public function isCurrentRoute(string $name, ?string $adminCode = null): bool
    {
        if (!$this->hasRequest()) {
            return false;
        }

        $request = $this->getRequest();
        $route = $request->get('_route');

        if (null !== $adminCode) {
            $pool = $this->getConfigurationPool();

            if ($pool->hasAdminByAdminCode($adminCode)) {
                $admin = $pool->getAdminByAdminCode($adminCode);
            } else {
                return false;
            }
        } else {
            $admin = $this;
        }

        return $admin->getRoutes()->getRouteName($name) === $route;
    }

    final public function generateObjectUrl(string $name, object $object, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): string
    {
        $parameters[$this->getIdParameter()] = $this->getUrlSafeIdentifier($object);

        return $this->generateUrl($name, $parameters, $referenceType);
    }

    final public function generateUrl(string $name, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): string
    {
        return $this->getRouteGenerator()->generateUrl($this, $name, $parameters, $referenceType);
    }

    final public function generateMenuUrl(string $name, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): array
    {
        return $this->getRouteGenerator()->generateMenuUrl($this, $name, $parameters, $referenceType);
    }

    final public function getNewInstance(): object
    {
        $object = $this->createNewInstance();

        $this->alterNewInstance($object);

        foreach ($this->getExtensions() as $extension) {
            $extension->alterNewInstance($this, $object);
        }

        return $object;
    }

    final public function getFormBuilder(): FormBuilderInterface
    {
        $formBuilder = $this->getFormContractor()->getFormBuilder(
            $this->getUniqId(),
            ['data_class' => $this->getClass()] + $this->getFormOptions(),
        );

        $this->defineFormBuilder($formBuilder);

        return $formBuilder;
    }

    /**
     * This method is being called by the main admin class and the child class,
     * the getFormBuilder is only call by the main admin class.
     */
    final public function defineFormBuilder(FormBuilderInterface $formBuilder): void
    {
        if (!$this->hasSubject()) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no subject.',
                static::class
            ));
        }

        $mapper = new FormMapper($this->getFormContractor(), $formBuilder, $this);

        $this->configureFormFields($mapper);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureFormFields($mapper);
        }
    }

    final public function attachAdminClass(FieldDescriptionInterface $fieldDescription): void
    {
        $pool = $this->getConfigurationPool();

        try {
            $admin = $pool->getAdminByFieldDescription($fieldDescription);
        } catch (AdminClassNotFoundException $exception) {
            // Using a fieldDescription with no admin class for the target model is a valid case.
            // Since there is no easy way to check for this case, we catch the exception instead.
            return;
        }

        if ($this->hasRequest()) {
            $admin->setRequest($this->getRequest());
        }

        $fieldDescription->setAssociationAdmin($admin);
    }

    /**
     * @param string|int|null $id
     *
     * @phpstan-return T|null
     */
    final public function getObject($id): ?object
    {
        if (null === $id) {
            return null;
        }

        $object = $this->getModelManager()->find($this->getClass(), $id);
        if (null === $object) {
            return null;
        }

        $this->alterObject($object);
        foreach ($this->getExtensions() as $extension) {
            $extension->alterObject($this, $object);
        }

        return $object;
    }

    final public function getForm(): FormInterface
    {
        $form = $this->buildForm();
        if (null === $form) {
            throw new \LogicException('Cannot access form during the building process.');
        }

        return $form;
    }

    final public function getList(): FieldDescriptionCollection
    {
        $list = $this->buildList();
        if (null === $list) {
            throw new \LogicException('Cannot access list during the building process.');
        }

        return $list;
    }

    final public function createQuery(): ProxyQueryInterface
    {
        $query = $this->getModelManager()->createQuery($this->getClass());

        $query = $this->configureQuery($query);
        foreach ($this->getExtensions() as $extension) {
            $extension->configureQuery($this, $query);
        }

        return $query;
    }

    final public function getDatagrid(): DatagridInterface
    {
        $datagrid = $this->buildDatagrid();
        if (null === $datagrid) {
            throw new \LogicException('Cannot access datagrid during the building process.');
        }

        return $datagrid;
    }

    final public function getSideMenu(string $action, ?AdminInterface $childAdmin = null): ItemInterface
    {
        if ($this->isChild()) {
            return $this->getParent()->getSideMenu($action, $this);
        }

        $menu = $this->buildTabMenu($action, $childAdmin);
        if (null === $menu) {
            throw new \LogicException('Cannot access menu during the building process.');
        }

        return $menu;
    }

    final public function getRootCode(): string
    {
        return $this->getRoot()->getCode();
    }

    final public function getRoot(): AdminInterface
    {
        if (!$this->hasParentFieldDescription()) {
            return $this;
        }

        return $this->getParentFieldDescription()->getAdmin()->getRoot();
    }

    final public function getMaxPerPage(): int
    {
        $sortValues = $this->getDefaultSortValues();

        return $sortValues[DatagridInterface::PER_PAGE] ?? self::DEFAULT_LIST_PER_PAGE_RESULTS;
    }

    final public function setMaxPageLinks(int $maxPageLinks): void
    {
        $this->maxPageLinks = $maxPageLinks;
    }

    final public function getMaxPageLinks(): int
    {
        return $this->maxPageLinks;
    }

    final public function getFormGroups(): array
    {
        return $this->formGroups;
    }

    final public function setFormGroups(array $formGroups): void
    {
        $this->formGroups = $formGroups;
    }

    final public function removeFieldFromFormGroup(string $key): void
    {
        foreach ($this->formGroups as $name => $_formGroup) {
            unset($this->formGroups[$name]['fields'][$key]);

            if ([] === $this->formGroups[$name]['fields']) {
                unset($this->formGroups[$name]);
            }
        }
    }

    final public function reorderFormGroup(string $group, array $keys): void
    {
        $formGroups = $this->getFormGroups();
        $formGroups[$group]['fields'] = array_merge(array_flip($keys), $formGroups[$group]['fields']);
        $this->setFormGroups($formGroups);
    }

    final public function getFormTabs(): array
    {
        return $this->formTabs;
    }

    final public function setFormTabs(array $formTabs): void
    {
        $this->formTabs = $formTabs;
    }

    final public function getShowTabs(): array
    {
        return $this->showTabs;
    }

    final public function setShowTabs(array $showTabs): void
    {
        $this->showTabs = $showTabs;
    }

    final public function getShowGroups(): array
    {
        return $this->showGroups;
    }

    final public function setShowGroups(array $showGroups): void
    {
        $this->showGroups = $showGroups;
    }

    final public function removeFieldFromShowGroup(string $key): void
    {
        foreach ($this->showGroups as $name => $_showGroup) {
            unset($this->showGroups[$name]['fields'][$key]);

            if ([] === $this->showGroups[$name]['fields']) {
                unset($this->showGroups[$name]);
            }
        }
    }

    final public function reorderShowGroup(string $group, array $keys): void
    {
        $showGroups = $this->getShowGroups();
        $showGroups[$group]['fields'] = array_merge(array_flip($keys), $showGroups[$group]['fields']);
        $this->setShowGroups($showGroups);
    }

    final public function setParentFieldDescription(FieldDescriptionInterface $parentFieldDescription): void
    {
        $this->parentFieldDescription = $parentFieldDescription;
    }

    final public function getParentFieldDescription(): FieldDescriptionInterface
    {
        if (!$this->hasParentFieldDescription()) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no parent field description.',
                static::class
            ));
        }

        return $this->parentFieldDescription;
    }

    /**
     * @phpstan-assert-if-true !null $this->parentFieldDescription
     */
    final public function hasParentFieldDescription(): bool
    {
        return null !== $this->parentFieldDescription;
    }

    final public function setSubject(?object $subject): void
    {
        if (null !== $subject && !is_a($subject, $this->getModelClass(), true)) {
            throw new \LogicException(sprintf(
                'Admin "%s" does not allow this subject: %s, use the one register with this admin class %s',
                static::class,
                \get_class($subject),
                $this->getModelClass()
            ));
        }

        $this->subject = $subject;
    }

    final public function getSubject(): object
    {
        if (!$this->hasSubject()) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no subject.',
                static::class
            ));
        }

        return $this->subject;
    }

    /**
     * @phpstan-assert-if-true !null $this->subject
     */
    final public function hasSubject(): bool
    {
        if (null === $this->subject && $this->hasRequest() && !$this->hasParentFieldDescription()) {
            $id = $this->getRequest()->get($this->getIdParameter());

            if (null !== $id) {
                $this->subject = $this->getObject($id);
            }
        }

        return null !== $this->subject;
    }

    final public function getFormFieldDescriptions(): array
    {
        $this->buildForm();

        return $this->formFieldDescriptions;
    }

    final public function getFormFieldDescription(string $name): FieldDescriptionInterface
    {
        $this->buildForm();

        if (!$this->hasFormFieldDescription($name)) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no form field description for the field %s.',
                static::class,
                $name
            ));
        }

        return $this->formFieldDescriptions[$name];
    }

    /**
     * Returns true if the admin has a FieldDescription with the given $name.
     */
    final public function hasFormFieldDescription(string $name): bool
    {
        $this->buildForm();

        return \array_key_exists($name, $this->formFieldDescriptions);
    }

    final public function addFormFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
    {
        $this->formFieldDescriptions[$name] = $fieldDescription;
    }

    /**
     * remove a FieldDescription.
     */
    final public function removeFormFieldDescription(string $name): void
    {
        unset($this->formFieldDescriptions[$name]);
    }

    /**
     * build and return the collection of form FieldDescription.
     *
     * @return FieldDescriptionInterface[] collection of form FieldDescription
     */
    final public function getShowFieldDescriptions(): array
    {
        $this->buildShow();

        return $this->showFieldDescriptions;
    }

    /**
     * Returns the form FieldDescription with the given $name.
     */
    final public function getShowFieldDescription(string $name): FieldDescriptionInterface
    {
        $this->buildShow();

        if (!$this->hasShowFieldDescription($name)) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no show field description for the field %s.',
                static::class,
                $name
            ));
        }

        return $this->showFieldDescriptions[$name];
    }

    final public function hasShowFieldDescription(string $name): bool
    {
        $this->buildShow();

        return \array_key_exists($name, $this->showFieldDescriptions);
    }

    final public function addShowFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
    {
        $this->showFieldDescriptions[$name] = $fieldDescription;
    }

    final public function removeShowFieldDescription(string $name): void
    {
        unset($this->showFieldDescriptions[$name]);
    }

    final public function getListFieldDescriptions(): array
    {
        $this->buildList();

        return $this->listFieldDescriptions;
    }

    final public function getListFieldDescription(string $name): FieldDescriptionInterface
    {
        $this->buildList();

        if (!$this->hasListFieldDescription($name)) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no list field description for %s.',
                static::class,
                $name
            ));
        }

        return $this->listFieldDescriptions[$name];
    }

    final public function hasListFieldDescription(string $name): bool
    {
        $this->buildList();

        return \array_key_exists($name, $this->listFieldDescriptions);
    }

    final public function addListFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
    {
        $this->listFieldDescriptions[$name] = $fieldDescription;
    }

    final public function removeListFieldDescription(string $name): void
    {
        unset($this->listFieldDescriptions[$name]);
    }

    final public function getFilterFieldDescription(string $name): FieldDescriptionInterface
    {
        $this->buildDatagrid();

        if (!$this->hasFilterFieldDescription($name)) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no filter field description for the field %s.',
                static::class,
                $name
            ));
        }

        return $this->filterFieldDescriptions[$name];
    }

    final public function hasFilterFieldDescription(string $name): bool
    {
        $this->buildDatagrid();

        return \array_key_exists($name, $this->filterFieldDescriptions);
    }

    final public function addFilterFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
    {
        $this->filterFieldDescriptions[$name] = $fieldDescription;
    }

    final public function removeFilterFieldDescription(string $name): void
    {
        unset($this->filterFieldDescriptions[$name]);
    }

    final public function getFilterFieldDescriptions(): array
    {
        $this->buildDatagrid();

        return $this->filterFieldDescriptions;
    }

    /**
     * @psalm-suppress PossiblyNullArgument Will be solved in NEXT_MAJOR
     */
    final public function addChild(AdminInterface $child, ?string $field = null): void
    {
        $parentAdmin = $this;
        while ($parentAdmin->isChild() && $parentAdmin->getCode() !== $child->getCode()) {
            $parentAdmin = $parentAdmin->getParent();
        }

        if ($parentAdmin->getCode() === $child->getCode()) {
            throw new \LogicException(sprintf(
                'Circular reference detected! The child admin `%s` is already in the parent tree of the `%s` admin.',
                $child->getCode(),
                $this->getCode()
            ));
        }

        $this->children[$child->getCode()] = $child;

        // @phpstan-ignore-next-line Will be solved in NEXT_MAJOR
        $child->setParent($this, $field);
    }

    final public function hasChild(string $code): bool
    {
        return isset($this->children[$code]);
    }

    final public function getChildren(): array
    {
        return $this->children;
    }

    final public function getChild(string $code): AdminInterface
    {
        if (!$this->hasChild($code)) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no child for the code %s.',
                static::class,
                $code
            ));
        }

        return $this->getChildren()[$code];
    }

    final public function setParent(AdminInterface $parent, ?string $parentAssociationMapping = null): void
    {
        $this->parent = $parent;
        $this->parentAssociationMapping[$parent->getCode()] = $parentAssociationMapping;
    }

    final public function getParent(): AdminInterface
    {
        if (null === $this->parent) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no parent.',
                static::class
            ));
        }

        return $this->parent;
    }

    final public function getRootAncestor(): AdminInterface
    {
        $parent = $this;

        while ($parent->isChild()) {
            $parent = $parent->getParent();
        }

        return $parent;
    }

    final public function getChildDepth(): int
    {
        $parent = $this;
        $depth = 0;

        while ($parent->isChild()) {
            $parent = $parent->getParent();
            ++$depth;
        }

        return $depth;
    }

    final public function getCurrentLeafChildAdmin(): ?AdminInterface
    {
        $child = $this->getCurrentChildAdmin();

        if (null === $child) {
            return null;
        }

        for ($c = $child; null !== $c; $c = $child->getCurrentChildAdmin()) {
            $child = $c;
        }

        return $child;
    }

    final public function isChild(): bool
    {
        return $this->parent instanceof AdminInterface;
    }

    /**
     * Returns true if the admin has children, false otherwise.
     *
     * @phpstan-assert-if-true non-empty-array $this->children
     */
    final public function hasChildren(): bool
    {
        return \count($this->children) > 0;
    }

    final public function setUniqId(string $uniqId): void
    {
        $this->uniqId = $uniqId;
    }

    final public function getUniqId(): string
    {
        if (null === $this->uniqId) {
            $this->uniqId = sprintf('s%s', uniqid());
        }

        return $this->uniqId;
    }

    final public function getClassnameLabel(): string
    {
        if (null === $this->classnameLabel) {
            throw new \LogicException(sprintf(
                'Admin "%s" has no classname label. Did you forgot to initialize the admin ?',
                static::class
            ));
        }

        return $this->classnameLabel;
    }

    final public function getPersistentParameters(): array
    {
        $parameters = $this->configurePersistentParameters();
        foreach ($this->getExtensions() as $extension) {
            $parameters = $extension->configurePersistentParameters($this, $parameters);
        }

        return $parameters;
    }

    final public function getPersistentParameter(string $name, $default = null)
    {
        $parameters = $this->getPersistentParameters();

        return $parameters[$name] ?? $default;
    }

    final public function setCurrentChild(bool $currentChild): void
    {
        $this->currentChild = $currentChild;
    }

    final public function isCurrentChild(): bool
    {
        return $this->currentChild;
    }

    final public function getCurrentChildAdmin(): ?AdminInterface
    {
        foreach ($this->getChildren() as $child) {
            if ($child->isCurrentChild()) {
                return $child;
            }
        }

        return null;
    }

    final public function setTranslationDomain(string $translationDomain): void
    {
        $this->translationDomain = $translationDomain;
    }

    final public function getTranslationDomain(): string
    {
        return $this->translationDomain;
    }

    final public function getTranslationLabel(string $label, string $context = '', string $type = ''): string
    {
        return $this->getLabelTranslatorStrategy()->getLabel($label, $context, $type);
    }

    final public function setRequest(Request $request): void
    {
        $this->request = $request;

        foreach ($this->getChildren() as $children) {
            $children->setRequest($request);
        }
    }

    final public function getRequest(): Request
    {
        if (!$this->hasRequest()) {
            throw new \LogicException('The Request object has not been set');
        }

        return $this->request;
    }

    /**
     * @phpstan-assert-if-true !null $this->request
     */
    final public function hasRequest(): bool
    {
        return null !== $this->request;
    }

    final public function getBaseCodeRoute(): string
    {
        if ($this->isChild()) {
            return $this->getParent()->getBaseCodeRoute().'|'.$this->getCode();
        }

        return $this->getCode();
    }

    /**
     * @return string
     */
    public function getObjectIdentifier()
    {
        return $this->getCode();
    }

    public function showInDashboard(): bool
    {
        // NEXT_MAJOR: Remove those lines and uncomment the last one.
        $permissionShow = $this->getPermissionsShow(self::CONTEXT_DASHBOARD, 'sonata_deprecation_mute');
        $permission = 1 === \count($permissionShow) ? reset($permissionShow) : $permissionShow;

        return $this->isGranted($permission);
        // return $this->isGranted('LIST');
    }

    /**
     * NEXT_MAJOR: Remove this method.
     *
     * @deprecated since sonata-project/admin-bundle version 4.7 use showInDashboard instead
     */
    final public function showIn(string $context): bool
    {
        if ('sonata_deprecation_mute' !== (\func_get_args()[1] ?? null)) {
            @trigger_error(sprintf(
                'The "%s()" method is deprecated since sonata-project/admin-bundle version 4.7 and will be'
                .' removed in 5.0 version. Use showInDashboard() instead.',
                __METHOD__
            ), \E_USER_DEPRECATED);
        }

        $permissionShow = $this->getPermissionsShow($context, 'sonata_deprecation_mute');
        // Avoid isGranted deprecation if there is only one permission show.
        $permission = 1 === \count($permissionShow) ? reset($permissionShow) : $permissionShow;

        return $this->isGranted($permission);
    }

    final public function createObjectSecurity(object $object): void
    {
        $this->getSecurityHandler()->createObjectSecurity($this, $object);
    }

    final public function isGranted($name, ?object $object = null): bool
    {
        if (\is_array($name)) {
            @trigger_error(
                sprintf(
                    'Passing an array as argument 1 of "%s()" is deprecated since sonata-project/admin-bundle 4.6'
                    .' and will throw an error in 5.0. You MUST pass a string instead.',
                    __METHOD__
                ),
                \E_USER_DEPRECATED
            );
        }

        $objectRef = null !== $object ? sprintf('/%s#%s', spl_object_hash($object), $this->id($object) ?? '') : '';
        $key = md5(json_encode($name, \JSON_THROW_ON_ERROR).$objectRef);

        if (!\array_key_exists($key, $this->cacheIsGranted)) {
            $this->cacheIsGranted[$key] = $this->getSecurityHandler()->isGranted($this, $name, $object ?? $this);
        }

        return $this->cacheIsGranted[$key];
    }

    final public function getUrlSafeIdentifier(object $model): ?string
    {
        return $this->getModelManager()->getUrlSafeIdentifier($model);
    }

    final public function getNormalizedIdentifier(object $model): ?string
    {
        return $this->getModelManager()->getNormalizedIdentifier($model);
    }

    public function id(object $model): ?string
    {
        return $this->getNormalizedIdentifier($model);
    }

    final public function getShow(): FieldDescriptionCollection
    {
        $show = $this->buildShow();
        if (null === $show) {
            throw new \LogicException('Cannot access show during the building process.');
        }

        return $show;
    }

    final public function setFormTheme(array $formTheme): void
    {
        $this->formTheme = $formTheme;
    }

    final public function getFormTheme(): array
    {
        return $this->formTheme;
    }

    final public function setFilterTheme(array $filterTheme): void
    {
        $this->filterTheme = $filterTheme;
    }

    final public function getFilterTheme(): array
    {
        return $this->filterTheme;
    }

    final public function addExtension(AdminExtensionInterface $extension): void
    {
        $this->extensions[] = $extension;
    }

    /**
     * @phpstan-param AdminExtensionInterface<T> $extension
     */
    final public function removeExtension(AdminExtensionInterface $extension): void
    {
        $key = array_search($extension, $this->extensions, true);
        if (false === $key) {
            throw new \InvalidArgumentException(
                sprintf('The extension "%s" was not set to the "%s" admin.', \get_class($extension), self::class)
            );
        }

        unset($this->extensions[$key]);
    }

    final public function getExtensions(): array
    {
        return $this->extensions;
    }

    public function toString(object $object): string
    {
        if (method_exists($object, '__toString') && null !== $object->__toString()) {
            return $object->__toString();
        }

        $modelManager = $this->getModelManager();
        if ($modelManager instanceof ProxyResolverInterface) {
            $class = $modelManager->getRealClass($object);
        } else {
            // NEXT_MAJOR: Change to `\get_class($object)`
            $class = BCHelper::getClass($object);
        }

        return sprintf('%s:%s', $class, spl_object_hash($object));
    }

    final public function supportsPreviewMode(): bool
    {
        return $this->supportsPreviewMode;
    }

    /**
     * Returns predefined per page options.
     *
     * @return array<int>
     */
    public function getPerPageOptions(): array
    {
        $perPageOptions = self::DEFAULT_LIST_PER_PAGE_OPTIONS;
        $perPageOptions[] = $this->getMaxPerPage();

        $perPageOptions = array_unique($perPageOptions);
        sort($perPageOptions);

        return $perPageOptions;
    }

    /**
     * Returns true if the per page value is allowed, false otherwise.
     */
    final public function determinedPerPageValue(int $perPage): bool
    {
        return \in_array($perPage, $this->getPerPageOptions(), true);
    }

    final public function isAclEnabled(): bool
    {
        return $this->getSecurityHandler() instanceof AclSecurityHandlerInterface;
    }

    public function getObjectMetadata(object $object): MetadataInterface
    {
        return new Metadata($this->toString($object));
    }

    final public function setListMode(string $mode): void
    {
        $this->getRequest()->getSession()->set(sprintf('%s.list_mode', $this->getCode()), $mode);
    }

    final public function getListMode(): string
    {
        $defaultListMode = array_keys($this->getListModes())[0];
        if (!$this->hasRequest() || !$this->getRequest()->hasSession()) {
            return $defaultListMode;
        }

        return $this->getRequest()->getSession()->get(sprintf('%s.list_mode', $this->getCode()), $defaultListMode);
    }

    final public function checkAccess(string $action, ?object $object = null): void
    {
        $access = $this->getAccess();

        if (!\array_key_exists($action, $access)) {
            throw new \InvalidArgumentException(sprintf(
                'Action "%s" could not be found in access mapping.'
                .' Please make sure your action is defined into your admin class accessMapping property.',
                $action
            ));
        }

        if (!\is_array($access[$action])) {
            $access[$action] = [$access[$action]];
        }

        foreach ($access[$action] as $role) {
            if (false === $this->isGranted($role, $object)) {
                throw new AccessDeniedException(sprintf('Access Denied to the action %s and role %s', $action, $role));
            }
        }
    }

    final public function hasAccess(string $action, ?object $object = null): bool
    {
        $access = $this->getAccess();

        if (!\array_key_exists($action, $access)) {
            return false;
        }

        if (!\is_array($access[$action])) {
            $access[$action] = [$access[$action]];
        }

        foreach ($access[$action] as $role) {
            if (false === $this->isGranted($role, $object)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @return array<string, array<string, mixed>>
     *
     * @phpstan-param T|null $object
     */
    final public function getActionButtons(string $action, ?object $object = null): array
    {
        $defaultButtonList = $this->getDefaultActionButtons($action, $object);
        $buttonList = $this->configureActionButtons($defaultButtonList, $action, $object);

        foreach ($this->getExtensions() as $extension) {
            $buttonList = $extension->configureActionButtons($this, $buttonList, $action, $object);
        }

        return $buttonList;
    }

    /**
     * Get the list of actions that can be accessed directly from the dashboard.
     *
     * @return array<string, array<string, mixed>>
     */
    final public function getDashboardActions(): array
    {
        $actions = [];

        if ($this->hasRoute('create') && $this->hasAccess('create')) {
            $actions['create'] = [
                'label' => 'link_add',
                'translation_domain' => 'SonataAdminBundle',
                'template' => $this->getTemplateRegistry()->getTemplate('action_create'),
                'url' => $this->generateUrl('create'),
                'icon' => 'fas fa-plus-circle',
            ];
        }

        if ($this->hasRoute('list') && $this->hasAccess('list')) {
            $actions['list'] = [
                'label' => 'link_list',
                'translation_domain' => 'SonataAdminBundle',
                'url' => $this->generateUrl('list'),
                'icon' => 'fas fa-list',
            ];
        }

        $actions = $this->configureDashboardActions($actions);

        foreach ($this->getExtensions() as $extension) {
            $actions = $extension->configureDashboardActions($this, $actions);
        }

        return $actions;
    }

    final public function createFieldDescription(string $propertyName, array $options = []): FieldDescriptionInterface
    {
        $fieldDescriptionFactory = $this->getFieldDescriptionFactory();

        $fieldDescription = $fieldDescriptionFactory->create($this->getClass(), $propertyName, $options);

        $fieldDescription->setAdmin($this);

        return $fieldDescription;
    }

    /**
     * Hook to run after initialization.
     */
    protected function configure(): void
    {
    }

    protected function generateBaseRoutePattern(bool $isChildAdmin = false): string
    {
        // NEXT_MAJOR: Remove this code
        if (null !== $this->baseRoutePattern) {
            @trigger_error(sprintf(
                'Overriding the baseRoutePattern property is deprecated since sonata-project/admin-bundle 4.15.'
                .' You MUST override the method %s() instead.',
                __METHOD__
            ), \E_USER_DEPRECATED);

            return $this->baseRoutePattern;
        }

        preg_match(self::CLASS_REGEX, $this->getModelClass(), $matches);

        if ([] === $matches) {
            throw new \LogicException(sprintf(
                'Please define a default `baseRoutePattern` value for the admin class `%s`',
                static::class
            ));
        }

        if ($isChildAdmin) {
            return $this->urlize($matches[5], '-');
        }

        return sprintf(
            '/%s%s/%s',
            '' === $matches[1] ? '' : $this->urlize($matches[1], '-').'/',
            $this->urlize($matches[3], '-'),
            $this->urlize($matches[5], '-')
        );
    }

    protected function generateBaseRouteName(bool $isChildAdmin = false): string
    {
        // NEXT_MAJOR: Remove this code
        if (null !== $this->baseRouteName) {
            @trigger_error(sprintf(
                'Overriding the baseRouteName property is deprecated since sonata-project/admin-bundle 4.15.'
                .' You MUST override the method %s() instead.',
                __METHOD__
            ), \E_USER_DEPRECATED);

            return $this->baseRouteName;
        }

        preg_match(self::CLASS_REGEX, $this->getModelClass(), $matches);

        if ([] === $matches) {
            throw new \LogicException(sprintf(
                'Cannot automatically determine base route name,'
                .' please define a default `baseRouteName` value for the admin class `%s`',
                static::class
            ));
        }

        if ($isChildAdmin) {
            return $this->urlize($matches[5]);
        }

        return sprintf(
            'admin_%s%s_%s',
            '' === $matches[1] ? '' : $this->urlize($matches[1]).'_',
            $this->urlize($matches[3]),
            $this->urlize($matches[5])
        );
    }

    /**
     * @phpstan-return T
     */
    protected function createNewInstance(): object
    {
        $object = Instantiator::instantiate($this->getClass());
        $this->appendParentObject($object);

        return $object;
    }

    /**
     * @phpstan-param T $object
     */
    protected function alterNewInstance(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function alterObject(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function preValidate(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function preUpdate(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function postUpdate(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function prePersist(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function postPersist(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function preRemove(object $object): void
    {
    }

    /**
     * @phpstan-param T $object
     */
    protected function postRemove(object $object): void
    {
    }

    /**
     * @return array<string, mixed>
     */
    protected function configurePersistentParameters(): array
    {
        return [];
    }

    /**
     * @return string[]
     */
    protected function configureExportFields(): array
    {
        return $this->getModelManager()->getExportFields($this->getClass());
    }

    /**
     * @param ProxyQueryInterface<T> $query
     *
     * @return ProxyQueryInterface<T>
     */
    protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
    {
        return $query;
    }

    /**
     * urlize the given word.
     *
     * @param string $sep the separator
     */
    final protected function urlize(string $word, string $sep = '_'): string
    {
        return strtolower(preg_replace('/[^a-z0-9_]/i', $sep.'$1', $word) ?? '');
    }

    /**
     * @param array<string, mixed> $parameters
     *
     * @return array<string, mixed>
     */
    protected function configureFilterParameters(array $parameters): array
    {
        return $parameters;
    }

    /**
     * Returns a list of default sort values.
     *
     * @phpstan-return array{
     *     _page?: int,
     *     _per_page?: int,
     *     _sort_by?: string,
     *     _sort_order?: string
     * }
     */
    final protected function getDefaultSortValues(): array
    {
        $defaultSortValues = [DatagridInterface::PAGE => 1, DatagridInterface::PER_PAGE => self::DEFAULT_LIST_PER_PAGE_RESULTS];

        $this->configureDefaultSortValues($defaultSortValues);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureDefaultSortValues($this, $defaultSortValues);
        }

        return $defaultSortValues;
    }

    /**
     * Returns a list of default filters.
     *
     * @return array<string, array<string, mixed>>
     */
    final protected function getDefaultFilterValues(): array
    {
        $defaultFilterValues = [];

        $this->configureDefaultFilterValues($defaultFilterValues);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureDefaultFilterValues($this, $defaultFilterValues);
        }

        return $defaultFilterValues;
    }

    /**
     * @return array<string, mixed>
     */
    final protected function getFormOptions(): array
    {
        $formOptions = [];

        $this->configureFormOptions($formOptions);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureFormOptions($this, $formOptions);
        }

        return $formOptions;
    }

    /**
     * @phpstan-param FormMapper<T> $form
     */
    protected function configureFormFields(FormMapper $form): void
    {
    }

    /**
     * @phpstan-param ListMapper<T> $list
     */
    protected function configureListFields(ListMapper $list): void
    {
    }

    /**
     * @phpstan-param DatagridMapper<T> $filter
     */
    protected function configureDatagridFilters(DatagridMapper $filter): void
    {
    }

    /**
     * @phpstan-param ShowMapper<T> $show
     */
    protected function configureShowFields(ShowMapper $show): void
    {
    }

    protected function configureRoutes(RouteCollectionInterface $collection): void
    {
    }

    /**
     * @param array<string, array<string, mixed>> $buttonList
     *
     * @return array<string, array<string, mixed>>
     *
     * @phpstan-param T|null $object
     */
    protected function configureActionButtons(array $buttonList, string $action, ?object $object = null): array
    {
        return $buttonList;
    }

    /**
     * @param array<string, array<string, mixed>> $actions
     *
     * @return array<string, array<string, mixed>>
     */
    protected function configureDashboardActions(array $actions): array
    {
        return $actions;
    }

    /**
     * Allows you to customize batch actions.
     *
     * @param array<string, array<string, mixed>> $actions
     *
     * @return array<string, array<string, mixed>>
     */
    protected function configureBatchActions(array $actions): array
    {
        return $actions;
    }

    /**
     * Configures the tab menu in your admin.
     *
     * @phpstan-template TChild of object
     * @phpstan-param AdminInterface<TChild>|null $childAdmin
     */
    protected function configureTabMenu(ItemInterface $menu, string $action, ?AdminInterface $childAdmin = null): void
    {
    }

    /**
     * Gets the subclass corresponding to the given name.
     *
     * @phpstan-return class-string<T>
     */
    protected function getSubClass(string $name): string
    {
        if ($this->hasSubClass($name)) {
            return $this->subClasses[$name];
        }

        throw new \LogicException(sprintf('Unable to find the subclass `%s` for admin `%s`', $name, static::class));
    }

    /**
     * Return list routes with permissions name.
     *
     * @return array<string, string|string[]>
     */
    final protected function getAccess(): array
    {
        $access = array_merge([
            'acl' => AdminPermissionMap::PERMISSION_MASTER,
            'export' => AdminPermissionMap::PERMISSION_EXPORT,
            'historyCompareRevisions' => AdminPermissionMap::PERMISSION_HISTORY,
            'historyViewRevision' => AdminPermissionMap::PERMISSION_HISTORY,
            'history' => AdminPermissionMap::PERMISSION_HISTORY,
            'edit' => AdminPermissionMap::PERMISSION_EDIT,
            'show' => AdminPermissionMap::PERMISSION_VIEW,
            'create' => AdminPermissionMap::PERMISSION_CREATE,
            'delete' => AdminPermissionMap::PERMISSION_DELETE,
            'batchDelete' => AdminPermissionMap::PERMISSION_DELETE,
            'list' => AdminPermissionMap::PERMISSION_LIST,
        ], $this->getAccessMapping());

        foreach ($this->getExtensions() as $extension) {
            $access = array_merge($access, $extension->getAccessMapping($this));
        }

        return $access;
    }

    /**
     * @return array<string, string|string[]> [action1 => requiredRole1, action2 => [requiredRole2, requiredRole3]]
     */
    protected function getAccessMapping(): array
    {
        return [];
    }

    /**
     * Return the list of permissions the user should have in order to display the admin.
     *
     * NEXT_MAJOR: Remove this method.
     *
     * @deprecated since sonata-project/admin-bundle version 4.7
     *
     * @return string[]
     */
    protected function getPermissionsShow(string $context): array
    {
        if ('sonata_deprecation_mute' !== (\func_get_args()[1] ?? null)) {
            @trigger_error(sprintf(
                'The "%s()" method is deprecated since sonata-project/admin-bundle version 4.7 and will be'
                .' removed in 5.0 version.',
                __METHOD__
            ), \E_USER_DEPRECATED);
        }

        return ['LIST'];
    }

    /**
     * Configures a list of default filters.
     *
     * @param array<string, array<string, mixed>> $filterValues
     */
    protected function configureDefaultFilterValues(array &$filterValues): void
    {
    }

    /**
     * Configures a list of form options.
     *
     * @param array<string, mixed> $formOptions
     */
    protected function configureFormOptions(array &$formOptions): void
    {
    }

    /**
     * Configures a list of default sort values.
     *
     * Example:
     *   $sortValues[DatagridInterface::SORT_BY] = 'foo'
     *   $sortValues[DatagridInterface::SORT_ORDER] = 'DESC'
     *
     * @param array<string, string|int> $sortValues
     *
     * @phpstan-param array{
     *     _page?: int,
     *     _per_page?: int,
     *     _sort_by?: string,
     *     _sort_order?: string
     * } $sortValues
     */
    protected function configureDefaultSortValues(array &$sortValues): void
    {
    }

    /**
     * Set the parent object, if any, to the provided object.
     *
     * @phpstan-param T $object
     */
    final protected function appendParentObject(object $object): void
    {
        if ($this->isChild()) {
            $parentAssociationMapping = $this->getParentAssociationMapping();

            if (null !== $parentAssociationMapping) {
                $parentAdmin = $this->getParent();
                $parentObject = $parentAdmin->getObject($this->getRequest()->get($parentAdmin->getIdParameter()));

                if (null !== $parentObject) {
                    $propertyAccessor = PropertyAccess::createPropertyAccessor();

                    try {
                        $value = $propertyAccessor->getValue($object, $parentAssociationMapping);
                    } catch (AccessException $e) {
                        // @todo: Catching and checking AccessException here as BC for symfony/property-access < 5.1.
                        //        Catch UninitializedPropertyException and remove the check when dropping support < 5.1
                        if (AccessException::class !== \get_class($e) && !$e instanceof UninitializedPropertyException) {
                            throw $e; // Re-throw. We only want to "ignore" pure AccessException (Sf < 5.1) and UninitializedPropertyException (Sf >= 5.1)
                        }
                        $value = null;
                    }

                    if (\is_array($value) || $value instanceof \ArrayAccess) {
                        $value[] = $parentObject;
                        $propertyAccessor->setValue($object, $parentAssociationMapping, $value);
                    } else {
                        $propertyAccessor->setValue($object, $parentAssociationMapping, $parentObject);
                    }
                }

                return;
            }
        }

        if ($this->hasParentFieldDescription()) {
            $parentAdmin = $this->getParentFieldDescription()->getAdmin();
            $parentObject = $parentAdmin->getObject($this->getRequest()->get($parentAdmin->getIdParameter()));

            if (null !== $parentObject) {
                ObjectManipulator::setObject($object, $parentObject, $this->getParentFieldDescription());
            }
        }
    }

    /**
     * @return array<string, array<string, mixed>>
     *
     * @phpstan-param T|null $object
     */
    private function getDefaultActionButtons(string $action, ?object $object = null): array
    {
        // nothing to do for non-internal actions
        if (!isset(self::INTERNAL_ACTIONS[$action])) {
            return [];
        }

        $buttonList = [];

        $actionBit = self::INTERNAL_ACTIONS[$action];

        if (0 !== (self::MASK_OF_ACTION_CREATE & $actionBit)
            && $this->hasRoute('create')
            && $this->hasAccess('create')
        ) {
            $buttonList['create'] = [
                'template' => $this->getTemplateRegistry()->getTemplate('button_create'),
            ];
        }

        $canAccessObject = 0 !== (self::MASK_OF_ACTIONS_USING_OBJECT & $actionBit)
            && null !== $object
            && null !== $this->id($object);

        if ($canAccessObject
            && 0 !== (self::MASK_OF_ACTION_EDIT & $actionBit)
            && $this->hasRoute('edit')
            && $this->hasAccess('edit', $object)
        ) {
            $buttonList['edit'] = [
                'template' => $this->getTemplateRegistry()->getTemplate('button_edit'),
            ];
        }

        if ($canAccessObject
            && 0 !== (self::MASK_OF_ACTION_HISTORY & $actionBit)
            && $this->hasRoute('history')
            && $this->hasAccess('history', $object)
        ) {
            $buttonList['history'] = [
                'template' => $this->getTemplateRegistry()->getTemplate('button_history'),
            ];
        }

        if ($canAccessObject
            && 0 !== (self::MASK_OF_ACTION_ACL & $actionBit)
            && $this->isAclEnabled()
            && $this->hasRoute('acl')
            && $this->hasAccess('acl', $object)
        ) {
            $buttonList['acl'] = [
                'template' => $this->getTemplateRegistry()->getTemplate('button_acl'),
            ];
        }

        if ($canAccessObject
            && 0 !== (self::MASK_OF_ACTION_SHOW & $actionBit)
            && $this->hasRoute('show')
            && $this->hasAccess('show', $object)
            && \count($this->getShow()) > 0
        ) {
            $buttonList['show'] = [
                'template' => $this->getTemplateRegistry()->getTemplate('button_show'),
            ];
        }

        if (0 !== (self::MASK_OF_ACTION_LIST & $actionBit)
            && $this->hasRoute('list')
            && $this->hasAccess('list')
        ) {
            $buttonList['list'] = [
                'template' => $this->getTemplateRegistry()->getTemplate('button_list'),
            ];
        }

        return $buttonList;
    }

    /**
     * @return DatagridInterface<ProxyQueryInterface<T>>|null
     */
    private function buildDatagrid(): ?DatagridInterface
    {
        if ($this->loaded['datagrid']) {
            return $this->datagrid;
        }

        $this->loaded['datagrid'] = true;

        $filterParameters = $this->getFilterParameters();

        // transform DatagridInterface::SORT_BY filter parameter from a string to a FieldDescriptionInterface for the datagrid.
        if (isset($filterParameters[DatagridInterface::SORT_BY]) && \is_string($filterParameters[DatagridInterface::SORT_BY])) {
            if ($this->hasListFieldDescription($filterParameters[DatagridInterface::SORT_BY])) {
                $filterParameters[DatagridInterface::SORT_BY] = $this->getListFieldDescription($filterParameters[DatagridInterface::SORT_BY]);
            } else {
                $filterParameters[DatagridInterface::SORT_BY] = $this->createFieldDescription(
                    $filterParameters[DatagridInterface::SORT_BY]
                );

                $this->getListBuilder()->buildField(null, $filterParameters[DatagridInterface::SORT_BY]);
            }
        }

        // initialize the datagrid
        $this->datagrid = $this->getDatagridBuilder()->getBaseDatagrid($this, $filterParameters);

        $this->datagrid->getPager()->setMaxPageLinks($this->getMaxPageLinks());

        /** @psalm-suppress InvalidArgument https://github.com/vimeo/psalm/issues/8423 */
        $mapper = new DatagridMapper($this->getDatagridBuilder(), $this->datagrid, $this);

        // build the datagrid filter
        $this->configureDatagridFilters($mapper);

        // ok, try to limit to add parent filter
        if ($this->isChild()) {
            $parentAssociationMapping = $this->getParentAssociationMapping();

            if (null !== $parentAssociationMapping && !$mapper->has($parentAssociationMapping)) {
                $mapper->add($parentAssociationMapping, null, [
                    'show_filter' => false,
                    'label' => false,
                    'field_type' => ModelHiddenType::class,
                    'field_options' => [
                        'model_manager' => $this->getParent()->getModelManager(),
                        'class' => $this->getParent()->getClass(),
                    ],
                    'operator_type' => HiddenType::class,
                ], [
                    'admin_code' => $this->getParent()->getCode(),
                ]);
            }
        }

        foreach ($this->getExtensions() as $extension) {
            $extension->configureDatagridFilters($mapper);
        }

        return $this->datagrid;
    }

    /**
     * @return FieldDescriptionCollection<FieldDescriptionInterface>|null
     */
    private function buildShow(): ?FieldDescriptionCollection
    {
        if ($this->loaded['show']) {
            return $this->show;
        }

        $this->loaded['show'] = true;

        $this->show = $this->getShowBuilder()->getBaseList();
        $mapper = new ShowMapper($this->getShowBuilder(), $this->show, $this);

        $this->configureShowFields($mapper);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureShowFields($mapper);
        }

        return $this->show;
    }

    /**
     * @return FieldDescriptionCollection<FieldDescriptionInterface>|null
     */
    private function buildList(): ?FieldDescriptionCollection
    {
        if ($this->loaded['list']) {
            return $this->list;
        }

        $this->loaded['list'] = true;

        $this->list = $this->getListBuilder()->getBaseList();
        $mapper = new ListMapper($this->getListBuilder(), $this->list, $this);

        if (\count($this->getBatchActions()) > 0 && $this->hasRequest() && !$this->getRequest()->isXmlHttpRequest()) {
            $mapper->add(ListMapper::NAME_BATCH, ListMapper::TYPE_BATCH, [
                'label' => 'batch',
                'sortable' => false,
                'virtual_field' => true,
                'template' => $this->getTemplateRegistry()->getTemplate('batch'),
            ]);
        }

        $this->configureListFields($mapper);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureListFields($mapper);
        }

        if ($this->hasRequest()
            && $this->getRequest()->isXmlHttpRequest()
            && $this->getRequest()->query->getBoolean('select', true) // NEXT_MAJOR: Change the default value to `false` in version 5
        ) {
            $mapper->add(ListMapper::NAME_SELECT, ListMapper::TYPE_SELECT, [
                'label' => false,
                'sortable' => false,
                'virtual_field' => false,
                'template' => $this->getTemplateRegistry()->getTemplate('select'),
            ]);
        }

        return $this->list;
    }

    private function buildForm(): ?FormInterface
    {
        if ($this->loaded['form']) {
            return $this->form;
        }

        $this->loaded['form'] = true;

        $formBuilder = $this->getFormBuilder();
        $formBuilder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
            /** @phpstan-var T $data */
            $data = $event->getData();
            $this->preValidate($data);
        }, 100);

        $this->form = $formBuilder->getForm();

        return $this->form;
    }

    private function buildRoutes(): ?RouteCollectionInterface
    {
        if ($this->loaded['routes']) {
            return $this->routes;
        }

        $this->loaded['routes'] = true;

        $routes = new RouteCollection(
            $this->getBaseCodeRoute(),
            $this->getBaseRouteName(),
            $this->getBaseRoutePattern(),
            $this->getBaseControllerName()
        );

        $this->getRouteBuilder()->build($this, $routes);

        $this->configureRoutes($routes);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureRoutes($this, $routes);
        }

        $this->routes = $routes;

        return $this->routes;
    }

    /**
     * @phpstan-template TChild of object
     * @phpstan-param AdminInterface<TChild>|null $childAdmin
     */
    private function buildTabMenu(string $action, ?AdminInterface $childAdmin = null): ?ItemInterface
    {
        if ($this->loaded['tab_menu']) {
            return $this->menu;
        }

        $this->loaded['tab_menu'] = true;

        $menu = $this->getMenuFactory()->createItem('root');
        $menu->setChildrenAttribute('class', 'nav navbar-nav');
        $menu->setExtra('translation_domain', $this->getTranslationDomain());

        $this->configureTabMenu($menu, $action, $childAdmin);

        foreach ($this->getExtensions() as $extension) {
            $extension->configureTabMenu($this, $menu, $action, $childAdmin);
        }

        $this->menu = $menu;

        return $this->menu;
    }
}
