<?php

/**
 * ---------------------------------------------------------------------
 *
 * GLPI - Gestionnaire Libre de Parc Informatique
 *
 * http://glpi-project.org
 *
 * @copyright 2015-2025 Teclib' and contributors.
 * @copyright 2003-2014 by the INDEPNET Development Team.
 * @licence   https://www.gnu.org/licenses/gpl-3.0.html
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * ---------------------------------------------------------------------
 */

use Glpi\Application\View\TemplateRenderer;
use Glpi\Event;
use Glpi\Plugin\Hooks;
use Glpi\RichText\RichText;
use Glpi\Team\Team;
use Glpi\Toolbox\Sanitizer;

/**
 * CommonITILObject Class
 *
 * @property-read array $users
 * @property-read array $groups
 * @property-read array $suppliers
 **/
abstract class CommonITILObject extends CommonDBTM
{
    use \Glpi\Features\Clonable;
    use \Glpi\Features\Timeline;
    use \Glpi\Features\Kanban;
    use \Glpi\Features\Teamwork;

    /// Users by type
    protected $lazy_loaded_users = null;
    public $userlinkclass  = '';
    /// Groups by type
    protected $lazy_loaded_groups = null;
    public $grouplinkclass = '';

    /// Suppliers by type
    protected $lazy_loaded_suppliers = null;
    public $supplierlinkclass = '';

    /// Use user entity to select entity of the object
    protected $userentity_oncreate = false;

    public $deduplicate_queued_notifications = false;

    protected static $showTitleInNavigationHeader = true;

    public const MATRIX_FIELD         = '';
    public const URGENCY_MASK_FIELD   = '';
    public const IMPACT_MASK_FIELD    = '';
    public const STATUS_MATRIX_FIELD  = '';


    // STATUS
    public const INCOMING      = 1; // new
    public const ASSIGNED      = 2; // assign
    public const PLANNED       = 3; // plan
    public const WAITING       = 4; // waiting
    public const SOLVED        = 5; // solved
    public const CLOSED        = 6; // closed
    public const ACCEPTED      = 7; // accepted
    public const OBSERVED      = 8; // observe
    public const EVALUATION    = 9; // evaluation
    public const APPROVAL      = 10; // approbation
    public const TEST          = 11; // test
    public const QUALIFICATION = 12; // qualification

    public const NO_TIMELINE       = -1;
    public const TIMELINE_NOTSET   = 0;
    public const TIMELINE_LEFT     = 1;
    public const TIMELINE_MIDLEFT  = 2;
    public const TIMELINE_MIDRIGHT = 3;
    public const TIMELINE_RIGHT    = 4;

    public const TIMELINE_ORDER_NATURAL = 'natural';
    public const TIMELINE_ORDER_REVERSE = 'reverse';

    abstract public static function getTaskClass();

    public function post_getFromDB()
    {
        // Object may be reused to load multiples tickets thus we must clear all
        // cached data when a new mysql row is loaded
        $this->clearLazyLoadedActors();
    }

    /**
     * Load linked users
     *
     * @return void
     */
    public function loadUsers(): void
    {
        if (!empty($this->userlinkclass) && !$this->isNewItem()) {
            $class = new $this->userlinkclass();
            $this->lazy_loaded_users = $class->getActors($this->fields['id']);
        } else {
            $this->lazy_loaded_users = [];
        }
    }

    /**
     * Load linked groups
     *
     * @return void
     */
    protected function loadGroups(): void
    {
        if (!empty($this->grouplinkclass) && !$this->isNewItem()) {
            $class = new $this->grouplinkclass();
            $this->lazy_loaded_groups = $class->getActors($this->fields['id']);
        } else {
            $this->lazy_loaded_groups = [];
        }
    }

    /**
     * Load linked suppliers
     *
     * @return void
     */
    public function loadSuppliers(): void
    {
        if (!empty($this->supplierlinkclass) && !$this->isNewItem()) {
            $class = new $this->supplierlinkclass();
            $this->lazy_loaded_suppliers = $class->getActors($this->fields['id']);
        } else {
            $this->lazy_loaded_suppliers = [];
        }
    }

    /**
     * @since 0.84
     **/
    public function loadActors()
    {
        // TODO 10.1 (breaking change): method should be protected instead of public

        // Might not be 100% needed to clear cache here but lets be safe
        // This way, any direct call to loadActors is assured to return accurate data
        $this->clearLazyLoadedActors();

        // Load each actors type
        $this->loadUsers();
        $this->loadGroups();
        $this->loadSuppliers();
    }

    /**
     * Clear lazy loaded actor data so it can be recomputed again next time its
     * accessed
     *
     * @return void
     */
    protected function clearLazyLoadedActors(): void
    {
        $this->lazy_loaded_users = null;
        $this->lazy_loaded_groups = null;
        $this->lazy_loaded_suppliers = null;
    }

    /**
     * Magic getter for lazy loaded properties
     *
     * @param string $property_name
     */
    public function __get(string $property_name)
    {
        switch ($property_name) {
            case 'users':
                if ($this->lazy_loaded_users === null) {
                    $this->loadUsers();
                }
                return $this->lazy_loaded_users;

            case 'groups':
                if ($this->lazy_loaded_groups === null) {
                    $this->loadGroups();
                }
                return $this->lazy_loaded_groups;

            case 'suppliers':
                if ($this->lazy_loaded_suppliers === null) {
                    $this->loadSuppliers();
                }
                return $this->lazy_loaded_suppliers;

            default:
                // Log error and keep running
                // TODO 10.1: throw exception instead
                trigger_error("Unknown field: '$property_name'", E_USER_WARNING);
                return null;
        }
    }

    /**
     * Magic setter for lazy loaded properties
     *
     * @param string $property_name
     * @param mixed $value
     */
    public function __set(string $property_name, $value)
    {
        switch ($property_name) {
            case 'users':
            case 'groups':
            case 'suppliers':
                // Log error and keep running
                // TODO 10.1: throw exception instead
                trigger_error("Readonly field: '$property_name'", E_USER_WARNING);
                break;

            default:
                if (version_compare(PHP_VERSION, '8.2.0', '<')) {
                    // Trigger same deprecation notice as the one triggered by PHP 8.2+
                    trigger_error(
                        sprintf('Creation of dynamic property %s::$%s is deprecated', get_called_class(), $property_name),
                        E_USER_DEPRECATED
                    );
                }
                $this->$property_name = $value;
                break;
        }
    }

    /**
     * Magic handler for isset() calls on lazy loaded properties
     *
     * @param string $property_name
     */
    public function __isset(string $property_name)
    {
        switch ($property_name) {
            case 'users':
            case 'groups':
            case 'suppliers':
                return true;

            default:
                // Log error and keep running
                // TODO 10.1: throw exception instead
                trigger_error("Unknown field: '$property_name'", E_USER_WARNING);
                return false;
        }
    }

    /**
     * Magic handler for unset() calls on lazy loaded properties
     *
     * @param string $property_name
     */
    public function __unset(string $property_name)
    {
        switch ($property_name) {
            case 'users':
                $this->lazy_loaded_users = null;
                break;

            case 'groups':
                $this->lazy_loaded_groups = null;
                break;

            case 'suppliers':
                $this->lazy_loaded_suppliers = null;
                break;

            default:
                // Log error and keep running
                // TODO 10.1: throw exception instead
                trigger_error("Unknown field: '$property_name'", E_USER_WARNING);
                break;
        }
    }

    /**
     * Return the number of actors currently assigned to the object
     *
     * @since 10.0
     *
     * @return int
     */
    public function countActors(): int
    {
        return $this->countGroups() + $this->countUsers() + $this->countSuppliers();
    }


    /**
     * Return the list of actors for a given actor type
     * We try to retrieve them by:
     * - in case new ticket
     *  - from virtual _actor field (present after a reload)
     *  - from template (predefined actor field)
     *  - from default actor if setting is defined in user preference
     * - for existing ticket (with an id > 0), directly from saved actors
     *
     * @since 10.0
     *
     * @param int $actortype 1=requester, 2=assign, 3=observer
     * @param array $params posted data of itil object
     *
     * @return array of actors
     */
    public function getActorsForType(int $actortype = 1, array $params = []): array
    {
        $actors = [];

        $fn_add_actor = static function (string $itemtype, int $items_id, array $params) use (&$actors) {
            $already_added = !empty(array_filter($actors, static function ($actor) use ($itemtype, $items_id, $params) {
                if ($actor['itemtype'] === $itemtype && (int) $actor['items_id'] === 0) {
                    // Anonymous actors unique based on email
                    return ($actor['alternative_email'] ?? null) === ($params['alternative_email'] ?? null);
                }
                return $actor['itemtype'] === $itemtype && (int) $actor['items_id'] === $items_id;
            }));
            if (!$already_added) {
                $actors[] = [
                    'itemtype' => $itemtype,
                    'items_id' => $items_id,
                ] + $params;
            }
        };

        $actortypestring = self::getActorFieldNameType($actortype);

        if ($this->isNewItem()) {
            $entities_id = $params['entities_id'] ?? $_SESSION['glpiactive_entity'];
            $default_use_notif = Entity::getUsedConfig('is_notif_enable_default', $entities_id, '', 1);

            // load default user from preference only at the first load of new ticket form
            // we don't want to trigger it on form reload
            // at first load, the key _skip_default_actor is not present (can only be present after a submit)
            if (!isset($params['_skip_default_actor'])) {
                // $params['_users_id_' . $actortypestring] corresponds to value defined by static::getDefaultValues()
                // fallback to static::getDefaultActor() value if empty
                $users_id_default = array_key_exists('_users_id_' . $actortypestring, $params) && $params['_users_id_' . $actortypestring] > 0
                    ? $params['_users_id_' . $actortypestring]
                    : $this->getDefaultActor($actortype);
                if ($users_id_default > 0) {
                    $userobj  = new User();
                    if ($userobj->getFromDB($users_id_default)) {
                        $name = formatUserName(
                            $userobj->fields["id"],
                            $userobj->fields["name"],
                            $userobj->fields["realname"],
                            $userobj->fields["firstname"]
                        );
                        $email = UserEmail::getDefaultForUser($users_id_default);
                        $fn_add_actor('User', $users_id_default, [
                            'text'              => $name,
                            'title'             => $name,
                            'use_notification'  => $email === '' ? false : $default_use_notif,
                            'default_email'     => $email,
                            'alternative_email' => '',
                        ]);
                    }
                }

                $groups_id = array_key_exists('_groups_id_' . $actortypestring, $params) && $params['_groups_id_' . $actortypestring] > 0
                    ? $params['_groups_id_' . $actortypestring] : 0;
                if ($groups_id > 0) {
                    $group_obj = new Group();
                    if ($group_obj->getFromDB($groups_id)) {
                        $fn_add_actor('Group', $groups_id, [
                            'text'  => $group_obj->getName(),
                            'title' => $group_obj->getRawCompleteName(),
                        ]);
                    }
                }
            }

            // load default actors from itiltemplate passed from showForm in `params` var
            // we find this key on the first load of template (when opening form)
            // or when the template change (by category loading)
            if (isset($params['_template_changed'])) {
                $users_id = (int) ($params['_predefined_fields']['_users_id_' . $actortypestring] ?? 0);
                if ($users_id > 0) {
                    $userobj  = new User();
                    if ($userobj->getFromDB($users_id)) {
                        $name = formatUserName(
                            $userobj->fields["id"],
                            $userobj->fields["name"],
                            $userobj->fields["realname"],
                            $userobj->fields["firstname"]
                        );
                        $email = UserEmail::getDefaultForUser($users_id);
                        $fn_add_actor('User', $users_id, [
                            'text'              => $name,
                            'title'             => $name,
                            'use_notification'  => $email === '' ? false : $default_use_notif,
                            'default_email'     => $email,
                            'alternative_email' => '',
                        ]);
                    }
                }

                $groups_id = (int) ($params['_predefined_fields']['_groups_id_' . $actortypestring] ?? 0);
                if ($groups_id > 0) {
                    $group_obj = new Group();
                    if ($group_obj->getFromDB($groups_id)) {
                        $fn_add_actor('Group', $groups_id, [
                            'text'  => CommonTreeDropdown::sanitizeSeparatorInCompletename($group_obj->getName()),
                            'title' => CommonTreeDropdown::sanitizeSeparatorInCompletename($group_obj->getRawCompleteName()),
                        ]);
                    }
                }

                $suppliers_id = (int) ($params['_predefined_fields']['_suppliers_id_' . $actortypestring] ?? 0);
                if ($suppliers_id > 0) {
                    $supplier_obj = new Supplier();
                    if ($supplier_obj->getFromDB($suppliers_id)) {
                        $fn_add_actor('Supplier', $suppliers_id, [
                            'text'              => $supplier_obj->fields['name'],
                            'title'             => $supplier_obj->fields['name'],
                            'use_notification'  => $supplier_obj->fields['email'] === '' ? false : $default_use_notif,
                            'default_email'     => $supplier_obj->fields['email'],
                            'alternative_email' => '',
                        ]);
                    }
                }
            }

            // if we load any actor from _itemtype_actortype_id, we are loading template,
            // and so we don't want more actors.
            // if any actor exists and was absent in a field from template, it will be loaded by the POST data.
            // we choose to erase existing actors for any defined in the template.
            if (count($actors)) {
                return $actors;
            }

            // existing actors (from a form reload)
            if (isset($params['_actors'])) {
                foreach ($params['_actors'] as $existing_actortype => $existing_actors) {
                    if ($existing_actortype != $actortypestring) {
                        continue;
                    }
                    foreach ($existing_actors as &$existing_actor) {
                        $actor_obj = new $existing_actor['itemtype']();
                        if ($actor_obj->getFromDB($existing_actor['items_id'])) {
                            if ($actor_obj instanceof User) {
                                $name = formatUserName(
                                    $actor_obj->fields["id"],
                                    $actor_obj->fields["name"],
                                    $actor_obj->fields["realname"],
                                    $actor_obj->fields["firstname"]
                                );
                                $fn_add_actor($existing_actor['itemtype'], $existing_actor['items_id'], $existing_actor + [
                                    'text'          => $name,
                                    'title'         => $name,
                                    'default_email' => UserEmail::getDefaultForUser($actor_obj->fields["id"]),
                                ]);
                            } elseif ($actor_obj instanceof Supplier) {
                                $fn_add_actor($existing_actor['itemtype'], $existing_actor['items_id'], $existing_actor + [
                                    'text'          => $actor_obj->fields['name'],
                                    'title'         => $actor_obj->fields['name'],
                                    'default_email' => $actor_obj->fields['email'],
                                ]);
                            } elseif ($actor_obj instanceof CommonTreeDropdown) {
                                // Group
                                $fn_add_actor($existing_actor['itemtype'], $existing_actor['items_id'], $existing_actor + [
                                    'text'  => CommonTreeDropdown::sanitizeSeparatorInCompletename($actor_obj->getName()),
                                    'title' => CommonTreeDropdown::sanitizeSeparatorInCompletename($actor_obj->getRawCompleteName()),
                                ]);
                            } else {
                                $fn_add_actor($existing_actor['itemtype'], $existing_actor['items_id'], $existing_actor + [
                                    'text'  => $actor_obj->getName(),
                                    'title' => $actor_obj->getRawCompleteName(),
                                ]);
                            }
                        } elseif (
                            $actor_obj instanceof User
                            && $existing_actor['items_id'] == 0
                            && strlen($existing_actor['alternative_email']) > 0
                        ) {
                            // direct mail actor
                            $fn_add_actor($existing_actor['itemtype'], $existing_actor['items_id'], $existing_actor + [
                                'text'  => $existing_actor['alternative_email'],
                                'title' => $existing_actor['alternative_email'],
                            ]);
                        }
                    }
                }
                return $actors;
            }
        }

        // load existing actors (from existing itilobject)
        if (isset($this->users[$actortype])) {
            foreach ($this->users[$actortype] as $user) {
                $name = getUserName(
                    $user['users_id'],
                    0,
                    in_array($actortype, [CommonITILActor::REQUESTER, CommonITILActor::OBSERVER])
                );
                $fn_add_actor('User', $user['users_id'], [
                    'id'                => $user['id'],
                    'text'              => $name,
                    'title'             => $name,
                    'use_notification'  => $user['use_notification'],
                    'default_email'     => UserEmail::getDefaultForUser($user['users_id']),
                    'alternative_email' => $user['alternative_email'],
                ]);
            }
        }
        if (isset($this->groups[$actortype])) {
            foreach ($this->groups[$actortype] as $group) {
                $group_obj = new Group();
                if ($group_obj->getFromDB($group['groups_id'])) {
                    $fn_add_actor('Group', $group['groups_id'], [
                        'id'       => $group['id'],
                        'text'     => CommonTreeDropdown::sanitizeSeparatorInCompletename($group_obj->getName()),
                        'title'    => CommonTreeDropdown::sanitizeSeparatorInCompletename($group_obj->getRawCompleteName()),
                    ]);
                }
            }
        }
        if (isset($this->suppliers[$actortype])) {
            foreach ($this->suppliers[$actortype] as $supplier) {
                $supplier_obj = new Supplier();
                if ($supplier_obj->getFromDB($supplier['suppliers_id'])) {
                    $fn_add_actor('Supplier', $supplier['suppliers_id'], [
                        'id'                => $supplier['id'],
                        'text'              => $supplier_obj->fields['name'],
                        'title'             => $supplier_obj->fields['name'],
                        'use_notification'  => $supplier['use_notification'],
                        'default_email'     => $supplier_obj->fields['email'],
                        'alternative_email' => $supplier['alternative_email'],
                    ]);
                }
            }
        }

        return $actors;
    }

    /**
     * Restores input, restores saved values, then sets the default options for any that are missing.
     * @param integer $ID The item ID
     * @param array $options ITIL Object options array passed to showFormXXXX functions. This is passed by reference and will be modified by this function.
     * @param ?array $overriden_defaults If specified, these values will be used as the defaults instead of the ones from the {@link getDefaultValues()} function.
     * @param bool $force_set_defaults If true, the defaults are set for missing options even if the item is not new.
     * @return void
     * @see getDefaultOptions()
     * @see restoreInput()
     * @see restoreSavedValues()
     */
    protected function restoreInputAndDefaults($ID, array &$options, ?array $overriden_defaults = null, bool $force_set_defaults = false): void
    {
        $default_values = $overriden_defaults ?? static::getDefaultValues();

        // Restore saved value or override with page parameter
        $options['_saved'] = $this->restoreInput();

        // Restore saved values and override $this->fields
        $this->restoreSavedValues($options['_saved']);

        // Set default options
        if ($force_set_defaults || static::isNewID($ID)) {
            foreach ($default_values as $key => $val) {
                if (!isset($options[$key])) {
                    if (isset($options['_saved'][$key])) {
                        $options[$key] = $options['_saved'][$key];
                    } else {
                        $options[$key] = $val;
                    }
                }
            }
        }
    }

    /**
     * @param $ID
     * @param $options   array
     **/
    public function showForm($ID, array $options = [])
    {
        if (!static::canView()) {
            return false;
        }

        $this->restoreInputAndDefaults($ID, $options);

        $canupdate = !$ID || (Session::getCurrentInterface() == "central" && $this->canUpdateItem());

        if ($ID && in_array($this->fields['status'], $this->getClosedStatusArray())) {
            $canupdate = false;
            // No update for actors
            $options['_noupdate'] = true;
        }

        if (!$this->isNewItem()) {
            $options['formtitle'] = sprintf(
                __('%1$s - ID %2$d'),
                $this->getTypeName(1),
                $ID
            );
            //set ID as already defined
            $options['noid'] = true;
        }

        $type = null;
        if (is_a($this, Ticket::class)) {
            $type = ($ID ? $this->fields['type'] : $options['type']);
        }
        // Load template if available
        $tt = $this->getITILTemplateToUse(
            $options['template_preview'] ?? 0,
            $type,
            ($ID ? $this->fields['itilcategories_id'] : $options['itilcategories_id']),
            ($ID ? $this->fields['entities_id'] : $options['entities_id'])
        );

        $predefined_fields = $this->setPredefinedFields($tt, $options, static::getDefaultValues());
        $this->initForm($this->fields['id'], $options);

        TemplateRenderer::getInstance()->display('components/itilobject/layout.html.twig', [
            'item'                    => $this,
            'timeline_itemtypes'      => $this->getTimelineItemtypes(),
            'legacy_timeline_actions' => $this->getLegacyTimelineActionsHTML(),
            'params'                  => $options,
            'entities_id'             => $ID ? $this->fields['entities_id'] : $options['entities_id'],
            'timeline'                => $this->getTimelineItems(),
            'itiltemplate_key'        => static::getTemplateFormFieldName(),
            'itiltemplate'            => $tt,
            'predefined_fields'       => Toolbox::prepareArrayForInput($predefined_fields),
            'canupdate'               => $canupdate,
            'canpriority'             => $canupdate,
            'canassign'               => $canupdate,
            'has_pending_reason'      => PendingReason_Item::getForItem($this) !== false,
            'load_kb_sol'             => $options['load_kb_sol'] ?? 0,
        ]);

        return true;
    }

    /**
     * Return an array of predefined fields from provided template
     * Append also data to $options param (passed by reference) :
     *  - if we transform a ticket (form change and problem) or a problem (for change) override with its field
     *  - override form fields from template (ex: if content field is set in template, content field in option will be overriden)
     *  - if template changed (provided template doesn't match the one found in options), append a key _template_changed in $options
     *  - finally, append templates_id in options
     *
     * @param ITILTemplate $tt The ticket template to use
     * @param array $options The current options array (PASSED BY REFERENCE)
     * @param array $default_values The default values to use in case they are not predefined
     * @return array An array of the predefined values
     */
    protected function setPredefinedFields(ITILTemplate $tt, array &$options, array $default_values): array
    {
        // Predefined fields from template : reset them
        if (isset($options['_predefined_fields'])) {
            $options['_predefined_fields'] = Toolbox::decodeArrayFromInput($options['_predefined_fields']);
        } else {
            $options['_predefined_fields'] = [];
        }
        if (!isset($options['_hidden_fields'])) {
            $options['_hidden_fields'] = [];
        }

        // check original ticket for change and problem
        $tickets_id = $options['tickets_id'] ?? $options['_tickets_id'] ?? null;
        $ticket = new Ticket();
        $ticket->getEmpty();
        if (in_array($this->getType(), ['Change', 'Problem']) && $tickets_id) {
            $ticket->getFromDB($tickets_id);

            // copy fields from original ticket, only when fields are not already set by the user (contained in _saved array)
            $fields = [
                'content',
                'name',
                'impact',
                'urgency',
                'priority',
                'time_to_resolve',
                'entities_id',
            ];
            foreach ($fields as $field) {
                if (!isset($options['_saved'][$field])) {
                    $options[$field] = $ticket->fields[$field];
                }
            }

            if (!isset($options['_saved']['itilcategories_id'])) {
                //page is reloaded on category change, we only want category on the very first load
                $category = new ITILCategory();
                $options['itilcategories_id'] = 0;
                if (
                    $category->getFromDB($ticket->fields['itilcategories_id'])
                    && (
                        ($this->getType() === Change::class && $category->fields['is_change'])
                        || ($this->getType() === Problem::class && $category->fields['is_problem'])
                    )
                ) {
                    $options['itilcategories_id'] = $ticket->fields['itilcategories_id'];
                }
            }
        }

        // check original problem for change
        $problems_id = $options['problems_id'] ?? $options['_problems_id'] ?? null;
        $problem = new Problem();
        $problem->getEmpty();
        if ($this->getType() == "Change" && $problems_id) {
            $problem->getFromDB($problems_id);

            $options['content']             = $problem->fields['content'];
            $options['name']                = $problem->fields['name'];
            $options['impact']              = $problem->fields['impact'];
            $options['urgency']             = $problem->fields['urgency'];
            $options['priority']            = $problem->fields['priority'];
            if (isset($options['problems_id'])) {
                //page is reloaded on category change, we only want category on the very first load
                $options['itilcategories_id'] = $problem->fields['itilcategories_id'];
            }
            $options['time_to_resolve']     = $problem->fields['time_to_resolve'];
            $options['entities_id']         = $problem->fields['entities_id'];
        }

        // Store predefined fields to be able not to take into account on change template
        $predefined_fields = [];
        $tpl_key = static::getTemplateFormFieldName();

        if ($this->isNewItem()) {
            if (isset($tt->predefined) && count($tt->predefined)) {
                foreach ($tt->predefined as $predeffield => $predefvalue) {
                    if (isset($options[$predeffield]) && isset($default_values[$predeffield])) {
                        // Is always default value : not set
                        // Set if already predefined field
                        // Set if ticket template change
                        if (
                            ((count($options['_predefined_fields']) == 0)
                                && ($options[$predeffield] == $default_values[$predeffield]))
                            || (isset($options['_predefined_fields'][$predeffield])
                                && ($options[$predeffield] == $options['_predefined_fields'][$predeffield]))
                            || (isset($options[$tpl_key])
                                && ($options[$tpl_key] != $tt->getID()))

                            // user pref for requestype can't overwrite requestype from template
                            // when change category
                            || ($predeffield == 'requesttypes_id'
                                && empty(($options['_saved'] ?? [])))

                            // tests specificic for change & problem
                            || ($tickets_id != null
                                && $options[$predeffield] == $ticket->fields[$predeffield])
                            || ($problems_id != null
                                && $options[$predeffield] == $problem->fields[$predeffield])
                        ) {
                            $options[$predeffield]           = $predefvalue;
                            $predefined_fields[$predeffield] = $predefvalue;
                            $this->fields[$predeffield]      = $predefvalue;
                        }
                    } else { // Not defined options set as hidden field
                        $options['_hidden_fields'][$predeffield] = $predefvalue;
                    }
                }
                // All predefined override : add option to say predifined exists
                if (count($predefined_fields) == 0) {
                    $predefined_fields['_all_predefined_override'] = 1;
                }
            } else { // No template load : reset predefined values
                if (count($options['_predefined_fields'])) {
                    foreach ($options['_predefined_fields'] as $predeffield => $predefvalue) {
                        if ($options[$predeffield] == $predefvalue) {
                            $options[$predeffield] = $default_values[$predeffield];
                        }
                    }
                }
            }
        }
        // append to options to know later we added predefined values
        // we may need this especially for actors
        if (count($predefined_fields)) {
            $options['_predefined_fields'] = $predefined_fields;
        }

        // check if we load the default template (when openning form for example) or the template changed
        if (!isset($options[$tpl_key]) || $options[$tpl_key] != $tt->getId()) {
            $options['_template_changed'] = true;
        }

        // Put ticket template id on $options for actors
        $options[str_replace('s_id', '', $tpl_key)] = $tt->getId();

        // Add all values to fields of tickets for template preview
        if (($options['template_preview'] ?? false)) {
            foreach ($options as $key => $val) {
                if (!isset($this->fields[$key])) {
                    $this->fields[$key] = $val;
                }
            }
        }

        // Recompute priority if not predefined and impact/urgency was changed
        if (
            !isset($predefined_fields['priority'])
            && (
                isset($predefined_fields['urgency'])
                || isset($predefined_fields['impact'])
            )
        ) {
            $this->fields['priority'] = self::computePriority(
                $this->fields['urgency'] ?? 3,
                $this->fields['impact'] ?? 3
            );
        }

        return $predefined_fields;
    }


    /**
     * Retrieve all possible entities for an itilobject posted data.
     * We try to retrieve requesters in the data:
     * - from `_users_id_requester` (data from template or default actor)
     * - from `_actors` (virtual field when the form is reloaded)
     * By default, if none of these fields are present, entities are get from current active entity.
     *
     * @since 10.0
     *
     * @param array $params posted data by an itil object
     * @return array of possible entities_id
     */
    public function getEntitiesForRequesters(array $params = [])
    {
        $requesters = [];
        if (array_key_exists('_users_id_requester', $params) && !empty($params["_users_id_requester"])) {
            $requesters = !is_array($params["_users_id_requester"])
                ? [$params["_users_id_requester"]]
                : $params["_users_id_requester"];
        }
        if (isset($params['_actors']['requester'])) {
            foreach ($params['_actors']['requester'] as $actor) {
                if (
                    $actor['itemtype'] == "User"
                    && (int) $actor['items_id'] > 0 // ignore actor that is added by only its email
                ) {
                    $requesters[] = $actor['items_id'];
                }
            }
        }

        $entities = $_SESSION['glpiactiveentities'] ?? [];
        foreach ($requesters as $users_id) {
            $user_entities = Profile_User::getUserEntities($users_id, true, true);
            $entities = array_intersect($user_entities, $entities);
        }

        $entities = array_values($entities); // Ensure keys are starting at 0

        return $entities;
    }


    /**
     * Retrieve an item from the database with datas associated (hardwares)
     *
     * @param integer $ID ID of the item to get
     *
     * @return boolean true if succeed else false
     **/
    public function getFromDBwithData($ID)
    {

        if ($this->getFromDB($ID)) {
            $this->getAdditionalDatas();
            return true;
        }
        return false;
    }


    public function getAdditionalDatas() {}


    /**
     * Can manage actors
     *
     * @return boolean
     */
    public function canAdminActors()
    {
        if (isset($this->fields['is_deleted']) && $this->fields['is_deleted'] == 1) {
            return false;
        }
        return Session::haveRight(static::$rightname, UPDATE);
    }


    /**
     * Can assign object
     *
     * @return boolean
     */
    public function canAssign()
    {
        if ($this->isDeleted() || (!$this->isNewItem() && $this->isClosed())) {
            return false;
        }

        return Session::haveRight(static::$rightname, UPDATE);
    }


    /**
     * Can be assigned to me
     *
     * @return boolean
     */
    public function canAssignToMe()
    {
        if (
            isset($this->fields['is_deleted']) && $this->fields['is_deleted'] == 1
            || isset($this->fields['status']) && in_array($this->fields['status'], $this->getClosedStatusArray())
        ) {
            return false;
        }
        return Session::haveRight(static::$rightname, UPDATE);
    }


    /**
     * Is the current user have right to approve solution of the current ITIL object.
     *
     * @since 9.4.0
     *
     * @return boolean
     */
    public function canApprove()
    {

        return (($this->fields["users_id_recipient"] === Session::getLoginUserID())
              || $this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID())
              || (isset($_SESSION["glpigroups"])
                  && $this->haveAGroup(CommonITILActor::REQUESTER, $_SESSION["glpigroups"])));
    }

    /**
     * Is the current user have right to add followups to the current ITIL Object ?
     *
     * @since 9.4.0
     *
     * @return boolean
     */
    public function canAddFollowups()
    {
        return (
            (
                Session::haveRight("followup", ITILFollowup::ADDMYTICKET)
            && (
                $this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID())
               || (
                   isset($this->fields["users_id_recipient"])
                  && ($this->fields["users_id_recipient"] == Session::getLoginUserID())
               )
            )
            )
            || (
                Session::haveRight("followup", ITILFollowup::ADD_AS_OBSERVER)
            && $this->isUser(CommonITILActor::OBSERVER, Session::getLoginUserID())
            )
            || Session::haveRight('followup', ITILFollowup::ADDALLTICKET)
            || (
                Session::haveRight('followup', ITILFollowup::ADDGROUPTICKET)
            && isset($_SESSION["glpigroups"])
            && (
                (
                    $this->haveAGroup(CommonITILActor::REQUESTER, $_SESSION['glpigroups'])
                    && Session::haveRight("followup", ITILFollowup::ADDMYTICKET)
                )
                || (
                    $this->haveAGroup(CommonITILActor::OBSERVER, $_SESSION['glpigroups'])
                    && Session::haveRight("followup", ITILFollowup::ADD_AS_OBSERVER)
                )
            )
            )
            || $this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID())
            || (
                isset($_SESSION["glpigroups"])
            && $this->haveAGroup(CommonITILActor::ASSIGN, $_SESSION['glpigroups'])
            )
            || $this->isValidator(Session::getLoginUserID())
        );
    }


    public function canMassiveAction($action, $field, $value)
    {

        switch ($action) {
            case 'update':
                switch ($field) {
                    case 'status':
                        if (!static::isAllowedStatus($this->fields['status'], $value)) {
                            return false;
                        }
                        break;
                }
                break;
        }
        return true;
    }


    /**
     * Do the current ItilObject need to be reopened by a requester answer
     *
     * @since 10.0.1
     *
     * @return boolean
     */
    public function needReopen(): bool
    {
        $my_id    = Session::getLoginUserID();
        $my_groups = $_SESSION["glpigroups"] ?? [];

        // Compute requester groups
        $requester_groups = array_filter(
            $my_groups,
            fn($group) => $this->isGroup(CommonITILActor::REQUESTER, $group)
        );

        // Compute assigned groups
        $assigned_groups = array_filter(
            $my_groups,
            fn($group) => $this->isGroup(CommonITILActor::ASSIGN, $group)
        );

        $ami_requester       = $this->isUser(CommonITILActor::REQUESTER, $my_id);
        $ami_requester_group = count($requester_groups) > 0;

        $ami_assignee        = $this->isUser(CommonITILActor::ASSIGN, $my_id);
        $ami_assignee_group  = count($assigned_groups) > 0;

        return in_array($this->fields["status"], static::getReopenableStatusArray())
            && ($ami_requester || $ami_requester_group)
            && !($ami_assignee || $ami_assignee_group);
    }

    /**
     * Check if the given users is a validator
     * @param int $users_id
     * @return bool
     */
    public function isValidator($users_id): bool
    {
        if (!$users_id) {
            // Invalid parameter
            return false;
        }

        if (!$this instanceof Ticket && !$this instanceof Change) {
            // Not a valid validation target
            return false;
        }

        $validation_class = static::class . "Validation";
        $valitation_obj = new $validation_class();
        $validation_requests = $valitation_obj->find([
            getForeignKeyFieldForItemType(static::class) => $this->getID(),
            'users_id_validate' => $users_id,
        ]);

        return count($validation_requests) > 0;
    }


    /**
     * Does current user have right to solve the current item?
     *
     * @return boolean
     **/
    public function canSolve()
    {

        return ((Session::haveRight(static::$rightname, UPDATE)
               || $this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID())
               || (isset($_SESSION["glpigroups"])
                   && $this->haveAGroup(CommonITILActor::ASSIGN, $_SESSION["glpigroups"])))
              && static::isAllowedStatus($this->fields['status'], self::SOLVED)
              // No edition on closed status
              && !in_array($this->fields['status'], $this->getClosedStatusArray()));
    }

    /**
     * Does current user have right to solve the current item; if it was not closed?
     *
     * @return boolean
     **/
    public function maySolve()
    {

        return ((Session::haveRight(static::$rightname, UPDATE)
               || $this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID())
               || (isset($_SESSION["glpigroups"])
                   && $this->haveAGroup(CommonITILActor::ASSIGN, $_SESSION["glpigroups"])))
              && static::isAllowedStatus($this->fields['status'], self::SOLVED));
    }


    /**
     * Get the ITIL object closed, solved or waiting status list
     *
     * @since 9.4.0
     *
     * @return array
     */
    public static function getReopenableStatusArray()
    {
        return [self::CLOSED, self::SOLVED, self::WAITING, self::OBSERVED];
    }


    /**
     * Is a user linked to the object ?
     *
     * @param integer $type     type to search (see constants)
     * @param integer $users_id user ID
     *
     * @return boolean
     **/
    public function isUser($type, $users_id)
    {

        if (isset($this->users[$type])) {
            foreach ($this->users[$type] as $data) {
                if ($data['users_id'] == $users_id) {
                    return true;
                }
            }
        }

        return false;
    }


    /**
     * Is a group linked to the object ?
     *
     * @param int $type      type to search (see constants)
     * @param int $groups_id group ID
     *
     * @return bool
     **/
    // FIXME add params typehint in GLPI 10.1
    public function isGroup($type, $groups_id): bool
    {
        if (isset($this->groups[$type])) {
            foreach ($this->groups[$type] as $data) {
                if ($data['groups_id'] == $groups_id) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * Is a supplier linked to the object ?
     *
     * @since 0.84
     *
     * @param integer $type         type to search (see constants)
     * @param integer $suppliers_id supplier ID
     *
     * @return boolean
     **/
    public function isSupplier($type, $suppliers_id)
    {

        if (isset($this->suppliers[$type])) {
            foreach ($this->suppliers[$type] as $data) {
                if ($data['suppliers_id'] == $suppliers_id) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * get users linked to a object
     *
     * @param integer $type type to search (see constants)
     *
     * @return array
     **/
    public function getUsers($type)
    {

        if (isset($this->users[$type])) {
            return $this->users[$type];
        }

        return [];
    }


    /**
     * get groups linked to a object
     *
     * @param integer $type type to search (see constants)
     *
     * @return array
     **/
    public function getGroups($type)
    {

        if (isset($this->groups[$type])) {
            return $this->groups[$type];
        }

        return [];
    }


    /**
     * get users linked to an object including groups ones
     *
     * @since 0.85
     *
     * @param integer $type type to search (see constants)
     *
     * @return array
     **/
    public function getAllUsers($type)
    {

        $users = [];
        foreach ($this->getUsers($type) as $link) {
            $users[$link['users_id']] = $link['users_id'];
        }

        foreach ($this->getGroups($type) as $link) {
            $gusers = Group_User::getGroupUsers($link['groups_id']);
            foreach ($gusers as $user) {
                $users[$user['id']] = $user['id'];
            }
        }

        return $users;
    }


    /**
     * get suppliers linked to a object
     *
     * @since 0.84
     *
     * @param integer $type type to search (see constants)
     *
     * @return array
     **/
    public function getSuppliers($type)
    {

        if (isset($this->suppliers[$type])) {
            return $this->suppliers[$type];
        }

        return [];
    }


    /**
     * count users linked to object by type or global
     *
     * @param integer $type type to search (see constants) / 0 for all (default 0)
     *
     * @return integer
     **/
    public function countUsers($type = 0)
    {

        if ($type > 0) {
            if (isset($this->users[$type])) {
                return count($this->users[$type]);
            }
        } else {
            if (count($this->users)) {
                $count = 0;
                foreach ($this->users as $u) {
                    $count += count($u);
                }
                return $count;
            }
        }
        return 0;
    }


    /**
     * count groups linked to object by type or global
     *
     * @param integer $type type to search (see constants) / 0 for all (default 0)
     *
     * @return integer
     **/
    public function countGroups($type = 0)
    {

        if ($type > 0) {
            if (isset($this->groups[$type])) {
                return count($this->groups[$type]);
            }
        } else {
            if (count($this->groups)) {
                $count = 0;
                foreach ($this->groups as $u) {
                    $count += count($u);
                }
                return $count;
            }
        }
        return 0;
    }


    /**
     * count suppliers linked to object by type or global
     *
     * @since 0.84
     *
     * @param integer $type type to search (see constants) / 0 for all (default 0)
     *
     * @return integer
     **/
    public function countSuppliers($type = 0)
    {

        if ($type > 0) {
            if (isset($this->suppliers[$type])) {
                return count($this->suppliers[$type]);
            }
        } else {
            if (count($this->suppliers)) {
                $count = 0;
                foreach ($this->suppliers as $u) {
                    $count += count($u);
                }
                return $count;
            }
        }
        return 0;
    }


    /**
     * Is one of groups linked to the object ?
     *
     * @param integer $type   type to search (see constants)
     * @param array   $groups groups IDs
     *
     * @return boolean
     **/
    public function haveAGroup($type, array $groups)
    {

        if (
            is_array($groups) && count($groups)
            && isset($this->groups[$type])
        ) {
            foreach ($groups as $groups_id) {
                foreach ($this->groups[$type] as $data) {
                    if ($data['groups_id'] == $groups_id) {
                        return true;
                    }
                }
            }
        }
        return false;
    }


    /**
     * Get Default actor when creating the object
     *
     * @param integer $type type to search (see constants)
     *
     * @return boolean
     **/
    public function getDefaultActor($type)
    {

        /// TODO own_ticket -> own_itilobject
        if ($type == CommonITILActor::ASSIGN) {
            if (Session::haveRight("ticket", Ticket::OWN)) {
                return Session::getLoginUserID();
            }
        }
        return 0;
    }


    /**
     * Get Default actor when creating the object
     *
     * @param integer $type type to search (see constants)
     *
     * @return boolean
     **/
    public function getDefaultActorRightSearch($type)
    {

        if ($type == CommonITILActor::ASSIGN) {
            return "own_ticket";
        }
        return "all";
    }


    /**
     * Count active ITIL Objects
     *
     * @since 9.3.1
     *
     * @param CommonITILActor $linkclass Link class instance
     * @param integer         $id        Item ID
     * @param integer         $role      ITIL role
     *
     * @return integer
     **/
    private function countActiveObjectsFor(CommonITILActor $linkclass, $id, $role)
    {

        $itemtable = $this->getTable();
        $itemfk    = $this->getForeignKeyField();
        $linktable = $linkclass->getTable();
        $field     = $linkclass::$items_id_2;

        return countElementsInTable(
            [$itemtable, $linktable],
            [
                "$linktable.$itemfk"    => new \QueryExpression(DBmysql::quoteName("$itemtable.id")),
                "$linktable.$field"     => $id,
                "$linktable.type"       => $role,
                "$itemtable.is_deleted" => 0,
                "NOT"                   => [
                    "$itemtable.status" => array_merge(
                        $this->getSolvedStatusArray(),
                        $this->getClosedStatusArray()
                    ),
                ],
            ] + getEntitiesRestrictCriteria($itemtable)
        );
    }




    /**
     * Count active ITIL Objects requested by a user
     *
     * @since 0.83
     *
     * @param integer $users_id ID of the User
     *
     * @return integer
     **/
    public function countActiveObjectsForUser($users_id)
    {
        $linkclass = new $this->userlinkclass();
        return $this->countActiveObjectsFor(
            $linkclass,
            $users_id,
            CommonITILActor::REQUESTER
        );
    }


    /**
     * Count active ITIL Objects having given user as observer.
     *
     * @param int $user_id
     *
     * @return int
     */
    final public function countActiveObjectsForObserverUser(int $user_id): int
    {
        $linkclass = new $this->userlinkclass();
        return $this->countActiveObjectsFor(
            $linkclass,
            $user_id,
            CommonITILActor::OBSERVER
        );
    }


    /**
     * Count active ITIL Objects assigned to a user
     *
     * @since 0.83
     *
     * @param integer $users_id ID of the User
     *
     * @return integer
     **/
    public function countActiveObjectsForTech($users_id)
    {
        $linkclass = new $this->userlinkclass();
        return $this->countActiveObjectsFor(
            $linkclass,
            $users_id,
            CommonITILActor::ASSIGN
        );
    }


    /**
     * Count active ITIL Objects having given group as requester.
     *
     * @param int $group_id
     *
     * @return int
     */
    final public function countActiveObjectsForRequesterGroup(int $group_id): int
    {
        $linkclass = new $this->grouplinkclass();
        return $this->countActiveObjectsFor(
            $linkclass,
            $group_id,
            CommonITILActor::REQUESTER
        );
    }


    /**
     * Count active ITIL Objects having given group as observer.
     *
     * @param int $group_id
     *
     * @return int
     */
    final public function countActiveObjectsForObserverGroup(int $group_id): int
    {
        $linkclass = new $this->grouplinkclass();
        return $this->countActiveObjectsFor(
            $linkclass,
            $group_id,
            CommonITILActor::OBSERVER
        );
    }


    /**
     * Count active ITIL Objects assigned to a group
     *
     * @since 0.84
     *
     * @param integer $groups_id ID of the User
     *
     * @return integer
     **/
    public function countActiveObjectsForTechGroup($groups_id)
    {
        $linkclass = new $this->grouplinkclass();
        return $this->countActiveObjectsFor(
            $linkclass,
            $groups_id,
            CommonITILActor::ASSIGN
        );
    }


    /**
     * Count active ITIL Objects assigned to a supplier
     *
     * @since 0.85
     *
     * @param integer $suppliers_id ID of the Supplier
     *
     * @return integer
     **/
    public function countActiveObjectsForSupplier($suppliers_id)
    {
        $linkclass = new $this->supplierlinkclass();
        return $this->countActiveObjectsFor(
            $linkclass,
            $suppliers_id,
            CommonITILActor::ASSIGN
        );
    }


    public function cleanDBonPurge()
    {

        $link_classes = [
            Itil_Project::class,
            ITILFollowup::class,
            ITILSolution::class,
        ];

        if (is_a($this->grouplinkclass, CommonDBConnexity::class, true)) {
            $link_classes[] = $this->grouplinkclass;
        }

        if (is_a($this->userlinkclass, CommonDBConnexity::class, true)) {
            $link_classes[] = $this->userlinkclass;
        }

        if (is_a($this->supplierlinkclass, CommonDBConnexity::class, true)) {
            $link_classes[] = $this->supplierlinkclass;
        }

        $this->deleteChildrenAndRelationsFromDb($link_classes);
    }

    /**
     * Handle template mandatory fields on update
     *
     * @param array $input Input
     *
     * @return false|array
     */
    protected function handleTemplateFields(array $input)
    {
        //// check mandatory fields
        // First get ticket template associated : entity and type/category
        if (isset($input['entities_id'])) {
            $entid = $input['entities_id'];
        } else {
            $entid = $this->fields['entities_id'];
        }

        $type = null;
        if (isset($input['type'])) {
            $type = $input['type'];
        } elseif (isset($this->fields['type'])) {
            $type = $this->fields['type'];
        }

        if (isset($input['itilcategories_id'])) {
            $categid = $input['itilcategories_id'];
        } else {
            $categid = $this->fields['itilcategories_id'];
        }

        $check_allowed_fields_for_template = false;
        $allowed_fields                    = [];
        if (
            !Session::isCron()
            && (!Session::haveRight(static::$rightname, UPDATE)
            // Closed tickets
            || in_array($this->fields['status'], $this->getClosedStatusArray()))
        ) {
            $allowed_fields                    = ['id'];
            $check_allowed_fields_for_template = true;

            if (in_array($this->fields['status'], $this->getClosedStatusArray())) {
                $allowed_fields[] = 'status';

                // probably transfer
                $allowed_fields[] = 'entities_id';
                $allowed_fields[] = 'itilcategories_id';
                $allowed_fields[] = 'locations_id';
            } else {
                if (
                    $this->canApprove()
                    || $this->canAssign()
                    || $this->canAssignToMe()
                    || isset($input['_from_assignment'])
                ) {
                    $allowed_fields[] = 'status';
                    $allowed_fields[] = '_accepted';
                }
                $validation_class = static::getType() . 'Validation';
                if (
                    class_exists($validation_class)
                    && (
                        // for validation created by rules
                        // FIXME Use a more precise input name to ensure that 'global_validation' has been defined by rules
                        // e.g. $input['_validation_from_rule']
                        isset($input["_rule_process"])
                        // for validation status updated after CommonITILValidation add/update/delete
                        || (array_key_exists('_from_itilvalidation', $input) && $input['_from_itilvalidation'])
                    )
                ) {
                    $allowed_fields[] = 'global_validation';
                }
                // Manage assign and steal right
                if (static::getType() === Ticket::getType() && Session::haveRightsOr(static::$rightname, [Ticket::ASSIGN, Ticket::STEAL])) {
                    $allowed_fields[] = '_itil_assign';
                    $allowed_fields[] = '_users_id_assign';
                    $allowed_fields[] = '_groups_id_assign';
                    $allowed_fields[] = '_suppliers_id_assign';
                }

                // Can only update initial fields if no followup or task already added
                if ($this->canUpdateItem()) {
                    $allowed_fields[] = 'content';
                    $allowed_fields[] = 'urgency';
                    $allowed_fields[] = 'priority'; // automatic recalculate if user changes urgence
                    $allowed_fields[] = 'itilcategories_id';
                    $allowed_fields[] = 'name';
                    $allowed_fields[] = 'items_id';
                    $allowed_fields[] = '_filename';
                    $allowed_fields[] = '_tag_filename';
                    $allowed_fields[] = '_prefix_filename';
                    $allowed_fields[] = '_content';
                    $allowed_fields[] = '_tag_content';
                    $allowed_fields[] = '_prefix_content';
                    $allowed_fields[] = 'takeintoaccount_delay_stat';
                    $allowed_fields[] = 'takeintoaccountdate';
                }
            }

            $ret = [];

            foreach ($allowed_fields as $field) {
                if (isset($input[$field])) {
                    $ret[$field] = $input[$field];
                }
            }

            $input = $ret;

            // Only ID return false
            if (count($input) == 1) {
                return false;
            }
        }

        $tt = $this->getITILTemplateToUse(0, $type, $categid, $entid);

        if (count($tt->mandatory)) {
            $mandatory_missing = [];
            $fieldsname        = $tt->getAllowedFieldsNames(true);
            foreach ($tt->mandatory as $key => $val) {
                if (
                    (!$check_allowed_fields_for_template || in_array($key, $allowed_fields))
                    && (
                        isset($input[$key])
                    && (empty($input[$key]) || ($input[$key] == 'NULL'))
                    )
                ) {
                    $mandatory_missing[$key] = $fieldsname[$val];
                }
            }
            if (count($mandatory_missing)) {
                //TRANS: %s are the fields concerned
                $message = sprintf(
                    __('Mandatory fields are not filled. Please correct: %s'),
                    implode(", ", $mandatory_missing)
                );
                Session::addMessageAfterRedirect($message, false, ERROR);
                return false;
            }
        }

        return $input;
    }

    public function prepareInputForUpdate($input)
    {

        if (!$this->checkFieldsConsistency($input)) {
            return false;
        }

        // Add document if needed
        $this->getFromDB($input["id"]); // entities_id field required

        if ($this->getType() !== Ticket::getType()) {
            //cannot be handled here for tickets. @see Ticket::prepareInputForUpdate()
            $input = $this->handleTemplateFields($input);
            if ($input === false) {
                return false;
            }
        }

        if (isset($input["document"]) && ($input["document"] > 0)) {
            $doc = new Document();
            if ($doc->getFromDB($input["document"])) {
                $docitem = new Document_Item();
                if (
                    $docitem->add(['documents_id' => $input["document"],
                        'itemtype'     => $this->getType(),
                        'items_id'     => $input["id"],
                    ])
                ) {
                    // Force date_mod of tracking
                    $input["date_mod"]     = $_SESSION["glpi_currenttime"];
                    $input['_doc_added'][] = $doc->fields["name"];
                }
            }
            unset($input["document"]);
        }

        if (isset($input["date"]) && empty($input["date"])) {
            unset($input["date"]);
        }

        if (isset($input["closedate"]) && empty($input["closedate"])) {
            unset($input["closedate"]);
        }

        if (isset($input["solvedate"]) && empty($input["solvedate"])) {
            unset($input["solvedate"]);
        }

        // "do not compute" flag set by business rules for "takeintoaccount_delay_stat" field
        $do_not_compute_takeintoaccount = $this->isTakeIntoAccountComputationBlocked($input);

        if (isset($input['_itil_requester'])) {
            // FIXME Deprecate this input key in GLPI 10.1.
            if (isset($input['_itil_requester']['_type'])) {
                $input['_itil_requester'] = [
                    'type'                            => CommonITILActor::REQUESTER,
                    $this->getForeignKeyField()       => $input['id'],
                    '_do_not_compute_takeintoaccount' => $do_not_compute_takeintoaccount,
                    '_from_object'                    => true,
                ] + $input['_itil_requester'];

                switch ($input['_itil_requester']['_type']) {
                    case "user":
                        if (
                            isset($input['_itil_requester']['use_notification'])
                            && is_array($input['_itil_requester']['use_notification'])
                        ) {
                            $input['_itil_requester']['use_notification'] = $input['_itil_requester']['use_notification'][0];
                        }
                        if (
                            isset($input['_itil_requester']['alternative_email'])
                            && is_array($input['_itil_requester']['alternative_email'])
                        ) {
                            $input['_itil_requester']['alternative_email'] = $input['_itil_requester']['alternative_email'][0];
                        }

                        if (!empty($this->userlinkclass)) {
                            if (
                                isset($input['_itil_requester']['alternative_email'])
                                && $input['_itil_requester']['alternative_email']
                                && !NotificationMailing::isUserAddressValid($input['_itil_requester']['alternative_email'])
                            ) {
                                $input['_itil_requester']['alternative_email'] = '';
                                Session::addMessageAfterRedirect(__('Invalid email address'), false, ERROR);
                            }

                            if (
                                (isset($input['_itil_requester']['alternative_email'])
                                && $input['_itil_requester']['alternative_email'])
                                || ($input['_itil_requester']['users_id'] > 0)
                            ) {
                                $useractors = new $this->userlinkclass();
                                if (
                                    isset($input['_auto_update'])
                                    || $useractors->can(-1, CREATE, $input['_itil_requester'])
                                ) {
                                    $useractors->add($input['_itil_requester']);
                                    $input['_forcenotif']                     = true;
                                }
                            }
                        }
                        break;

                    case "group":
                        if (
                            !empty($this->grouplinkclass)
                            && ($input['_itil_requester']['groups_id'] > 0)
                        ) {
                            $groupactors = new $this->grouplinkclass();
                            if (
                                isset($input['_auto_update'])
                                || $groupactors->can(-1, CREATE, $input['_itil_requester'])
                            ) {
                                $groupactors->add($input['_itil_requester']);
                                $input['_forcenotif']                     = true;
                            }
                        }
                        break;
                }
            }
        }

        if (isset($input['_itil_observer'])) {
            // FIXME Deprecate this input key in GLPI 10.1.
            if (isset($input['_itil_observer']['_type'])) {
                $input['_itil_observer'] = [
                    'type'                            => CommonITILActor::OBSERVER,
                    $this->getForeignKeyField()       => $input['id'],
                    '_do_not_compute_takeintoaccount' => $do_not_compute_takeintoaccount,
                    '_from_object'                    => true,
                ] + $input['_itil_observer'];

                switch ($input['_itil_observer']['_type']) {
                    case "user":
                        if (
                            isset($input['_itil_observer']['use_notification'])
                            && is_array($input['_itil_observer']['use_notification'])
                        ) {
                            $input['_itil_observer']['use_notification'] = $input['_itil_observer']['use_notification'][0];
                        }
                        if (
                            isset($input['_itil_observer']['alternative_email'])
                            && is_array($input['_itil_observer']['alternative_email'])
                        ) {
                            $input['_itil_observer']['alternative_email'] = $input['_itil_observer']['alternative_email'][0];
                        }

                        if (!empty($this->userlinkclass)) {
                            if (
                                isset($input['_itil_observer']['alternative_email'])
                                && $input['_itil_observer']['alternative_email']
                                && !NotificationMailing::isUserAddressValid($input['_itil_observer']['alternative_email'])
                            ) {
                                $input['_itil_observer']['alternative_email'] = '';
                                Session::addMessageAfterRedirect(__('Invalid email address'), false, ERROR);
                            }
                            if (
                                (isset($input['_itil_observer']['alternative_email'])
                                && $input['_itil_observer']['alternative_email'])
                                || ($input['_itil_observer']['users_id'] > 0)
                            ) {
                                $useractors = new $this->userlinkclass();
                                if (
                                    isset($input['_auto_update'])
                                    || $useractors->can(-1, CREATE, $input['_itil_observer'])
                                ) {
                                    $useractors->add($input['_itil_observer']);
                                    $input['_forcenotif']                    = true;
                                }
                            }
                        }
                        break;

                    case "group":
                        if (
                            !empty($this->grouplinkclass)
                            && ($input['_itil_observer']['groups_id'] > 0)
                        ) {
                            $groupactors = new $this->grouplinkclass();
                            if (
                                isset($input['_auto_update'])
                                || $groupactors->can(-1, CREATE, $input['_itil_observer'])
                            ) {
                                $groupactors->add($input['_itil_observer']);
                                $input['_forcenotif']                    = true;
                            }
                        }
                        break;
                }
            }
        }

        if (isset($input['_itil_assign'])) {
            // FIXME Deprecate this input key in GLPI 10.1.
            if (isset($input['_itil_assign']['_type'])) {
                $input['_itil_assign'] = [
                    'type'                            => CommonITILActor::ASSIGN,
                    $this->getForeignKeyField()       => $input['id'],
                    '_do_not_compute_takeintoaccount' => $do_not_compute_takeintoaccount,
                    '_from_object'                    => true,
                ] + $input['_itil_assign'];

                if (
                    isset($input['_itil_assign']['use_notification'])
                    && is_array($input['_itil_assign']['use_notification'])
                ) {
                    $input['_itil_assign']['use_notification'] = $input['_itil_assign']['use_notification'][0];
                }
                if (
                    isset($input['_itil_assign']['alternative_email'])
                    && is_array($input['_itil_assign']['alternative_email'])
                ) {
                    $input['_itil_assign']['alternative_email'] = $input['_itil_assign']['alternative_email'][0];
                }

                switch ($input['_itil_assign']['_type']) {
                    case "user":
                        if (
                            !empty($this->userlinkclass)
                            && ((isset($input['_itil_assign']['alternative_email'])
                            && $input['_itil_assign']['alternative_email'])
                            || $input['_itil_assign']['users_id'] > 0)
                        ) {
                            $useractors = new $this->userlinkclass();
                            if (
                                isset($input['_auto_update'])
                                || $useractors->can(-1, CREATE, $input['_itil_assign'])
                            ) {
                                $useractors->add($input['_itil_assign']);
                                $input['_forcenotif']                  = true;
                                if (
                                    ((!isset($input['status'])
                                    && in_array($this->fields['status'], $this->getNewStatusArray()))
                                    || (isset($input['status'])
                                    && in_array($input['status'], $this->getNewStatusArray())))
                                    && !$this->isStatusComputationBlocked($input)
                                ) {
                                    if (in_array(self::ASSIGNED, array_keys($this->getAllStatusArray()))) {
                                        $input['status'] = self::ASSIGNED;
                                    }
                                }
                            }
                        }
                        break;

                    case "group":
                        if (
                            !empty($this->grouplinkclass)
                            && ($input['_itil_assign']['groups_id'] > 0)
                        ) {
                            $groupactors = new $this->grouplinkclass();

                            if (
                                isset($input['_auto_update'])
                                || $groupactors->can(-1, CREATE, $input['_itil_assign'])
                            ) {
                                $groupactors->add($input['_itil_assign']);
                                $input['_forcenotif']                  = true;
                                if (
                                    ((!isset($input['status'])
                                    && (in_array($this->fields['status'], $this->getNewStatusArray())))
                                    || (isset($input['status'])
                                    && (in_array($input['status'], $this->getNewStatusArray()))))
                                    && !$this->isStatusComputationBlocked($input)
                                ) {
                                    if (in_array(self::ASSIGNED, array_keys($this->getAllStatusArray()))) {
                                        $input['status'] = self::ASSIGNED;
                                    }
                                }
                            }
                        }
                        break;

                    case "supplier":
                        if (
                            !empty($this->supplierlinkclass)
                            && ((isset($input['_itil_assign']['alternative_email'])
                            && $input['_itil_assign']['alternative_email'])
                            || $input['_itil_assign']['suppliers_id'] > 0)
                        ) {
                            $supplieractors = new $this->supplierlinkclass();
                            if (
                                isset($input['_auto_update'])
                                || $supplieractors->can(-1, CREATE, $input['_itil_assign'])
                            ) {
                                $supplieractors->add($input['_itil_assign']);
                                $input['_forcenotif']                  = true;
                                if (
                                    ((!isset($input['status'])
                                    && (in_array($this->fields['status'], $this->getNewStatusArray())))
                                    || (isset($input['status'])
                                    && (in_array($input['status'], $this->getNewStatusArray()))))
                                    && !$this->isStatusComputationBlocked($input)
                                ) {
                                    if (in_array(self::ASSIGNED, array_keys($this->getAllStatusArray()))) {
                                        $input['status'] = self::ASSIGNED;
                                    }
                                }
                            }
                        }
                        break;
                }
            }
        }

        // set last updater if interactive user
        if (!Session::isCron()) {
            $input['users_id_lastupdater'] = Session::getLoginUserID();
        }

        $solvedclosed = array_merge(
            $this->getSolvedStatusArray(),
            $this->getClosedStatusArray()
        );

        if (
            isset($input["status"])
            && !in_array($input["status"], $solvedclosed)
        ) {
            $input['solvedate'] = 'NULL';
        }

        if (isset($input["status"]) && !in_array($input["status"], $this->getClosedStatusArray())) {
            $input['closedate'] = 'NULL';
        }

        // Setting a solution type means the ticket is solved
        if (
            isset($input["solutiontypes_id"])
            && (!isset($input['status']) || !in_array($input["status"], $solvedclosed))
        ) {
            $solution = new ITILSolution();
            $soltype = new SolutionType();
            $soltype->getFromDB($input['solutiontypes_id']);
            $solution->add([
                'itemtype'           => $this->getType(),
                'items_id'           => $this->getID(),
                'solutiontypes_id'   => $input['solutiontypes_id'],
                'content'            => 'Solved using type ' . $soltype->getName(),
            ]);
        }

        // If status changed from pending to anything else, remove pending reason
        if (
            isset($this->input["status"])
            && $this->input["status"] != self::WAITING
        ) {
            PendingReason_Item::deleteForItem($this);
        }

        return $input;
    }

    public function post_updateItem($history = true)
    {
        // Handle rich-text images and uploaded documents
        $this->input = $this->addFiles($this->input, ['force_update' => true]);

        // handle actors changes
        $this->updateActors();

        // Handle "_tasktemplates_id" special input
        $this->handleTaskTemplateInput();

        // Handle "_itilfollowuptemplates_id" special input
        $this->handleITILFollowupTemplateInput();

        // Handle "_solutiontemplates_id" special input
        $this->handleSolutionTemplateInput();

        // Send validation requests
        $this->manageValidationAdd($this->input);

        parent::post_updateItem();
    }


    public function pre_updateInDB()
    {
        /** @var \DBmysql $DB */
        global $DB;

        // get again object to reload actors
        $this->loadActors();

        // Check dates change interval due to the fact that second are not displayed in form
        if (
            (($key = array_search('date', $this->updates)) !== false)
            && (substr($this->fields["date"], 0, 16) === substr($this->oldvalues['date'], 0, 16))
        ) {
            unset($this->updates[$key]);
            unset($this->oldvalues['date']);
        }

        if (
            (($key = array_search('closedate', $this->updates)) !== false)
            && isset($this->oldvalues['closedate'])
            && (substr($this->fields["closedate"], 0, 16) === substr($this->oldvalues['closedate'], 0, 16))
        ) {
            unset($this->updates[$key]);
            unset($this->oldvalues['closedate']);
        }

        if (
            (($key = array_search('time_to_resolve', $this->updates)) !== false)
            && isset($this->oldvalues['time_to_resolve'])
            && (substr($this->fields["time_to_resolve"], 0, 16) === substr($this->oldvalues['time_to_resolve'], 0, 16))
        ) {
            unset($this->updates[$key]);
            unset($this->oldvalues['time_to_resolve']);
        }

        if (
            (($key = array_search('solvedate', $this->updates)) !== false)
            && isset($this->oldvalues['solvedate'])
            && (substr($this->fields["solvedate"], 0, 16) === substr($this->oldvalues['solvedate'], 0, 16))
        ) {
            unset($this->updates[$key]);
            unset($this->oldvalues['solvedate']);
        }

        if (isset($this->input["status"])) {
            if (
                in_array("status", $this->updates)
                && in_array($this->input["status"], $this->getSolvedStatusArray())
            ) {
                $this->updates[]              = "solvedate";
                $this->oldvalues['solvedate'] = $this->fields["solvedate"];
                $this->fields["solvedate"]    = $_SESSION["glpi_currenttime"];
                // If invalid date : set open date
                if ($this->fields["solvedate"] < $this->fields["date"]) {
                    $this->fields["solvedate"] = $this->fields["date"];
                }
            }

            if (
                in_array("status", $this->updates)
                && in_array($this->input["status"], $this->getClosedStatusArray())
            ) {
                $this->updates[]              = "closedate";
                $this->oldvalues['closedate'] = $this->fields["closedate"];
                $this->fields["closedate"]    = $_SESSION["glpi_currenttime"];
                // If invalid date : set open date
                if ($this->fields["closedate"] < $this->fields["date"]) {
                    $this->fields["closedate"] = $this->fields["date"];
                }
                // Set solvedate to closedate
                if (empty($this->fields["solvedate"])) {
                    $this->updates[]              = "solvedate";
                    $this->oldvalues['solvedate'] = $this->fields["solvedate"];
                    $this->fields["solvedate"]    = $this->fields["closedate"];
                }
            }
        }

        // check dates

        // check time_to_resolve (SLA)
        if (
            (in_array("date", $this->updates) || in_array("time_to_resolve", $this->updates))
            && !is_null($this->fields["time_to_resolve"])
        ) { // Date set
            if ($this->fields["time_to_resolve"] < $this->fields["date"]) {
                Session::addMessageAfterRedirect(__('Invalid dates. Update cancelled.'), false, ERROR);

                if (($key = array_search('date', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['date']);
                }
                if (($key = array_search('time_to_resolve', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['time_to_resolve']);
                }
            }
        }

        // check internal_time_to_resolve (OLA)
        if (
            (in_array("date", $this->updates) || in_array("internal_time_to_resolve", $this->updates))
            && !is_null($this->fields["internal_time_to_resolve"])
        ) { // Date set
            if ($this->fields["internal_time_to_resolve"] < $this->fields["date"]) {
                Session::addMessageAfterRedirect(__('Invalid dates. Update cancelled.'), false, ERROR);

                if (($key = array_search('date', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['date']);
                }
                if (($key = array_search('internal_time_to_resolve', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['internal_time_to_resolve']);
                }
            }
        }

        // Status close : check dates
        if (
            in_array($this->fields["status"], $this->getClosedStatusArray())
            && (in_array("date", $this->updates) || in_array("closedate", $this->updates))
        ) {
            // Invalid dates : no change
            // closedate must be > solvedate
            if ($this->fields["closedate"] < $this->fields["solvedate"]) {
                Session::addMessageAfterRedirect(__('Invalid dates. Update cancelled.'), false, ERROR);

                if (($key = array_search('closedate', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['closedate']);
                }
            }

            // closedate must be > create date
            if ($this->fields["closedate"] < $this->fields["date"]) {
                Session::addMessageAfterRedirect(__('Invalid dates. Update cancelled.'), false, ERROR);
                if (($key = array_search('date', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['date']);
                }
                if (($key = array_search('closedate', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['closedate']);
                }
            }
        }

        if (
            (($key = array_search('status', $this->updates)) !== false)
            && $this->oldvalues['status'] == $this->fields['status']
        ) {
            unset($this->updates[$key]);
            unset($this->oldvalues['status']);
        }

        // Status solved : check dates
        if (
            in_array($this->fields["status"], $this->getSolvedStatusArray())
            && (in_array("date", $this->updates) || in_array("solvedate", $this->updates))
        ) {
            // Invalid dates : no change
            // solvedate must be > create date
            if ($this->fields["solvedate"] < $this->fields["date"]) {
                Session::addMessageAfterRedirect(__('Invalid dates. Update cancelled.'), false, ERROR);

                if (($key = array_search('date', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['date']);
                }
                if (($key = array_search('solvedate', $this->updates)) !== false) {
                    unset($this->updates[$key]);
                    unset($this->oldvalues['solvedate']);
                }
            }
        }

        // Manage come back to waiting state
        if (
            !is_null($this->fields['begin_waiting_date'])
            && ($key = array_search('status', $this->updates)) !== false
            && (
                $this->oldvalues['status'] == self::WAITING
            // From solved to another state than closed
            || (
                in_array($this->oldvalues["status"], $this->getSolvedStatusArray())
               && !in_array($this->fields["status"], $this->getClosedStatusArray())
            )
            // From closed to any open state
            || (
                in_array($this->oldvalues["status"], $this->getClosedStatusArray())
               && in_array($this->fields["status"], $this->getNotSolvedStatusArray())
            )
            )
        ) {
            // Compute ticket waiting time use calendar if exists
            $calendar     = new Calendar();
            $calendars_id = $this->getCalendar();
            $delay_time   = 0;

            // Compute ticket waiting time use calendar if exists
            // Using calendar
            if (
                ($calendars_id > 0)
                && $calendar->getFromDB($calendars_id)
            ) {
                $delay_time = $calendar->getActiveTimeBetween(
                    $this->fields['begin_waiting_date'],
                    $_SESSION["glpi_currenttime"]
                );
            } else { // Not calendar defined
                $delay_time = strtotime($_SESSION["glpi_currenttime"])
                           - strtotime($this->fields['begin_waiting_date']);
            }

            // SLA case : compute sla_ttr duration
            if (isset($this->fields['slas_id_ttr']) && ($this->fields['slas_id_ttr'] > 0)) {
                $sla = new SLA();
                if ($sla->getFromDB($this->fields['slas_id_ttr'])) {
                    $sla->setTicketCalendar($calendars_id);
                    $delay_time_sla  = $sla->getActiveTimeBetween(
                        $this->fields['begin_waiting_date'],
                        $_SESSION["glpi_currenttime"]
                    );
                    $this->updates[] = "sla_waiting_duration";
                    $this->fields["sla_waiting_duration"] += $delay_time_sla;
                }

                // Compute new time_to_resolve
                $this->updates[]                 = "time_to_resolve";
                $this->fields['time_to_resolve'] = $sla->computeDate(
                    $this->fields['date'],
                    $this->fields["sla_waiting_duration"]
                );
                // Add current level to do
                $sla->addLevelToDo($this);
            } else {
                // Using calendar
                if (
                    ($calendars_id > 0)
                    && $calendar->getFromDB($calendars_id)
                    && $calendar->hasAWorkingDay()
                ) {
                    if ((int) $this->fields['time_to_resolve'] > 0) {
                        // compute new due date using calendar
                        $this->updates[]                 = "time_to_resolve";
                        $this->fields['time_to_resolve'] = $calendar->computeEndDate(
                            $this->fields['time_to_resolve'],
                            $delay_time
                        );
                    }
                } else { // Not calendar defined
                    if ((int) $this->fields['time_to_resolve'] > 0) {
                        // compute new due date : no calendar so add computed delay_time
                        $this->updates[]                 = "time_to_resolve";
                        $this->fields['time_to_resolve'] = date(
                            'Y-m-d H:i:s',
                            $delay_time + strtotime($this->fields['time_to_resolve'])
                        );
                    }
                }
            }

            // OLA case : compute ola_ttr duration
            if (isset($this->fields['olas_id_ttr']) && ($this->fields['olas_id_ttr'] > 0)) {
                $ola = new OLA();
                if ($ola->getFromDB($this->fields['olas_id_ttr'])) {
                    $ola->setTicketCalendar($calendars_id);
                    $delay_time_ola  = $ola->getActiveTimeBetween(
                        $this->fields['begin_waiting_date'],
                        $_SESSION["glpi_currenttime"]
                    );
                    $this->updates[]                      = "ola_waiting_duration";
                    $this->fields["ola_waiting_duration"] += $delay_time_ola;
                }

                // Compute new internal_time_to_resolve
                $this->updates[]                          = "internal_time_to_resolve";
                $this->fields['internal_time_to_resolve'] = $ola->computeDate(
                    $this->fields['ola_ttr_begin_date'],
                    $this->fields["ola_waiting_duration"]
                );
                // Add current level to do
                $ola->addLevelToDo($this, $this->fields["olalevels_id_ttr"]);
            } elseif (array_key_exists("internal_time_to_resolve", $this->fields)) {
                // Change doesn't have internal_time_to_resolve
                // Using calendar
                if (
                    ($calendars_id > 0)
                    && $calendar->getFromDB($calendars_id)
                    && $calendar->hasAWorkingDay()
                ) {
                    if ((int) $this->fields['internal_time_to_resolve'] > 0) {
                        // compute new internal_time_to_resolve using calendar
                        $this->updates[]                          = "internal_time_to_resolve";
                        $this->fields['internal_time_to_resolve'] = $calendar->computeEndDate(
                            $this->fields['internal_time_to_resolve'],
                            $delay_time
                        );
                    }
                } else { // Not calendar defined
                    if ((int) $this->fields['internal_time_to_resolve'] > 0) {
                        // compute new internal_time_to_resolve : no calendar so add computed delay_time
                        $this->updates[]                          = "internal_time_to_resolve";
                        $this->fields['internal_time_to_resolve'] = date(
                            'Y-m-d H:i:s',
                            $delay_time +
                            strtotime($this->fields['internal_time_to_resolve'])
                        );
                    }
                }
            }

            $this->updates[]                   = "waiting_duration";
            $this->fields["waiting_duration"] += $delay_time;

            // Reset begin_waiting_date
            $this->updates[]                    = "begin_waiting_date";
            $this->fields["begin_waiting_date"] = 'NULL';
        }

        // Set begin waiting date if needed
        if (
            (($key = array_search('status', $this->updates)) !== false)
            && (($this->fields['status'] == self::WAITING)
              || in_array($this->fields["status"], $this->getSolvedStatusArray()))
        ) {
            $this->updates[]                    = "begin_waiting_date";
            $this->fields["begin_waiting_date"] = $_SESSION["glpi_currenttime"];

            // Specific for tickets
            if (isset($this->fields['slas_id_ttr']) && ($this->fields['slas_id_ttr'] > 0)) {
                SLA::deleteLevelsToDo($this);
            }

            if (isset($this->fields['olas_id_ttr']) && ($this->fields['olas_id_ttr'] > 0)) {
                OLA::deleteLevelsToDo($this);
            }
        }

        // solve_delay_stat : use delay between opendate and solvedate
        if (in_array("solvedate", $this->updates)) {
            $this->updates[]                  = "solve_delay_stat";
            $this->fields['solve_delay_stat'] = $this->computeSolveDelayStat();
        }
        // close_delay_stat : use delay between opendate and closedate
        if (in_array("closedate", $this->updates)) {
            $this->updates[]                  = "close_delay_stat";
            $this->fields['close_delay_stat'] = $this->computeCloseDelayStat();
        }

        // Update of the global validation status if the validation percentage has changed
        if (in_array("validation_percent", $this->updates)) {
            $this->updates[] = 'global_validation';
            $this->fields['global_validation'] = $this->getValidationClassInstance()->computeValidationStatus($this);
        }

        //Look for reopening
        $statuses = array_merge(
            $this->getSolvedStatusArray(),
            $this->getClosedStatusArray()
        );
        if (
            ($key = array_search('status', $this->updates)) !== false
            && in_array($this->oldvalues['status'], $statuses)
            && !in_array($this->fields['status'], $statuses)
        ) {
            $users_id_reject = 0;
            // set last updater if interactive user
            if (!Session::isCron()) {
                $users_id_reject = Session::getLoginUserID();
            }

            //Mark existing solutions as refused
            $DB->update(
                ITILSolution::getTable(),
                [
                    'status'             => CommonITILValidation::REFUSED,
                    'users_id_approval'  => $users_id_reject,
                    'date_approval'      => date('Y-m-d H:i:s'),
                ],
                [
                    'WHERE'  => [
                        'itemtype'  => static::getType(),
                        'items_id'  => $this->getID(),
                    ],
                    'ORDER'  => [
                        'date_creation DESC',
                        'id DESC',
                    ],
                    'LIMIT'  => 1,
                ]
            );

            //Delete existing survey
            $inquest = new TicketSatisfaction();
            $inquest->delete(['tickets_id' => $this->getID()]);
        }

        if (isset($this->input['_accepted'])) {
            //Mark last solution as approved
            $DB->update(
                ITILSolution::getTable(),
                [
                    'status'             => CommonITILValidation::ACCEPTED,
                    'users_id_approval'  => Session::getLoginUserID(),
                    'date_approval'      => date('Y-m-d H:i:s'),
                ],
                [
                    'WHERE'  => [
                        'itemtype'  => static::getType(),
                        'items_id'  => $this->getID(),
                    ],
                    'ORDER'  => [
                        'date_creation DESC',
                        'id DESC',
                    ],
                    'LIMIT'  => 1,
                ]
            );
        }

        // Do not take into account date_mod if no update is done
        if (
            (count($this->updates) == 1)
            && (($key = array_search('date_mod', $this->updates)) !== false)
        ) {
            unset($this->updates[$key]);
        }
    }


    public function prepareInputForAdd($input)
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (!$this->checkFieldsConsistency($input)) {
            return false;
        }

        $input = $this->transformActorsInput($input);

        // save value before clean;
        $title = ltrim($input['name']);

        // Set default status to avoid notice
        if (!isset($input["status"])) {
            $input["status"] = self::INCOMING;
        }

        if (
            !isset($input["urgency"])
            || !($CFG_GLPI['urgency_mask'] & (1 << $input["urgency"]))
        ) {
            $input["urgency"] = 3;
        }
        if (
            !isset($input["impact"])
            || !($CFG_GLPI['impact_mask'] & (1 << $input["impact"]))
        ) {
            $input["impact"] = 3;
        }

        $canpriority = true;
        if ($this->getType() == 'Ticket') {
            $canpriority = Session::haveRight(Ticket::$rightname, Ticket::CHANGEPRIORITY);
        }

        if ($canpriority && !isset($input["priority"]) || !$canpriority) {
            $input["priority"] = $this->computePriority($input["urgency"], $input["impact"]);
        }

        // set last updater if interactive user
        if (!Session::isCron() && ($last_updater = Session::getLoginUserID(true))) {
            $input['users_id_lastupdater'] = $last_updater;
        }

        if (!isset($input['_skip_auto_assign']) || $input['_skip_auto_assign'] === false) {
            // No Auto set Import for external source
            if (
                ($uid = Session::getLoginUserID())
                && !isset($input['_auto_import'])
            ) {
                $input["users_id_recipient"] = $uid;
            } elseif (
                isset($input["_users_id_requester"])
                && !is_array($input['_users_id_requester'])
                && !empty($input["_users_id_requester"])
                && !isset($input["users_id_recipient"])
            ) {
                $input["users_id_recipient"] = $input["_users_id_requester"];
            }
        }

        // No name set name
        $input["name"]    = ltrim($input["name"]);
        $input['content'] = ltrim($input['content']);
        if (empty($input["name"])) {
            // Build name based on content

            // Unsanitize
            $content = Sanitizer::unsanitize($input['content']);

            // Get unformatted text
            $name = RichText::getTextFromHtml($content, false);

            // Shorten result
            $name = Toolbox::substr(preg_replace('/\s{2,}/', ' ', $name), 0, 70);

            // Sanitize result
            $input['name'] = Sanitizer::sanitize($name);
        }

        // Set default dropdown
        $dropdown_fields = ['entities_id', 'itilcategories_id'];
        foreach ($dropdown_fields as $field) {
            if (!isset($input[$field])) {
                $input[$field] = 0;
            }
        }

        $input = $this->computeDefaultValuesForAdd($input);

        // Do not check mandatory on auto import (mailgates)
        $key = $this->getTemplateFormFieldName();
        if (!isset($input['_auto_import'])) {
            if (isset($input[$key]) && $input[$key]) {
                $tt_class = $this->getType() . 'Template';
                $tt = new $tt_class();
                if ($tt->getFromDBWithData($input[$key])) {
                    if (count($tt->mandatory)) {
                        $mandatory_missing = [];
                        $fieldsname        = $tt->getAllowedFieldsNames(true);
                        foreach ($tt->mandatory as $key => $val) {
                            // for title if mandatory (restore initial value)
                            if ($key == 'name') {
                                $input['name']                     = $title;
                            }
                            // Check only defined values : Not defined not in form
                            if (isset($input[$key])) {
                                // If content is also predefined need to be different from predefined value
                                if (
                                    ($key == 'content')
                                    && isset($tt->predefined['content'])
                                ) {
                                    // Decode then re-encode predefine content to be sure to have it encoded using the same
                                    // entities.
                                    // Without this, predefined content created prior to GLPI 10.0 will never match
                                    // sanitized input, as entities used for encoding will be different in both.
                                    $predefined_content = Sanitizer::encodeHtmlSpecialChars(
                                        Sanitizer::decodeHtmlSpecialChars($tt->predefined['content'])
                                    );
                                    // Clean new lines to be fix encoding
                                    if (
                                        strcmp(
                                            preg_replace(
                                                "/\r?\n/",
                                                "",
                                                Html::cleanPostForTextArea($input[$key])
                                            ),
                                            preg_replace(
                                                "/\r?\n/",
                                                "",
                                                $predefined_content
                                            )
                                        ) == 0
                                    ) {
                                        Session::addMessageAfterRedirect(
                                            __('You cannot use predefined description verbatim'),
                                            false,
                                            ERROR
                                        );
                                        $mandatory_missing[$key] = $fieldsname[$val];
                                    }
                                }

                                if (
                                    empty($input[$key]) || ($input[$key] == 'NULL')
                                    || (is_array($input[$key])
                                    && ($input[$key] === [0 => "0"]))
                                ) {
                                    $mandatory_missing[$key] = $fieldsname[$val];
                                }
                            }

                            if (
                                ($key == '_add_validation')
                                && !empty($input['users_id_validate'])
                                && isset($input['users_id_validate'][0])
                                && ($input['users_id_validate'][0] > 0)
                            ) {
                                unset($mandatory_missing['_add_validation']);
                            }

                            if (static::getType() === Ticket::getType()) {
                                // For time_to_resolve and time_to_own : check also slas
                                // For internal_time_to_resolve and internal_time_to_own : check also olas
                                foreach ([SLM::TTR, SLM::TTO] as $slmType) {
                                    [$dateField, $slaField] = SLA::getFieldNames($slmType);
                                    if (
                                        ($key == $dateField)
                                        && isset($input[$slaField]) && ($input[$slaField] > 0)
                                        && isset($mandatory_missing[$dateField])
                                    ) {
                                        unset($mandatory_missing[$dateField]);
                                    }
                                    [$dateField, $olaField] = OLA::getFieldNames($slmType);
                                    if (
                                        ($key == $dateField)
                                        && isset($input[$olaField]) && ($input[$olaField] > 0)
                                        && isset($mandatory_missing[$dateField])
                                    ) {
                                        unset($mandatory_missing[$dateField]);
                                    }
                                }
                            }

                            // For document mandatory
                            if (
                                ($key == '_documents_id')
                                && !isset($input['_filename'])
                                && !isset($input['_tag_filename'])
                                && !isset($input['_content'])
                                && !isset($input['_tag_content'])
                                && !isset($input['_stock_image'])
                                && !isset($input['_tag_stock_image'])
                            ) {
                                $mandatory_missing[$key] = $fieldsname[$val];
                            }
                        }

                        if (count($mandatory_missing)) {
                            //TRANS: %s are the fields concerned
                            $message = sprintf(
                                __('Mandatory fields are not filled. Please correct: %s'),
                                implode(", ", $mandatory_missing)
                            );
                            Session::addMessageAfterRedirect($message, false, ERROR);
                            return false;
                        }
                    }
                }
            }
        }

        return $input;
    }

    /**
     * Check input fields consistency.
     *
     * @param array $input
     *
     * @return bool
     */
    private function checkFieldsConsistency(array $input): bool
    {
        if (
            array_key_exists('date', $input) && !empty($input['date']) && $input['date'] != 'NULL'
            && (!is_string($input['date']) || !preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $input['date']))
        ) {
            Session::addMessageAfterRedirect(__('Incorrect value for date field.'), false, ERROR);
            return false;
        }

        return true;
    }

    /**
     * Compute default values for Add
     * (to be passed in prepareInputForAdd before and after rules if needed)
     *
     * @since 0.84
     *
     * @param $input
     *
     * @return string
     **/
    public function computeDefaultValuesForAdd($input)
    {

        if (!isset($input["status"])) {
            $input["status"] = self::INCOMING;
        }

        if (!isset($input["date"]) || empty($input["date"]) || $input["date"] == 'NULL') {
            $input["date"] = $_SESSION["glpi_currenttime"];
        }

        if (in_array($input["status"], $this->getSolvedStatusArray())) {
            if (
                !isset($input["solvedate"])
                || $input["solvedate"] < $input["date"]
            ) {
                $input["solvedate"] = $input["date"];
            }
        }

        if (in_array($input["status"], $this->getClosedStatusArray())) {
            if (
                !isset($input["closedate"])
                || $input["closedate"] < $input["date"]
            ) {
                $input["closedate"] = $input["date"];
            }
            if (
                !isset($input["solvedate"])
                || $input["solvedate"] < $input["date"]
                || $input["solvedate"] > $input["closedate"]
            ) {
                $input['solvedate'] = $input["closedate"];
            }
        }

        // Set begin waiting time if status is waiting
        if ($input["status"] == self::WAITING) {
            $input['begin_waiting_date'] = $input['date'];
        }

        return $input;
    }


    public function post_addItem()
    {
        // Handle rich-text images and uploaded documents
        $this->input = $this->addFiles($this->input, ['force_update' => true]);

        // Add default document if set in template
        if (
            isset($this->input['_documents_id'])
            && is_array($this->input['_documents_id'])
            && count($this->input['_documents_id'])
        ) {
            $docitem = new Document_Item();
            foreach ($this->input['_documents_id'] as $docID) {
                $docitem->add(['documents_id' => $docID,
                    '_do_notif'    => false,
                    'itemtype'     => $this->getType(),
                    'items_id'     => $this->fields['id'],
                ]);
            }
        }

        // handle actors changes
        $this->updateActors(true);

        // Handle "_tasktemplates_id" special input
        $this->handleTaskTemplateInput();

        // Handle "_itilfollowuptemplates_id" special input
        $this->handleITILFollowupTemplateInput();

        // Handle "_solutiontemplates_id" special input
        $this->handleSolutionTemplateInput();

        // Send validation requests
        $this->manageValidationAdd($this->input);

        parent::post_addItem();
    }

    /**
     * @see Glpi\Features\Clonable::post_clone
     */
    public function post_clone($source, $history)
    {
        /** @var \DBmysql $DB */
        global $DB;
        $update = [];
        if (isset($source->fields['users_id_lastupdater'])) {
            $update['users_id_lastupdater'] = $source->fields['users_id_lastupdater'];
        }
        if (isset($source->fields['status'])) {
            $update['status'] = $source->fields['status'];
        }
        $DB->update(
            $this->getTable(),
            $update,
            ['id' => $this->getID()]
        );
    }

    public function getCloneRelations(): array
    {
        $relations = [];

        if (is_a($this->userlinkclass, CommonITILActor::class, true)) {
            $relations[] = $this->userlinkclass;
        }
        if (is_a($this->grouplinkclass, CommonITILActor::class, true)) {
            $relations[] = $this->grouplinkclass;
        }
        if (is_a($this->supplierlinkclass, CommonITILActor::class, true)) {
            $relations[] = $this->supplierlinkclass;
        }

        return $relations;
    }


    /**
     * Compute Priority
     *
     * @since 0.84
     *
     * @param $urgency   integer from 1 to 5
     * @param $impact    integer from 1 to 5
     *
     * @return integer from 1 to 5 (priority)
     **/
    public static function computePriority($urgency, $impact)
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (isset($CFG_GLPI[static::MATRIX_FIELD][$urgency][$impact])) {
            return $CFG_GLPI[static::MATRIX_FIELD][$urgency][$impact];
        }
        // Failback to trivial
        return round(($urgency + $impact) / 2);
    }


    /**
     * Dropdown of ITIL object priority
     *
     * @since  version 0.84 new proto
     *
     * @param $options array of options
     *       - name     : select name (default is urgency)
     *       - value    : default value (default 0)
     *       - showtype : list proposed : normal, search (default normal)
     *       - wthmajor : boolean with major priority ?
     *       - display  : boolean if false get string
     *
     * @return string id of the select
     **/
    public static function dropdownPriority(array $options = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $p = [
            'name'      => 'priority',
            'value'     => 0,
            'showtype'  => 'normal',
            'display'   => true,
            'withmajor' => false,
            'enable_filtering' => true,
            'templateResult'    => "templateItilPriority",
            'templateSelection' => "templateItilPriority",
        ];

        if (is_array($options) && count($options)) {
            foreach ($options as $key => $val) {
                $p[$key] = $val;
            }
        }

        $values = [];

        if ($p['showtype'] == 'search') {
            $values[0]  = static::getPriorityName(0);
            $values[-5] = static::getPriorityName(-5);
            $values[-4] = static::getPriorityName(-4);
            $values[-3] = static::getPriorityName(-3);
            $values[-2] = static::getPriorityName(-2);
            $values[-1] = static::getPriorityName(-1);
        }

        if (
            ($p['showtype'] == 'search')
            || $p['withmajor']
        ) {
            $values[6] = static::getPriorityName(6);
        }

        $values[5] = static::getPriorityName(5);
        $values[4] = static::getPriorityName(4);
        $values[3] = static::getPriorityName(3);
        $values[2] = static::getPriorityName(2);
        $values[1] = static::getPriorityName(1);

        if ($p['enable_filtering']) {
            $urgencies = [];
            if (isset($CFG_GLPI[static::URGENCY_MASK_FIELD])) {
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 5)
                ) {
                    $urgencies[] = 5;
                }
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 4)
                ) {
                    $urgencies[] = 4;
                }
                $urgencies[] = 3;
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 2)
                ) {
                    $urgencies[] = 2;
                }
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 1)
                ) {
                    $urgencies[] = 1;
                }
            }
            $impacts = [];
            if (isset($CFG_GLPI[static::IMPACT_MASK_FIELD])) {
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 5)
                ) {
                    $impacts[] = 5;
                }
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 4)
                ) {
                    $impacts[] = 4;
                }
                $impacts[] = 3;
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 2)
                ) {
                    $impacts[] = 2;
                }
                if (
                    ($p['showtype'] == 'search')
                    || $CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 1)
                ) {
                    $impacts[] = 1;
                }
            }

            $active_priorities = [];
            foreach ($urgencies as $urgency) {
                foreach ($impacts as $impact) {
                    if (isset($CFG_GLPI["_matrix_{$urgency}_{$impact}"])) {
                        $active_priorities[] = $CFG_GLPI["_matrix_{$urgency}_{$impact}"];
                    }
                }
            }
            $active_priorities = array_unique($active_priorities);
            if (count($active_priorities) > 0) {
                foreach ($values as $priority => $name) {
                    if (!in_array($priority, $active_priorities)) {
                        if ($p['withmajor'] && $priority == 6) {
                            continue;
                        }

                        // don't unset current value (to avoid selecting major priority on existing item)
                        if ($priority != $p['value']) {
                            unset($values[$priority]);
                        }
                    }
                }
            }
        }

        return Dropdown::showFromArray($p['name'], $values, $p);
    }


    /**
     * Get ITIL object priority Name
     *
     * @param integer $value priority ID
     **/
    public static function getPriorityName($value)
    {

        switch ($value) {
            case 6:
                return _x('priority', 'Major');

            case 5:
                return _x('priority', 'Very high');

            case 4:
                return _x('priority', 'High');

            case 3:
                return _x('priority', 'Medium');

            case 2:
                return _x('priority', 'Low');

            case 1:
                return _x('priority', 'Very low');

                // No standard one :
            case 0:
                return _x('priority', 'All');
            case -1:
                return _x('priority', 'At least very low');
            case -2:
                return _x('priority', 'At least low');
            case -3:
                return _x('priority', 'At least medium');
            case -4:
                return _x('priority', 'At least high');
            case -5:
                return _x('priority', 'At least very high');

            default:
                // Return $value if not define
                return $value;
        }
    }


    /**
     * Dropdown of ITIL object Urgency
     *
     * @since 0.84 new proto
     *
     * @param $options array of options
     *       - name     : select name (default is urgency)
     *       - value    : default value (default 0)
     *       - showtype : list proposed : normal, search (default normal)
     *       - display  : boolean if false get string
     *
     * @return string id of the select
     **/
    public static function dropdownUrgency(array $options = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $p = [
            'name'     => 'urgency',
            'value'    => 0,
            'showtype' => 'normal',
            'display'  => true,
        ];

        if (is_array($options) && count($options)) {
            foreach ($options as $key => $val) {
                $p[$key] = $val;
            }
        }

        $values = [];

        if ($p['showtype'] == 'search') {
            $values[0]  = static::getUrgencyName(0);
            $values[-5] = static::getUrgencyName(-5);
            $values[-4] = static::getUrgencyName(-4);
            $values[-3] = static::getUrgencyName(-3);
            $values[-2] = static::getUrgencyName(-2);
            $values[-1] = static::getUrgencyName(-1);
        }

        if (isset($CFG_GLPI[static::URGENCY_MASK_FIELD])) {
            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 5))
            ) {
                $values[5]  = static::getUrgencyName(5);
            }

            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 4))
            ) {
                $values[4]  = static::getUrgencyName(4);
            }

            $values[3]  = static::getUrgencyName(3);

            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 2))
            ) {
                $values[2]  = static::getUrgencyName(2);
            }

            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::URGENCY_MASK_FIELD] & (1 << 1))
            ) {
                $values[1]  = static::getUrgencyName(1);
            }
        }

        return Dropdown::showFromArray($p['name'], $values, $p);
    }


    /**
     * Get ITIL object Urgency Name
     *
     * @param integer $value urgency ID
     **/
    public static function getUrgencyName($value)
    {

        switch ($value) {
            case 5:
                return _x('urgency', 'Very high');

            case 4:
                return _x('urgency', 'High');

            case 3:
                return _x('urgency', 'Medium');

            case 2:
                return _x('urgency', 'Low');

            case 1:
                return _x('urgency', 'Very low');

                // No standard one :
            case 0:
                return _x('urgency', 'All');
            case -1:
                return _x('urgency', 'At least very low');
            case -2:
                return _x('urgency', 'At least low');
            case -3:
                return _x('urgency', 'At least medium');
            case -4:
                return _x('urgency', 'At least high');
            case -5:
                return _x('urgency', 'At least very high');

            default:
                // Return $value if not define
                return $value;
        }
    }


    /**
     * Dropdown of ITIL object Impact
     *
     * @since 0.84 new proto
     *
     * @param $options   array of options
     *  - name     : select name (default is impact)
     *  - value    : default value (default 0)
     *  - showtype : list proposed : normal, search (default normal)
     *  - display  : boolean if false get string
     *
     * \
     * @return string id of the select
     **/
    public static function dropdownImpact(array $options = [])
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $p = [
            'name'     => 'impact',
            'value'    => 0,
            'showtype' => 'normal',
            'display'  => true,
        ];

        if (is_array($options) && count($options)) {
            foreach ($options as $key => $val) {
                $p[$key] = $val;
            }
        }
        $values = [];

        if ($p['showtype'] == 'search') {
            $values[0]  = static::getImpactName(0);
            $values[-5] = static::getImpactName(-5);
            $values[-4] = static::getImpactName(-4);
            $values[-3] = static::getImpactName(-3);
            $values[-2] = static::getImpactName(-2);
            $values[-1] = static::getImpactName(-1);
        }

        if (isset($CFG_GLPI[static::IMPACT_MASK_FIELD])) {
            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 5))
            ) {
                $values[5]  = static::getImpactName(5);
            }

            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 4))
            ) {
                $values[4]  = static::getImpactName(4);
            }

            $values[3]  = static::getImpactName(3);

            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 2))
            ) {
                $values[2]  = static::getImpactName(2);
            }

            if (
                ($p['showtype'] == 'search')
                || ($CFG_GLPI[static::IMPACT_MASK_FIELD] & (1 << 1))
            ) {
                $values[1]  = static::getImpactName(1);
            }
        }

        return Dropdown::showFromArray($p['name'], $values, $p);
    }


    /**
     * Get ITIL object Impact Name
     *
     * @param integer $value impact ID
     **/
    public static function getImpactName($value)
    {

        switch ($value) {
            case 5:
                return _x('impact', 'Very high');

            case 4:
                return _x('impact', 'High');

            case 3:
                return _x('impact', 'Medium');

            case 2:
                return _x('impact', 'Low');

            case 1:
                return _x('impact', 'Very low');

                // No standard one :
            case 0:
                return _x('impact', 'All');
            case -1:
                return _x('impact', 'At least very low');
            case -2:
                return _x('impact', 'At least low');
            case -3:
                return _x('impact', 'At least medium');
            case -4:
                return _x('impact', 'At least high');
            case -5:
                return _x('impact', 'At least very high');

            default:
                // Return $value if not define
                return $value;
        }
    }


    /**
     * Get the ITIL object status list
     *
     * @param $withmetaforsearch boolean (false by default)
     *
     * @return array
     **/
    public static function getAllStatusArray($withmetaforsearch = false)
    {

        // To be overridden by class
        $tab = [];

        return $tab;
    }


    /**
     * Get the ITIL object closed status list
     *
     * @since 0.83
     *
     * @return array
     **/
    public static function getClosedStatusArray()
    {

        // To be overridden by class
        $tab = [];
        return $tab;
    }


    /**
     * Get the ITIL object solved status list
     *
     * @since 0.83
     *
     * @return array
     **/
    public static function getSolvedStatusArray()
    {

        // To be overridden by class
        $tab = [];
        return $tab;
    }

    /**
     * Get the ITIL object all status list without solved and closed status
     *
     * @since 9.2.1
     *
     * @return array
     **/
    public static function getNotSolvedStatusArray()
    {
        $all = static::getAllStatusArray();
        foreach (static::getSolvedStatusArray() as $status) {
            if (isset($all[$status])) {
                unset($all[$status]);
            }
        }
        foreach (static::getClosedStatusArray() as $status) {
            if (isset($all[$status])) {
                unset($all[$status]);
            }
        }
        $nosolved = array_keys($all);

        return $nosolved;
    }


    /**
     * Get the ITIL object new status list
     *
     * @since 0.83.8
     *
     * @return array
     **/
    public static function getNewStatusArray()
    {

        // To be overriden by class
        $tab = [];
        return $tab;
    }

    public static function getProcessStatusArray()
    {
        // To be overriden by class
        return [];
    }

    /**
     * Get the ITIL object process status list
     *
     * @since 0.83
     *
     * @return array
     **/
    public static function getProcessStatus()
    {

        // To be overridden by class
        $tab = [];
        return $tab;
    }


    /**
     * check is the user can change from / to a status
     *
     * @since 0.84
     *
     * @param integer $old value of old/current status
     * @param integer $new value of target status
     *
     * @return boolean
     **/
    public static function isAllowedStatus($old, $new)
    {

        if (
            isset($_SESSION['glpiactiveprofile'][static::STATUS_MATRIX_FIELD][$old][$new])
            && !$_SESSION['glpiactiveprofile'][static::STATUS_MATRIX_FIELD][$old][$new]
        ) {
            return false;
        }

        if (
            array_key_exists(
                static::STATUS_MATRIX_FIELD,
                $_SESSION['glpiactiveprofile']
            )
            && static::isStatusExists($new)
        ) { // maybe not set for post-only
            return true;
        }

        return false;
    }


    /**
     * Check if an itil object is still in an open status
     *
     * @since 10.0
     *
     * @return bool
     */
    public function isNotSolved()
    {
        return !in_array(
            $this->fields['status'],
            array_merge(
                $this->getSolvedStatusArray(),
                $this->getClosedStatusArray()
            )
        );
    }

    /**
     * Check if an itil object has a solved status
     *
     * @since 10.0
     *
     * @param bool $include_closed do we want ticket with closed status also ?
     *
     * @return bool
     */
    public function isSolved(bool $include_closed = false)
    {
        $status = $this->getSolvedStatusArray();
        if ($include_closed) {
            $status = array_merge($status, $this->getClosedStatusArray());
        }

        return in_array(
            $this->fields['status'] ?? null,
            $status
        );
    }

    /**
     * Check if an itil object has a closed status
     *
     * @since 10.0
     *
     * @return bool
     */
    public function isClosed()
    {
        return in_array(
            $this->fields['status'] ?? null,
            $this->getClosedStatusArray()
        );
    }


    /**
     * Get the ITIL object status allowed for a current status
     *
     * @since 0.84 new proto
     *
     * @param int|null $current   status
     *
     * @return array
     **/
    public static function getAllowedStatusArray($current)
    {

        $tab = static::getAllStatusArray();
        if (!static::isStatusExists($current)) {
            $current = self::INCOMING;
        }

        foreach (array_keys($tab) as $status) {
            if (
                ($status != $current)
                && !static::isAllowedStatus($current, $status)
            ) {
                unset($tab[$status]);
            }
        }
        return $tab;
    }

    /**
     * Is the ITIL object status exists for the object
     *
     * @since 0.85
     *
     * @param integer $status   status
     *
     * @return boolean
     **/
    public static function isStatusExists($status)
    {

        $tab = static::getAllStatusArray();

        return isset($tab[$status]);
    }

    /**
     * Dropdown of object status
     *
     * @since 0.84 new proto
     *
     * @param $options   array of options
     *  - name     : select name (default is status)
     *  - value    : default value (default self::INCOMING)
     *  - showtype : list proposed : normal, search or allowed (default normal)
     *  - display  : boolean if false get string
     *
     * @return string|integer Output string if display option is set to false,
     *                        otherwise random part of dropdown id
     **/
    public static function dropdownStatus(array $options = [])
    {

        $p = [
            'name'              => 'status',
            'showtype'          => 'normal',
            'display'           => true,
            'templateResult'    => "templateItilStatus",
            'templateSelection' => "templateItilStatus",
        ];

        if (is_array($options) && count($options)) {
            foreach ($options as $key => $val) {
                $p[$key] = $val;
            }
        }

        if (!isset($p['value']) || empty($p['value'])) {
            $p['value']     = self::INCOMING;
        }

        switch ($p['showtype']) {
            case 'allowed':
                $current = isset($p['value_calculation']) && $p['value_calculation'] !== ''
                    ? $p['value_calculation']
                    : $p['value'];
                $tab = static::getAllowedStatusArray($current);
                break;

            case 'search':
                $tab = static::getAllStatusArray(true);
                break;

            default:
                $tab = static::getAllStatusArray(false);
                break;
        }

        return Dropdown::showFromArray($p['name'], $tab, $p);
    }


    /**
     * Get ITIL object status Name
     *
     * @since 0.84
     *
     * @param integer $value     status ID
     **/
    public static function getStatus($value)
    {

        $tab  = static::getAllStatusArray(true);
        // Return $value if not defined
        return ($tab[$value] ?? $value);
    }


    /**
     * get field part name corresponding to actor type
     *
     * @param $type      integer : user type
     *
     * @since 0.84.6
     *
     * @return string|boolean Field part or false if not applicable
     **/
    public static function getActorFieldNameType($type)
    {

        switch ($type) {
            case CommonITILActor::REQUESTER:
                return 'requester';

            case CommonITILActor::OBSERVER:
                return 'observer';

            case CommonITILActor::ASSIGN:
                return 'assign';

            default:
                return false;
        }
    }

    /**
     * display a value according to a field
     *
     * @since 0.83
     *
     * @param $field     String         name of the field
     * @param $values    String / Array with the value to display
     * @param $options   Array          of option
     *
     * @return string
     **/
    public static function getSpecificValueToDisplay($field, $values, array $options = [])
    {

        if (!is_array($values)) {
            $values = [$field => $values];
        }
        switch ($field) {
            case 'status':
                return static::getStatus($values[$field]);

            case 'urgency':
                return static::getUrgencyName($values[$field]);

            case 'impact':
                return static::getImpactName($values[$field]);

            case 'priority':
                return static::getPriorityName($values[$field]);

            case 'global_validation':
                return CommonITILValidation::getStatus($values[$field]);
        }
        return parent::getSpecificValueToDisplay($field, $values, $options);
    }


    /**
     * @since 0.84
     *
     * @param $field
     * @param $name            (default '')
     * @param $values          (default '')
     * @param $options   array
     **/
    public static function getSpecificValueToSelect($field, $name = '', $values = '', array $options = [])
    {

        if (!is_array($values)) {
            $values = [$field => $values];
        }
        $options['display'] = false;

        switch ($field) {
            case 'status':
                $options['name']  = $name;
                $options['value'] = $values[$field];
                return static::dropdownStatus($options);

            case 'impact':
                $options['name']  = $name;
                $options['value'] = $values[$field];
                return static::dropdownImpact($options);

            case 'urgency':
                $options['name']  = $name;
                $options['value'] = $values[$field];
                return static::dropdownUrgency($options);

            case 'priority':
                $options['name']  = $name;
                $options['value'] = $values[$field];
                $options['enable_filtering'] = false;
                return static::dropdownPriority($options);

            case 'global_validation':
                $options['global'] = true;
                $options['value']  = $values[$field];
                return CommonITILValidation::dropdownStatus($name, $options);
        }
        return parent::getSpecificValueToSelect($field, $name, $values, $options);
    }


    /**
     * @since 0.85
     *
     * @see CommonDBTM::showMassiveActionsSubForm()
     **/
    public static function showMassiveActionsSubForm(MassiveAction $ma)
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        switch ($ma->getAction()) {
            case 'add_task':
                $itemtype = $ma->getItemtype(true);
                $tasktype = $itemtype . 'Task';
                if ($ttype = getItemForItemtype($tasktype)) {
                    /** @var CommonITILTask $ttype */
                    $ttype->showMassiveActionAddTaskForm();
                    return true;
                }
                return false;

            case 'add_actor':
                $types            = [0                          => Dropdown::EMPTY_VALUE,
                    CommonITILActor::REQUESTER => _n('Requester', 'Requesters', 1),
                    CommonITILActor::OBSERVER  => _n('Watcher', 'Watchers', 1),
                    CommonITILActor::ASSIGN    => __('Assigned to'),
                ];
                $rand             = Dropdown::showFromArray('actortype', $types);

                $paramsmassaction = ['actortype' => '__VALUE__'];

                Ajax::updateItemOnSelectEvent(
                    "dropdown_actortype$rand",
                    "show_massiveaction_field",
                    $CFG_GLPI["root_doc"] .
                                             "/ajax/dropdownMassiveActionAddActor.php",
                    $paramsmassaction
                );
                echo "<span id='show_massiveaction_field'>&nbsp;</span>\n";
                return true;
            case 'update_notif':
                Dropdown::showYesNo('use_notification');
                echo "<br><br>";
                echo Html::submit(_x('button', 'Post'), ['name' => 'massiveaction']);
                return true;
                return true;
        }
        return parent::showMassiveActionsSubForm($ma);
    }


    /**
     * @since 0.85
     *
     * @see CommonDBTM::processMassiveActionsForOneItemtype()
     **/
    public static function processMassiveActionsForOneItemtype(
        MassiveAction $ma,
        CommonDBTM $item,
        array $ids
    ) {
        /** @var CommonITILObject $item */
        switch ($ma->getAction()) {
            case 'add_actor':
                $input = $ma->getInput();
                foreach ($ids as $id) {
                    $input2 = ['id' => $id];
                    if (isset($input['_itil_requester'])) {
                        $input2['_itil_requester'] = $input['_itil_requester'];
                    }
                    if (isset($input['_itil_observer'])) {
                        $input2['_itil_observer'] = $input['_itil_observer'];
                    }
                    if (isset($input['_itil_assign'])) {
                        $input2['_itil_assign'] = $input['_itil_assign'];
                    }
                    if ($item->can($id, UPDATE)) {
                        if ($item->update($input2)) {
                            $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK);
                        } else {
                            $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO);
                            $ma->addMessage($item->getErrorMessage(ERROR_ON_ACTION));
                        }
                    } else {
                        $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_NORIGHT);
                        $ma->addMessage($item->getErrorMessage(ERROR_RIGHT));
                    }
                }
                return;

            case 'update_notif':
                $input = $ma->getInput();
                foreach ($ids as $id) {
                    if ($item->can($id, UPDATE)) {
                        $linkclass = new $item->userlinkclass();
                        foreach ($linkclass->getActors($id) as $users) {
                            foreach ($users as $data) {
                                $data['use_notification'] = $input['use_notification'];
                                $linkclass->update($data);
                            }
                        }
                        $linkclass = new $item->supplierlinkclass();
                        foreach ($linkclass->getActors($id) as $users) {
                            foreach ($users as $data) {
                                $data['use_notification'] = $input['use_notification'];
                                $linkclass->update($data);
                            }
                        }

                        $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK);
                    } else {
                        $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_NORIGHT);
                        $ma->addMessage($item->getErrorMessage(ERROR_RIGHT));
                    }
                }
                return;

            case 'add_task':
                if (!($task = getItemForItemtype($item->getType() . 'Task'))) {
                    $ma->itemDone($item->getType(), $ids, MassiveAction::ACTION_KO);
                    break;
                }
                $field = $item->getForeignKeyField();

                $input = $ma->getInput();

                foreach ($ids as $id) {
                    if ($item->getFromDB($id)) {
                        $input2 = [
                            $field              => $id,
                            'taskcategories_id' => $input['taskcategories_id'],
                            'actiontime'        => $input['actiontime'],
                            'state'             => $input['state'],
                            'content'           => $input['content'],
                        ];
                        if (
                            $task->can(-1, CREATE, $input2)
                            && !in_array(
                                $item->fields['status'],
                                array_merge($item->getSolvedStatusArray(), $item->getClosedStatusArray())
                            )
                        ) {
                            if ($task->add($input2)) {
                                $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK);
                            } else {
                                $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO);
                                $ma->addMessage($item->getErrorMessage(ERROR_ON_ACTION));
                            }
                        } else {
                            $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_NORIGHT);
                            $ma->addMessage($item->getErrorMessage(ERROR_RIGHT));
                        }
                    } else {
                        $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO);
                        $ma->addMessage($item->getErrorMessage(ERROR_NOT_FOUND));
                    }
                }
                return;
        }
        parent::processMassiveActionsForOneItemtype($ma, $item, $ids);
    }


    /**
     * @since 0.85
     **/
    public function getSearchOptionsMain()
    {
        /** @var \DBmysql $DB */
        global $DB;

        $tab = [];

        $tab[] = [
            'id'                 => 'common',
            'name'               => __('Characteristics'),
        ];

        $tab[] = [
            'id'                 => '1',
            'table'              => $this->getTable(),
            'field'              => 'name',
            'name'               => __('Title'),
            'datatype'           => 'itemlink',
            'searchtype'         => 'contains',
            'massiveaction'      => false,
            'additionalfields'   => ['id', 'status'],
        ];

        $tab[] = [
            'id'                 => '21',
            'table'              => $this->getTable(),
            'field'              => 'content',
            'name'               => __('Description'),
            'massiveaction'      => false,
            'datatype'           => 'text',
            'htmltext'           => true,
        ];

        $tab[] = [
            'id'                 => '2',
            'table'              => $this->getTable(),
            'field'              => 'id',
            'name'               => __('ID'),
            'massiveaction'      => false,
            'datatype'           => 'number',
        ];

        $tab[] = [
            'id'                 => '12',
            'table'              => $this->getTable(),
            'field'              => 'status',
            'name'               => __('Status'),
            'searchtype'         => 'equals',
            'datatype'           => 'specific',
        ];

        $tab[] = [
            'id'                 => '10',
            'table'              => $this->getTable(),
            'field'              => 'urgency',
            'name'               => __('Urgency'),
            'searchtype'         => 'equals',
            'datatype'           => 'specific',
        ];

        $tab[] = [
            'id'                 => '11',
            'table'              => $this->getTable(),
            'field'              => 'impact',
            'name'               => __('Impact'),
            'searchtype'         => 'equals',
            'datatype'           => 'specific',
        ];

        $tab[] = [
            'id'                 => '3',
            'table'              => $this->getTable(),
            'field'              => 'priority',
            'name'               => __('Priority'),
            'searchtype'         => 'equals',
            'datatype'           => 'specific',
        ];

        $tab[] = [
            'id'                 => '15',
            'table'              => $this->getTable(),
            'field'              => 'date',
            'name'               => __('Opening date'),
            'datatype'           => 'datetime',
            'massiveaction'      => false,
        ];

        $tab[] = [
            'id'                 => '16',
            'table'              => $this->getTable(),
            'field'              => 'closedate',
            'name'               => __('Closing date'),
            'datatype'           => 'datetime',
            'massiveaction'      => false,
        ];

        $tab[] = [
            'id'                 => '18',
            'table'              => $this->getTable(),
            'field'              => 'time_to_resolve',
            'name'               => __('Time to resolve'),
            'datatype'           => 'datetime',
            'maybefuture'        => true,
            'massiveaction'      => false,
            'additionalfields'   => ['solvedate', 'status'],
        ];

        $tab[] = [
            'id'                 => '151',
            'table'              => $this->getTable(),
            'field'              => 'time_to_resolve',
            'name'               => __('Time to resolve + Progress'),
            'massiveaction'      => false,
            'nosearch'           => true,
            'additionalfields'   => ['status'],
        ];

        $tab[] = [
            'id'                 => '82',
            'table'              => $this->getTable(),
            'field'              => 'is_late',
            'name'               => __('Time to resolve exceeded'),
            'datatype'           => 'bool',
            'massiveaction'      => false,
            'computation'        => self::generateSLAOLAComputation('time_to_resolve'),
        ];

        $tab[] = [
            'id'                 => '17',
            'table'              => $this->getTable(),
            'field'              => 'solvedate',
            'name'               => __('Resolution date'),
            'datatype'           => 'datetime',
            'massiveaction'      => false,
        ];

        $tab[] = [
            'id'                 => '19',
            'table'              => $this->getTable(),
            'field'              => 'date_mod',
            'name'               => __('Last update'),
            'datatype'           => 'datetime',
            'massiveaction'      => false,
        ];

        $newtab = [
            'id'                 => '7',
            'table'              => 'glpi_itilcategories',
            'field'              => 'completename',
            'name'               => _n('Category', 'Categories', 1),
            'datatype'           => 'dropdown',
        ];

        if (
            !Session::isCron() // no filter for cron
            && Session::getCurrentInterface() == 'helpdesk'
        ) {
            $newtab['condition']         = ['is_helpdeskvisible' => 1];
        }
        $tab[] = $newtab;

        $tab[] = [
            'id'                 => '80',
            'table'              => 'glpi_entities',
            'field'              => 'completename',
            'name'               => Entity::getTypeName(1),
            'massiveaction'      => false,
            'datatype'           => 'dropdown',
        ];

        $tab[] = [
            'id'                 => '45',
            'table'              => $this->getTable(),
            'field'              => 'actiontime',
            'name'               => __('Total duration'),
            'datatype'           => 'timestamp',
            'massiveaction'      => false,
            'nosearch'           => true,
        ];

        $newtab = [
            'id'                 => '64',
            'table'              => 'glpi_users',
            'field'              => 'name',
            'linkfield'          => 'users_id_lastupdater',
            'name'               => __('Last edit by'),
            'massiveaction'      => false,
            'datatype'           => 'dropdown',
            'right'              => 'all',
        ];

        // Filter search fields for helpdesk
        if (
            !Session::isCron() // no filter for cron
            && Session::getCurrentInterface() != 'central'
        ) {
            // last updater no search
            $newtab['nosearch'] = true;
        }
        $tab[] = $newtab;

        // add objectlock search options
        $tab = array_merge($tab, ObjectLock::rawSearchOptionsToAdd(get_class($this)));

        // For ITIL template
        $tab[] = [
            'id'                 => '142',
            'table'              => 'glpi_documents',
            'field'              => 'name',
            'name'               => Document::getTypeName(Session::getPluralNumber()),
            'forcegroupby'       => true,
            'usehaving'          => true,
            'nosearch'           => true,
            'nodisplay'          => true,
            'datatype'           => 'dropdown',
            'massiveaction'      => false,
            'joinparams'         => [
                'jointype'           => 'items_id',
                'beforejoin'         => [
                    'table'              => 'glpi_documents_items',
                    'joinparams'         => [
                        'jointype'           => 'itemtype_item',
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => '400',
            'table'              => PendingReason::getTable(),
            'field'              => 'name',
            'name'               => PendingReason::getTypeName(1),
            'massiveaction'      => false,
            'searchtype'         => ['equals', 'notequals'],
            'datatype'           => 'dropdown',
            'joinparams'         => [
                'jointype'           => 'items_id',
                'beforejoin'         => [
                    'table'              => PendingReason_Item::getTable(),
                    'joinparams'         => [
                        'jointype'           => 'itemtype_item',
                    ],
                ],
            ],
        ];

        $location_so = Location::rawSearchOptionsToAdd();
        foreach ($location_so as &$so) {
            //duplicated search options :(
            switch ($so['id']) {
                case 3:
                    $so['id'] = 83;
                    break;
                case 91:
                    $so['id'] = 84;
                    break;
                case 92:
                    $so['id'] = 85;
                    break;
                case 93:
                    $so['id'] = 86;
                    break;
            }
        }

        $tab = array_merge($tab, $location_so);

        $tab = array_merge($tab, Project::rawSearchOptionsToAdd(static::class));

        return $tab;
    }


    /**
     * @since 0.85
     **/
    public function getSearchOptionsSolution()
    {
        /** @var \DBmysql $DB */
        global $DB;
        $tab = [];

        $tab[] = [
            'id'                 => 'solution',
            'name'               => ITILSolution::getTypeName(1),
        ];

        $tab[] = [
            'id'                 => '23',
            'table'              => 'glpi_solutiontypes',
            'field'              => 'name',
            'name'               => SolutionType::getTypeName(1),
            'datatype'           => 'dropdown',
            'massiveaction'      => false,
            'forcegroupby'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => ITILSolution::getTable(),
                    'joinparams'         => [
                        'jointype'           => 'itemtype_item',
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => '24',
            'table'              => ITILSolution::getTable(),
            'field'              => 'content',
            'name'               => ITILSolution::getTypeName(1),
            'datatype'           => 'text',
            'htmltext'           => true,
            'massiveaction'      => false,
            'forcegroupby'       => true,
            'joinparams'         => [
                'jointype'           => 'itemtype_item',
            ],
        ];

        $tab[] = [
            'id'                  => '38',
            'table'               => ITILSolution::getTable(),
            'field'               => 'status',
            'name'                => __('Any solution status'),
            'datatype'            => 'specific',
            'searchtype'          => ['equals', 'notequals'],
            'searchequalsonfield' => true,
            'massiveaction'       => false,
            'forcegroupby'        => true,
            'joinparams'          => [
                'jointype' => 'itemtype_item',
            ],
        ];

        $tab[] = [
            'id'                  => '39',
            'table'               => ITILSolution::getTable(),
            'field'               => 'status',
            'name'                => __('Last solution status'),
            'datatype'            => 'specific',
            'searchtype'          => ['equals', 'notequals'],
            'searchequalsonfield' => true,
            'massiveaction'       => false,
            'forcegroupby'        => true,
            'joinparams'          => [
                'jointype'  => 'itemtype_item',
                // Get only last created solution
                'condition' => [
                    'NEWTABLE.id'  => ['=', new QuerySubQuery([
                        'SELECT' => 'id',
                        'FROM'   => ITILSolution::getTable(),
                        'WHERE'  => [
                            ITILSolution::getTable() . '.items_id' => new QueryExpression($DB->quoteName('REFTABLE.id')),
                            ITILSolution::getTable() . '.itemtype' => static::getType(),
                        ],
                        'ORDER'  => ITILSolution::getTable() . '.id DESC',
                        'LIMIT'  => 1,
                    ]),
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => '74',
            'table'              => ITILSolution::getTable(),
            'field'              => 'date_creation',
            'name'               => _n('Latest date', 'Latest dates', 1),
            'datatype'           => 'datetime',
            'massiveaction'      => false,
            'forcegroupby'       => true,
            'joinparams'         => [
                'jointype'           => 'itemtype_item',
            ],
            'computation' => 'MAX( ' . $DB->quoteName('TABLE.date_creation') . ')',
            'nometa'             => true, // cannot GROUP_CONCAT a MAX
        ];

        return $tab;
    }


    public function getSearchOptionsStats()
    {
        $tab = [];

        $tab[] = [
            'id'                 => 'stats',
            'name'               => __('Statistics'),
        ];

        $tab[] = [
            'id'                 => '154',
            'table'              => $this->getTable(),
            'field'              => 'solve_delay_stat',
            'name'               => __('Resolution time'),
            'datatype'           => 'timestamp',
            'forcegroupby'       => true,
            'massiveaction'      => false,
        ];

        $tab[] = [
            'id'                 => '152',
            'table'              => $this->getTable(),
            'field'              => 'close_delay_stat',
            'name'               => __('Closing time'),
            'datatype'           => 'timestamp',
            'forcegroupby'       => true,
            'massiveaction'      => false,
        ];

        $tab[] = [
            'id'                 => '153',
            'table'              => $this->getTable(),
            'field'              => 'waiting_duration',
            'name'               => __('Waiting time'),
            'datatype'           => 'timestamp',
            'forcegroupby'       => true,
            'massiveaction'      => false,
        ];

        return $tab;
    }


    public function getSearchOptionsActors()
    {
        $tab = [];

        $tab[] = [
            'id'                 => 'requester',
            'name'               => _n('Requester', 'Requesters', 1),
        ];

        $newtab = [
            'id'                 => '4', // Also in Ticket_User::post_addItem() and Log::getHistoryData()
            'table'              => 'glpi_users',
            'field'              => 'name',
            'datatype'           => 'dropdown',
            'right'              => 'all',
            'name'               => _n('Requester', 'Requesters', 1),
            'forcegroupby'       => true,
            'massiveaction'      => false,
            'use_subquery'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => getTableForItemType($this->userlinkclass),
                    'joinparams'         => [
                        'jointype'           => 'child',
                        'condition'          => ['NEWTABLE.type' => CommonITILActor::REQUESTER],
                    ],
                ],
            ],
        ];

        if (
            !Session::isCron() // no filter for cron
            && Session::getCurrentInterface() == 'helpdesk'
        ) {
            $newtab['right']       = 'id';
        }
        $tab[] = $newtab;

        $newtab = [
            'id'                 => '71',  // Also in Group_Ticket::post_addItem() and Log::getHistoryData()
            'table'              => 'glpi_groups',
            'field'              => 'completename',
            'datatype'           => 'dropdown',
            'name'               => _n('Requester group', 'Requester groups', 1),
            'forcegroupby'       => true,
            'massiveaction'      => false,
            'condition'          => ['is_requester' => 1],
            'use_subquery'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => getTableForItemType($this->grouplinkclass),
                    'joinparams'         => [
                        'jointype'           => 'child',
                        'condition'          => ['NEWTABLE.type' => CommonITILActor::REQUESTER],
                    ],
                ],
            ],
        ];

        if (
            !Session::isCron() // no filter for cron
            && Session::getCurrentInterface() == 'helpdesk'
        ) {
            $newtab['condition'] = array_merge(
                $newtab['condition'],
                ['id' => $_SESSION['glpigroups']]
            );
        }
        $tab[] = $newtab;

        $newtab = [
            'id'                 => '22',
            'table'              => 'glpi_users',
            'field'              => 'name',
            'datatype'           => 'dropdown',
            'right'              => 'all',
            'linkfield'          => 'users_id_recipient',
            'name'               => __('Writer'),
        ];

        if (
            !Session::isCron() // no filter for cron
            && Session::getCurrentInterface() == 'helpdesk'
        ) {
            $newtab['right']       = 'id';
        }
        $tab[] = $newtab;

        $tab[] = [
            'id'                 => 'observer',
            'name'               => _n('Watcher', 'Watchers', 1),
        ];

        $tab[] = [
            'id'                 => '66', // Also in Ticket_User::post_addItem() and Log::getHistoryData()
            'table'              => 'glpi_users',
            'field'              => 'name',
            'datatype'           => 'dropdown',
            'right'              => 'all',
            'name'               => _n('Watcher', 'Watchers', 1),
            'forcegroupby'       => true,
            'massiveaction'      => false,
            'use_subquery'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => getTableForItemType($this->userlinkclass),
                    'joinparams'         => [
                        'jointype'           => 'child',
                        'condition'          => ['NEWTABLE.type' => CommonITILActor::OBSERVER],
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => '65', // Also in Group_Ticket::post_addItem() and Log::getHistoryData()
            'table'              => 'glpi_groups',
            'field'              => 'completename',
            'datatype'           => 'dropdown',
            'name'               => _n('Watcher group', 'Watcher groups', 1),
            'forcegroupby'       => true,
            'massiveaction'      => false,
            'condition'          => ['is_watcher' => 1],
            'use_subquery'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => getTableForItemType($this->grouplinkclass),
                    'joinparams'         => [
                        'jointype'           => 'child',
                        'condition'          => ['NEWTABLE.type' => CommonITILActor::OBSERVER],
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => 'assign',
            'name'               => __('Assigned to'),
        ];

        $tab[] = [
            'id'                 => '5', // Also in Ticket_User::post_addItem() and Log::getHistoryData()
            'table'              => 'glpi_users',
            'field'              => 'name',
            'datatype'           => 'dropdown',
            'right'              => 'own_ticket',
            'name'               => __('Technician'),
            'forcegroupby'       => true,
            'massiveaction'      => false,
            'use_subquery'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => getTableForItemType($this->userlinkclass),
                    'joinparams'         => [
                        'jointype'           => 'child',
                        'condition'          => ['NEWTABLE.type' => CommonITILActor::ASSIGN],
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => '6', // Also in Supplier_Ticket::post_addItem() and Log::getHistoryData()
            'table'              => 'glpi_suppliers',
            'field'              => 'name',
            'datatype'           => 'dropdown',
            'name'               => __('Assigned to a supplier'),
            'forcegroupby'       => true,
            'massiveaction'      => false,
            'use_subquery'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => getTableForItemType($this->supplierlinkclass),
                    'joinparams'         => [
                        'jointype'           => 'child',
                        'condition'          => ['NEWTABLE.type' => CommonITILActor::ASSIGN],
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => '8', // Also in Group_Ticket::post_addItem() and Log::getHistoryData()
            'table'              => 'glpi_groups',
            'field'              => 'completename',
            'datatype'           => 'dropdown',
            'name'               => __('Technician group'),
            'forcegroupby'       => true,
            'massiveaction'      => false,
            'condition'          => ['is_assign' => 1],
            'use_subquery'       => true,
            'joinparams'         => [
                'beforejoin'         => [
                    'table'              => getTableForItemType($this->grouplinkclass),
                    'joinparams'         => [
                        'jointype'           => 'child',
                        'condition'          => ['NEWTABLE.type' => CommonITILActor::ASSIGN],
                    ],
                ],
            ],
        ];

        $tab[] = [
            'id'                 => 'notification',
            'name'               => _n('Notification', 'Notifications', Session::getPluralNumber()),
        ];

        $tab[] = [
            'id'                 => '35',
            'table'              => getTableForItemType($this->userlinkclass),
            'field'              => 'use_notification',
            'name'               => __('Email followup'),
            'datatype'           => 'bool',
            'massiveaction'      => false,
            'joinparams'         => [
                'jointype'           => 'child',
                'condition'          => ['NEWTABLE.type' => CommonITILActor::REQUESTER],
            ],
        ];

        $tab[] = [
            'id'                 => '34',
            'table'              => getTableForItemType($this->userlinkclass),
            'field'              => 'alternative_email',
            'name'               => __('Email for followup'),
            'datatype'           => 'email',
            'massiveaction'      => false,
            'joinparams'         => [
                'jointype'           => 'child',
                'condition'          => ['NEWTABLE.type' => CommonITILActor::REQUESTER],
            ],
        ];

        return $tab;
    }

    public static function generateSLAOLAComputation($type, $table = "TABLE")
    {
        /** @var \DBmysql $DB */
        global $DB;

        switch ($type) {
            case 'internal_time_to_own':
            case 'time_to_own':
                return 'IF(' . $DB->quoteName($table . '.' . $type) . ' IS NOT NULL
            AND ' . $DB->quoteName($table . '.status') . ' <> ' . self::WAITING . '
            AND ((' . $DB->quoteName($table . '.takeintoaccountdate') . ' IS NOT NULL AND
                 ' . $DB->quoteName($table . '.takeintoaccountdate') . ' > ' . $DB->quoteName($table . '.' . $type) . ')
                 OR (' . $DB->quoteName($table . '.takeintoaccountdate') . ' IS NULL AND
                 ' . $DB->quoteName($table . '.takeintoaccount_delay_stat') . '
                        > TIMESTAMPDIFF(SECOND,
                                        ' . $DB->quoteName($table . '.date') . ',
                                        ' . $DB->quoteName($table . '.' . $type) . '))
                 OR (' . $DB->quoteName($table . '.takeintoaccount_delay_stat') . ' = 0
                      AND ' . $DB->quoteName($table . '.' . $type) . ' < NOW())),
            1, 0)';
                break;

            case 'internal_time_to_resolve':
            case 'time_to_resolve':
                return 'IF(' . $DB->quoteName($table . '.' . $type) . ' IS NOT NULL
            AND ' . $DB->quoteName($table . '.status') . ' <> ' . self::WAITING . '
            AND (' . $DB->quoteName($table . '.solvedate') . ' > ' . $DB->quoteName($table . '.' . $type) . '
                  OR (' . $DB->quoteName($table . '.solvedate') . ' IS NULL
                     AND ' . $DB->quoteName($table . '.' . $type) . ' < NOW())),
            1, 0)';
                break;
        }
    }

    /**
     * Get status icon
     *
     * @since 9.3
     *
     * @return string
     */
    public static function getStatusIcon($status)
    {
        $class = static::getStatusClass($status);
        $label = static::getStatus($status);
        return "<i class='$class me-1' title='$label' data-bs-toggle='tooltip'></i>";
    }

    /**
     * Get status class
     *
     * @since 9.3
     *
     * @return string
     */
    public static function getStatusClass($status)
    {
        $class = null;
        $solid = true;

        switch ($status) {
            case self::INCOMING:
                $class = 'circle';
                break;
            case self::ASSIGNED:
                $class = 'circle';
                $solid = false;
                break;
            case self::PLANNED:
                $class = 'calendar';
                $solid = false;
                break;
            case self::WAITING:
                $class = 'circle';
                break;
            case self::SOLVED:
                $class = 'circle';
                $solid = false;
                break;
            case self::CLOSED:
                $class = 'circle';
                break;
            case self::ACCEPTED:
                $class = 'check-circle';
                break;
            case self::OBSERVED:
                $class = 'eye';
                break;
            case self::EVALUATION:
                $class = 'circle';
                $solid = false;
                break;
            case self::APPROVAL:
                $class = 'question-circle';
                break;
            case self::TEST:
                $class = 'question-circle';
                break;
            case self::QUALIFICATION:
                $class = 'circle';
                $solid = false;
                break;
            case Change::REFUSED:
                $class = 'times-circle';
                $solid = false;
                break;
            case Change::CANCELED:
                $class = 'ban';
                break;
        }

        return $class == null
         ? ''
         : 'itilstatus ' . ($solid ? 'fas fa-' : 'far fa-') . $class .
         " " . static::getStatusKey($status);
    }

    /**
     * Get status key
     *
     * @since 9.3
     *
     * @return string
     */
    public static function getStatusKey($status)
    {
        $key = '';
        switch ($status) {
            case self::INCOMING:
                $key = 'new';
                break;
            case self::ASSIGNED:
                $key = 'assigned';
                break;
            case self::PLANNED:
                $key = 'planned';
                break;
            case self::WAITING:
                $key = 'waiting';
                break;
            case self::SOLVED:
                $key = 'solved';
                break;
            case self::CLOSED:
                $key = 'closed';
                break;
            case self::ACCEPTED:
                $key = 'accepted';
                break;
            case self::OBSERVED:
                $key = 'observe';
                break;
            case self::EVALUATION:
                $key = 'eval';
                break;
            case self::APPROVAL:
                $key = 'approval';
                break;
            case self::TEST:
                $key = 'test';
                break;
            case self::QUALIFICATION:
                $key = 'qualif';
                break;
        }
        return $key;
    }


    /**
     * show actor add div
     *
     * @param $type         string   actor type
     * @param $rand_type    integer  rand value of div to use
     * @param $entities_id  integer  entity ID
     * @param $is_hidden    array    of hidden fields (if empty consider as not hidden)
     * @param $withgroup    boolean  allow adding a group (true by default)
     * @param $withsupplier boolean  allow adding a supplier (only one possible in ASSIGN case)
     *                               (false by default)
     * @param $inobject     boolean  display in ITIL object ? (true by default)
     *
     * @return void|boolean Nothing if displayed, false if not applicable
     **/
    public function showActorAddForm(
        $type,
        $rand_type,
        $entities_id,
        $is_hidden = [],
        $withgroup = true,
        $withsupplier = false,
        $inobject = true
    ) {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $types = ['user'  => User::getTypeName(1)];

        if ($withgroup) {
            $types['group'] = Group::getTypeName(1);
        }

        if (
            $withsupplier
            && ($type == CommonITILActor::ASSIGN)
        ) {
            $types['supplier'] = Supplier::getTypeName(1);
        }

        $typename = static::getActorFieldNameType($type);
        switch ($type) {
            case CommonITILActor::REQUESTER:
                if (isset($is_hidden['_users_id_requester']) && $is_hidden['_users_id_requester']) {
                    unset($types['user']);
                }
                if (isset($is_hidden['_groups_id_requester']) && $is_hidden['_groups_id_requester']) {
                    unset($types['group']);
                }
                break;

            case CommonITILActor::OBSERVER:
                if (isset($is_hidden['_users_id_observer']) && $is_hidden['_users_id_observer']) {
                    unset($types['user']);
                }
                if (isset($is_hidden['_groups_id_observer']) && $is_hidden['_groups_id_observer']) {
                    unset($types['group']);
                }
                break;

            case CommonITILActor::ASSIGN:
                if (isset($is_hidden['_users_id_assign']) && $is_hidden['_users_id_assign']) {
                    unset($types['user']);
                }
                if (isset($is_hidden['_groups_id_assign']) && $is_hidden['_groups_id_assign']) {
                    unset($types['group']);
                }
                if (
                    isset($types['supplier'])
                    && isset($is_hidden['_suppliers_id_assign']) && $is_hidden['_suppliers_id_assign']
                ) {
                    unset($types['supplier']);
                }
                break;

            default:
                return false;
        }

        echo "<div " . ($inobject ? "style='display:none'" : '') . " id='itilactor$rand_type' class='actor-dropdown'>";
        $rand   = Dropdown::showFromArray(
            "_itil_" . $typename . "[_type]",
            $types,
            ['display_emptychoice' => true]
        );
        $params = ['type'            => '__VALUE__',
            'actortype'       => $typename,
            'itemtype'        => $this->getType(),
            'allow_email'     => (($type == CommonITILActor::OBSERVER)
                                            || $type == CommonITILActor::REQUESTER),
            'entity_restrict' => $entities_id,
            'use_notif'       => Entity::getUsedConfig('is_notif_enable_default', $entities_id, '', 1),
        ];

        Ajax::updateItemOnSelectEvent(
            "dropdown__itil_" . $typename . "[_type]$rand",
            "showitilactor" . $typename . "_$rand",
            $CFG_GLPI["root_doc"] . "/ajax/dropdownItilActors.php",
            $params
        );
        echo "<span id='showitilactor" . $typename . "_$rand' class='actor-dropdown'>&nbsp;</span>";
        if ($inobject) {
            echo "<hr>";
        }
        echo "</div>";
    }


    /**
     * show user add div on creation
     *
     * @param $type      integer  actor type
     * @param $options   array    options for default values ($options of showForm)
     *
     * @return integer Random part of inputs ids
     **/
    public function showActorAddFormOnCreate($type, array $options)
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        $typename = static::getActorFieldNameType($type);

        $itemtype = $this->getType();

        // For ticket templates : mandatories
        $key = $this->getTemplateFormFieldName();
        if (isset($options[$key])) {
            $tt = $options[$key];
            if (is_numeric($options[$key])) {
                $tt_id = $options[$key];
                $tt_classname = self::getTemplateClass();
                $tt = new $tt_classname();
                $tt->getFromDB($tt_id);
            }
            echo $tt->getMandatoryMark("_users_id_" . $typename);
        }

        $right = $options["_right"] ?? $this->getDefaultActorRightSearch($type);

        if ($options["_users_id_" . $typename] == 0 && !isset($_REQUEST["_users_id_$typename"]) && !isset($this->input["_users_id_$typename"])) {
            $options["_users_id_" . $typename] = $this->getDefaultActor($type);
        }
        $rand = $options['rand'] ?? mt_rand();
        $actor_name = '_users_id_' . $typename;
        if ($type == CommonITILActor::OBSERVER) {
            $actor_name = '_users_id_' . $typename . '[]';
        }
        $params = [
            'name'   => $actor_name,
            'value'  => $options["_users_id_" . $typename],
            'right'  => $right,
            'rand'   => $rand,
            'width'  => "95%",
            'entity' => Session::getMatchingActiveEntities($options['entities_id'] ?? $options['entity_restrict']),
        ];

        //only for active ldap and corresponding right
        $ldap_methods = getAllDataFromTable('glpi_authldaps', ['is_active' => 1]);
        if (
            count($ldap_methods)
            && Session::haveRight('user', User::IMPORTEXTAUTHUSERS)
        ) {
            $params['ldap_import'] = true;
        }

        if (
            $this->userentity_oncreate
            && ($type == CommonITILActor::REQUESTER)
        ) {
            $params['on_change'] = 'this.form.submit()';
            unset($params['entity']);
        }

        $params['_user_index'] = 0;
        if (isset($options['_user_index'])) {
            $params['_user_index'] = $options['_user_index'];
        }

        $paramscomment = [];
        if ($CFG_GLPI['notifications_mailing']) {
            $paramscomment = [
                'value'            => '__VALUE__',
                'field'            => "_users_id_" . $typename . "_notif",
                '_user_index'      => $params['_user_index'],
                'allow_email'      => $type == CommonITILActor::REQUESTER
                               || $type == CommonITILActor::OBSERVER,
                'use_notification' => $options["_users_id_" . $typename . "_notif"]['use_notification'],
            ];
            if (isset($options["_users_id_" . $typename . "_notif"]['alternative_email'])) {
                $paramscomment['alternative_email']
                = $options["_users_id_" . $typename . "_notif"]['alternative_email'];
            }
            $params['toupdate'] = [
                'value_fieldname' => 'value',
                'to_update'       => "notif_" . $typename . "_$rand",
                'url'             => $CFG_GLPI["root_doc"] . "/ajax/uemailUpdate.php",
                'moreparams'      => $paramscomment,
            ];
        }

        if (
            ($itemtype == 'Ticket')
            && ($type == CommonITILActor::ASSIGN)
        ) {
            $toupdate = [];
            if (isset($params['toupdate']) && is_array($params['toupdate'])) {
                $toupdate[] = $params['toupdate'];
            }
            $toupdate[] = [
                'value_fieldname' => 'value',
                'to_update'       => "countassign_$rand",
                'url'             => $CFG_GLPI["root_doc"] . "/ajax/actorinformation.php",
                'moreparams'      => ['users_id_assign' => '__VALUE__'],
            ];
            $params['toupdate'] = $toupdate;
        }

        // List all users in the active entities
        echo "<div class='text-nowrap'>";
        User::dropdown($params);

        if ($itemtype == 'Ticket') {
            // display opened tickets for user
            if (
                ($type == CommonITILActor::REQUESTER)
                && ($options["_users_id_" . $typename] > 0)
                && (Session::getCurrentInterface() != "helpdesk")
            ) {
                $options2 = [
                    'criteria' => [
                        [
                            'field'      => 4, // users_id
                            'searchtype' => 'equals',
                            'value'      => $options["_users_id_" . $typename],
                            'link'       => 'AND',
                        ],
                        [
                            'field'      => 12, // status
                            'searchtype' => 'equals',
                            'value'      => 'notold',
                            'link'       => 'AND',
                        ],
                    ],
                    'reset'    => 'reset',
                ];

                $url = $this->getSearchURL() . "?" . Toolbox::append_params($options2, '&amp;');

                echo "<a href='$url' title=\"" . __s('Processing') . "\" class='badge bg-secondary ms-1'>";
                echo $this->countActiveObjectsForUser($options["_users_id_" . $typename]);
                echo "</a>";
            }
        }
        echo "</div>";

        if ($itemtype == 'Ticket') {
            // Display active tickets for a tech
            // Need to update information on dropdown changes
            if ($type == CommonITILActor::ASSIGN) {
                echo "<span id='countassign_$rand'>";
                echo "</span>";

                echo "<script type='text/javascript'>";
                echo "$(function() {";
                Ajax::updateItemJsCode(
                    "countassign_$rand",
                    $CFG_GLPI["root_doc"] . "/ajax/actorinformation.php",
                    ['users_id_assign' => '__VALUE__'],
                    "dropdown__users_id_" . $typename . $rand
                );
                echo "});</script>";
            }
        }

        if ($CFG_GLPI['notifications_mailing']) {
            echo "<div id='notif_" . $typename . "_$rand' class='mt-2'>";
            echo "</div>";

            echo "<script type='text/javascript'>";
            echo "$(function() {";
            Ajax::updateItemJsCode(
                "notif_" . $typename . "_$rand",
                $CFG_GLPI["root_doc"] . "/ajax/uemailUpdate.php",
                $paramscomment,
                "dropdown_" . $actor_name . $rand
            );
            echo "});</script>";
        }

        return $rand;
    }


    /**
     * @param $actiontime
     **/
    public static function getActionTime($actiontime)
    {
        return Html::timestampToString($actiontime, false);
    }


    /**
     * @param $ID
     * @param $itemtype
     * @param $link      (default 0)
     **/
    public static function getAssignName($ID, $itemtype, $link = 0)
    {

        switch ($itemtype) {
            case 'User':
                if ($ID == 0) {
                    return "";
                }
                return getUserName($ID, $link);

            case 'Supplier':
            case 'Group':
                $item = new $itemtype();
                if ($item->getFromDB($ID)) {
                    if ($link) {
                        return $item->getLink(['comments' => true]);
                    }
                    return $item->getNameID();
                }
                return "";
        }
    }

    /**
     * Form to add a solution to an ITIL object
     *
     * @since 0.84
     * @since 9.2 Signature has changed
     *
     * @param CommonITILObject $item item instance
     *
     * @param $entities_id
     **/
    public static function showMassiveSolutionForm(CommonITILObject $item)
    {
        $solution = new ITILSolution();
        $solution->showForm(
            null,
            [
                'parent' => $item,
                'entity' => $item->getEntityID(),
                'noform' => true,
                'nokb'   => true,
            ]
        );
    }


    /**
     * Update date mod of the ITIL object
     *
     * @param $ID                    integer  ID of the ITIL object
     * @param $no_stat_computation   boolean  do not cumpute take into account stat (false by default)
     * @param $users_id_lastupdater  integer  to force last_update id (default 0 = not used)
     **/
    public function updateDateMod($ID, $no_stat_computation = false, $users_id_lastupdater = 0)
    {
        /** @var \DBmysql $DB */
        global $DB;

        if ($this->getFromDB($ID)) {
            // Force date mod and lastupdater
            $update = ['date_mod' => $_SESSION['glpi_currenttime']];

            // set last updater if interactive user
            if (!Session::isCron()) {
                $update['users_id_lastupdater'] = Session::getLoginUserID();
            } elseif ($users_id_lastupdater > 0) {
                $update['users_id_lastupdater'] = $users_id_lastupdater;
            }

            $DB->update(
                $this->getTable(),
                $update,
                ['id' => $ID]
            );
        }
    }


    /**
     * Update actiontime of the object based on actiontime of the tasks
     *
     * @param integer $ID ID of the object
     *
     * @return boolean : success
     **/
    public function updateActionTime($ID)
    {
        /** @var \DBmysql $DB */
        global $DB;

        $tot       = 0;
        $tasktable = getTableForItemType($this->getType() . 'Task');

        $result = $DB->request([
            'SELECT' => ['SUM' => 'actiontime as sumtime'],
            'FROM'   => $tasktable,
            'WHERE'  => [$this->getForeignKeyField() => $ID],
        ])->current();
        $sum = $result['sumtime'];
        if (!is_null($sum)) {
            $tot += $sum;
        }

        $result = $DB->update(
            $this->getTable(),
            [
                'actiontime' => $tot,
            ],
            [
                'id' => $ID,
            ]
        );
        return $result;
    }


    /**
     * Get all available types to which an ITIL object can be assigned
     **/
    public static function getAllTypesForHelpdesk()
    {
        /**
         * @var array $CFG_GLPI
         * @var array $PLUGIN_HOOKS
         */
        global $CFG_GLPI, $PLUGIN_HOOKS;

        /// TODO ticket_types -> itil_types

        $types = [];
        $ptypes = [];
        //Types of the plugins (keep the plugin hook for right check)
        if (isset($PLUGIN_HOOKS['assign_to_ticket'])) {
            foreach (array_keys($PLUGIN_HOOKS['assign_to_ticket']) as $plugin) {
                if (!Plugin::isPluginActive($plugin)) {
                    continue;
                }
                $ptypes = Plugin::doOneHook($plugin, 'AssignToTicket', $ptypes);
            }
        }
        asort($ptypes);
        //Types of the core (after the plugin for robustness)
        foreach ($CFG_GLPI["ticket_types"] as $itemtype) {
            if ($item = getItemForItemtype($itemtype)) {
                if (
                    !isPluginItemType($itemtype) // No plugin here
                    && isset($_SESSION["glpiactiveprofile"]["helpdesk_item_type"])
                    && in_array($itemtype, $_SESSION["glpiactiveprofile"]["helpdesk_item_type"])
                ) {
                    $types[$itemtype] = $item->getTypeName(1);
                }
            }
        }
        asort($types); // core type first... asort could be better ?

        // Drop not available plugins
        foreach (array_keys($ptypes) as $itemtype) {
            if (
                !isset($_SESSION["glpiactiveprofile"]["helpdesk_item_type"])
                || !in_array($itemtype, $_SESSION["glpiactiveprofile"]["helpdesk_item_type"])
            ) {
                unset($ptypes[$itemtype]);
            }
        }

        $types = array_merge($types, $ptypes);
        return $types;
    }


    /**
     * Check if it's possible to assign ITIL object to a type (core or plugin)
     *
     * @param string $itemtype the object's type
     *
     * @return boolean true if ticket can be assigned to this type, false if not
     **/
    public static function isPossibleToAssignType($itemtype)
    {

        if (in_array($itemtype, $_SESSION["glpiactiveprofile"]["helpdesk_item_type"])) {
            return true;
        }
        return false;
    }


    /**
     * Compute solve delay stat of the current ticket
     **/
    public function computeSolveDelayStat()
    {

        if (
            isset($this->fields['id'])
            && !empty($this->fields['date'])
            && !empty($this->fields['solvedate'])
        ) {
            $calendars_id = $this->getCalendar();
            $calendar     = new Calendar();

            // Using calendar
            if (
                ($calendars_id > 0)
                && $calendar->getFromDB($calendars_id)
            ) {
                return max(0, $calendar->getActiveTimeBetween(
                    $this->fields['date'],
                    $this->fields['solvedate']
                )
                                                            - $this->fields["waiting_duration"]);
            }
            // Not calendar defined
            return max(0, strtotime($this->fields['solvedate']) - strtotime($this->fields['date'])
                       - $this->fields["waiting_duration"]);
        }
        return 0;
    }


    /**
     * Compute close delay stat of the current ticket
     **/
    public function computeCloseDelayStat()
    {

        if (
            isset($this->fields['id'])
            && !empty($this->fields['date'])
            && !empty($this->fields['closedate'])
        ) {
            $calendars_id = $this->getCalendar();
            $calendar     = new Calendar();

            // Using calendar
            if (
                ($calendars_id > 0)
                && $calendar->getFromDB($calendars_id)
            ) {
                return max(0, $calendar->getActiveTimeBetween(
                    $this->fields['date'],
                    $this->fields['closedate']
                )
                                                             - $this->fields["waiting_duration"]);
            }
            // Not calendar defined
            return max(0, strtotime($this->fields['closedate']) - strtotime($this->fields['date'])
                       - $this->fields["waiting_duration"]);
        }
        return 0;
    }


    public function showStats()
    {

        if (
            !$this->canView()
            || !isset($this->fields['id'])
        ) {
            return false;
        }

        $this->showStatsDates();
        Plugin::doHook(Hooks::SHOW_ITEM_STATS, $this);
        $this->showStatsTimes();
    }

    public function showStatsDates()
    {
        echo "<table class='tab_cadre_fixe'>";
        echo "<tr><th colspan='2'>" . _n('Date', 'Dates', Session::getPluralNumber()) . "</th></tr>";

        echo "<tr class='tab_bg_2'><td>" . __('Opening date') . "</td>";
        echo "<td>" . Html::convDateTime($this->fields['date']) . "</td></tr>";

        echo "<tr class='tab_bg_2'><td>" . __('Time to resolve') . "</td>";
        echo "<td>" . Html::convDateTime($this->fields['time_to_resolve']) . "</td></tr>";

        if (!$this->isNotSolved()) {
            echo "<tr class='tab_bg_2'><td>" . __('Resolution date') . "</td>";
            echo "<td>" . Html::convDateTime($this->fields['solvedate']) . "</td></tr>";
        }

        if (in_array($this->fields['status'], $this->getClosedStatusArray())) {
            echo "<tr class='tab_bg_2'><td>" . __('Closing date') . "</td>";
            echo "<td>" . Html::convDateTime($this->fields['closedate']) . "</td></tr>";
        }
        echo "</table>";
    }

    public function showStatsTimes()
    {
        echo "<div class='dates_timelines'>";
        echo "<table class='tab_cadre_fixe'>";
        echo "<tr><th colspan='2'>" . _n('Time', 'Times', Session::getPluralNumber()) . "</th></tr>";

        if (isset($this->fields['takeintoaccount_delay_stat'])) {
            echo "<tr class='tab_bg_2'><td>" . __('Take into account') . "</td><td>";
            if ($this->fields['takeintoaccount_delay_stat'] > 0) {
                echo Html::timestampToString($this->fields['takeintoaccount_delay_stat'], 0, false);
            } else {
                echo '&nbsp;';
            }
            echo "</td></tr>";
        }

        if (!$this->isNotSolved()) {
            echo "<tr class='tab_bg_2'><td>" . __('Resolution') . "</td><td>";

            if ($this->fields['solve_delay_stat'] > 0) {
                echo Html::timestampToString($this->fields['solve_delay_stat'], 0, false);
            } else {
                echo '&nbsp;';
            }
            echo "</td></tr>";
        }

        if (in_array($this->fields['status'], $this->getClosedStatusArray())) {
            echo "<tr class='tab_bg_2'><td>" . __('Closure') . "</td><td>";
            if ($this->fields['close_delay_stat'] > 0) {
                echo Html::timestampToString($this->fields['close_delay_stat'], true, false);
            } else {
                echo '&nbsp;';
            }
            echo "</td></tr>";
        }

        echo "<tr class='tab_bg_2'><td>" . __('Pending') . "</td><td>";
        if ($this->fields['waiting_duration'] > 0) {
            echo Html::timestampToString($this->fields['waiting_duration'], 0, false);
        } else {
            echo '&nbsp;';
        }
        echo "</td></tr>";

        echo "</table>";
        echo "</div>";
    }


    /** Get users_ids of itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct users_ids which have itil object
     **/
    public function getUsedAuthorBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $linkclass = new $this->userlinkclass();
        $linktable = $linkclass->getTable();

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => [
                'glpi_users.id AS users_id',
                'glpi_users.name AS name',
                'glpi_users.realname AS realname',
                'glpi_users.firstname AS firstname',
            ],
            'DISTINCT' => true,
            'FROM'            => $ctable,
            'LEFT JOIN'       => [
                $linktable  => [
                    'ON' => [
                        $linktable  => $this->getForeignKeyField(),
                        $ctable     => 'id', [
                            'AND' => [
                                "$linktable.type"    => CommonITILActor::REQUESTER,
                            ],
                        ],
                    ],
                ],
            ],
            'INNER JOIN'      => [
                'glpi_users'   => [
                    'ON' => [
                        $linktable     => 'users_id',
                        'glpi_users'   => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                'realname',
                'firstname',
                'name',
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];
        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['users_id'],
                'link' => formatUserName(
                    $line['users_id'],
                    $line['name'],
                    $line['realname'],
                    $line['firstname'],
                    1
                ),
            ];
        }
        return $tab;
    }


    /** Get recipient of itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct recipents which have itil object
     **/
    public function getUsedRecipientBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => [
                'glpi_users.id AS user_id',
                'glpi_users.name AS name',
                'glpi_users.realname AS realname',
                'glpi_users.firstname AS firstname',
            ],
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'LEFT JOIN'       => [
                'glpi_users'   => [
                    'ON' => [
                        $ctable        => 'users_id_recipient',
                        'glpi_users'   => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                'realname',
                'firstname',
                'name',
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];

        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['user_id'],
                'link' => formatUserName(
                    $line['user_id'],
                    $line['name'],
                    $line['realname'],
                    $line['firstname'],
                    1
                ),
            ];
        }
        return $tab;
    }


    /** Get groups which have itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct groups of tickets
     **/
    public function getUsedGroupBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $linkclass = new $this->grouplinkclass();
        $linktable = $linkclass->getTable();

        $ctable = $this->getTable();
        $criteria = [
            'SELECT' => [
                'glpi_groups.id',
                'glpi_groups.completename',
            ],
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'LEFT JOIN'       => [
                $linktable  => [
                    'ON' => [
                        $linktable  => $this->getForeignKeyField(),
                        $ctable     => 'id', [
                            'AND' => [
                                "$linktable.type"    => CommonITILActor::REQUESTER,
                            ],
                        ],
                    ],
                ],
            ],
            'INNER JOIN'      => [
                'glpi_groups'   => [
                    'ON' => [
                        $linktable     => 'groups_id',
                        'glpi_groups'   => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                'glpi_groups.completename',
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];

        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['id'],
                'link' => $line['completename'],
            ];
        }
        return $tab;
    }


    /** Get recipient of itil object between 2 dates
     *
     * @param string  $date1 begin date
     * @param string  $date2 end date
     * @param boolean $title indicates if stat if by title (true) or type (false)
     *
     * @return array contains the distinct recipents which have tickets
     **/
    public function getUsedUserTitleOrTypeBetween($date1 = '', $date2 = '', $title = true)
    {
        /** @var \DBmysql $DB */
        global $DB;

        $linkclass = new $this->userlinkclass();
        $linktable = $linkclass->getTable();

        if ($title) {
            $table = "glpi_usertitles";
            $field = "usertitles_id";
        } else {
            $table = "glpi_usercategories";
            $field = "usercategories_id";
        }

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => "glpi_users.$field",
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'INNER JOIN'      => [
                $linktable  => [
                    'ON' => [
                        $linktable  => $this->getForeignKeyField(),
                        $ctable     => 'id',
                    ],
                ],
                'glpi_users'   => [
                    'ON' => [
                        $linktable     => 'users_id',
                        'glpi_users'   => 'id',
                    ],
                ],
            ],
            'LEFT JOIN'       => [
                $table         => [
                    'ON' => [
                        'glpi_users'   => $field,
                        $table         => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                "glpi_users.$field",
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];
        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line[$field],
                'link' => Dropdown::getDropdownName($table, $line[$field]),
            ];
        }
        return $tab;
    }


    /**
     * Get priorities of itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct priorities of tickets
     **/
    public function getUsedPriorityBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => 'priority',
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => 'priority',
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];
        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['priority'],
                'link' => static::getPriorityName($line['priority']),
            ];
        }
        return $tab;
    }


    /**
     * Get urgencies of itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct priorities of tickets
     **/
    public function getUsedUrgencyBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => 'urgency',
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => 'urgency',
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];

        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['urgency'],
                'link' => static::getUrgencyName($line['urgency']),
            ];
        }
        return $tab;
    }


    /**
     * Get impacts of itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct priorities of tickets
     **/
    public function getUsedImpactBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => 'impact',
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => 'impact',
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];

        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['impact'],
                'link' => static::getImpactName($line['impact']),
            ];
        }
        return $tab;
    }


    /**
     * Get request types of itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct request types of tickets
     **/
    public function getUsedRequestTypeBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => 'requesttypes_id',
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => 'requesttypes_id',
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];
        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['requesttypes_id'],
                'link' => Dropdown::getDropdownName('glpi_requesttypes', $line['requesttypes_id']),
            ];
        }
        return $tab;
    }


    /**
     * Get solution types of itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct request types of tickets
     **/
    public function getUsedSolutionTypeBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => 'solutiontypes_id',
            'DISTINCT'        => true,
            'FROM'            => ITILSolution::getTable(),
            'INNER JOIN'      => [
                $ctable   => [
                    'ON' => [
                        ITILSolution::getTable()   => 'items_id',
                        $ctable                    => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                ITILSolution::getTable() . ".itemtype" => $this->getType(),
                "$ctable.is_deleted"                   => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => 'solutiontypes_id',
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];
        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['solutiontypes_id'],
                'link' => Dropdown::getDropdownName('glpi_solutiontypes', $line['solutiontypes_id']),
            ];
        }
        return $tab;
    }


    /** Get users which have intervention assigned to  between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct users which have any intervention assigned to.
     **/
    public function getUsedTechBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $linkclass = new $this->userlinkclass();
        $linktable = $linkclass->getTable();
        $showlink = User::canView();

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => [
                'glpi_users.id AS users_id',
                'glpi_users.name AS name',
                'glpi_users.realname AS realname',
                'glpi_users.firstname AS firstname',
            ],
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'LEFT JOIN'       => [
                $linktable  => [
                    'ON' => [
                        $linktable  => $this->getForeignKeyField(),
                        $ctable     => 'id', [
                            'AND' => [
                                "$linktable.type"    => CommonITILActor::ASSIGN,
                            ],
                        ],
                    ],
                ],
                'glpi_users'   => [
                    'ON' => [
                        $linktable     => 'users_id',
                        'glpi_users'   => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                'realname',
                'firstname',
                'name',
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];

        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['users_id'],
                'link' => formatUserName($line['users_id'], $line['name'], $line['realname'], $line['firstname'], $showlink),
            ];
        }
        return $tab;
    }


    /** Get users which have followup assigned to  between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct users which have any followup assigned to.
     **/
    public function getUsedTechTaskBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $linktable = getTableForItemType($this->getType() . 'Task');
        $showlink = User::canView();

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => [
                'glpi_users.id AS users_id',
                'glpi_users.name AS name',
                'glpi_users.realname AS realname',
                'glpi_users.firstname AS firstname',
            ],
            'DISTINCT' => true,
            'FROM'            => $ctable,
            'LEFT JOIN'       => [
                $linktable  => [
                    'ON' => [
                        $linktable  => $this->getForeignKeyField(),
                        $ctable     => 'id',
                    ],
                ],
                'glpi_users'   => [
                    'ON' => [
                        $linktable     => 'users_id',
                        'glpi_users'   => 'id',
                    ],
                ],
                'glpi_profiles_users'   => [
                    'ON' => [
                        'glpi_users'            => 'id',
                        'glpi_profiles_users'   => 'users_id',
                    ],
                ],
                'glpi_profiles'         => [
                    'ON' => [
                        'glpi_profiles'         => 'id',
                        'glpi_profiles_users'   => 'profiles_id',
                    ],
                ],
                'glpi_profilerights'    => [
                    'ON' => [
                        'glpi_profiles'      => 'id',
                        'glpi_profilerights' => 'profiles_id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted"          => 0,
                'glpi_profilerights.name'     => 'ticket',
                'glpi_profilerights.rights'   => ['&', Ticket::OWN],
                "$linktable.users_id"         => ['<>', 0],
                ['NOT'                        => ["$linktable.users_id" => null]],
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                'realname',
                'firstname',
                'name',
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];

        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['users_id'],
                'link' => formatUserName($line['users_id'], $line['name'], $line['realname'], $line['firstname'], $showlink),
            ];
        }
        return $tab;
    }


    /** Get enterprises which have itil object assigned to between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct enterprises which have any tickets assigned to.
     **/
    public function getUsedSupplierBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $linkclass = new $this->supplierlinkclass();
        $linktable = $linkclass->getTable();

        $ctable = $this->getTable();
        $criteria = [
            'SELECT'          => [
                'glpi_suppliers.id AS suppliers_id_assign',
                'glpi_suppliers.name AS name',
            ],
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'LEFT JOIN'       => [
                $linktable        => [
                    'ON' => [
                        $linktable  => $this->getForeignKeyField(),
                        $ctable     => 'id', [
                            'AND' => [
                                "$linktable.type"    => CommonITILActor::ASSIGN,
                            ],
                        ],
                    ],
                ],
                'glpi_suppliers'  => [
                    'ON' => [
                        $linktable        => 'suppliers_id',
                        'glpi_suppliers'  => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                'name',
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];
        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['suppliers_id_assign'],
                'link' => '<a href="' . Supplier::getFormURLWithID($line['suppliers_id_assign']) . '">' . $line['name'] . '</a>',
            ];
        }
        return $tab;
    }


    /** Get groups assigned to itil object between 2 dates
     *
     * @param string $date1 begin date
     * @param string $date2 end date
     *
     * @return array contains the distinct groups assigned to a tickets
     **/
    public function getUsedAssignGroupBetween($date1 = '', $date2 = '')
    {
        /** @var \DBmysql $DB */
        global $DB;

        $linkclass = new $this->grouplinkclass();
        $linktable = $linkclass->getTable();

        $ctable = $this->getTable();
        $criteria = [
            'SELECT' => [
                'glpi_groups.id',
                'glpi_groups.completename',
            ],
            'DISTINCT'        => true,
            'FROM'            => $ctable,
            'LEFT JOIN'       => [
                $linktable  => [
                    'ON' => [
                        $linktable  => $this->getForeignKeyField(),
                        $ctable     => 'id', [
                            'AND' => [
                                "$linktable.type"    => CommonITILActor::ASSIGN,
                            ],
                        ],
                    ],
                ],
                'glpi_groups'   => [
                    'ON' => [
                        $linktable     => 'groups_id',
                        'glpi_groups'   => 'id',
                    ],
                ],
            ],
            'WHERE'           => [
                "$ctable.is_deleted" => 0,
            ] + getEntitiesRestrictCriteria($ctable),
            'ORDERBY'         => [
                'glpi_groups.completename',
            ],
        ];

        if (!empty($date1) || !empty($date2)) {
            $criteria['WHERE'][] = [
                'OR' => [
                    getDateCriteria("$ctable.date", $date1, $date2),
                    getDateCriteria("$ctable.closedate", $date1, $date2),
                ],
            ];
        }

        $iterator = $DB->request($criteria);
        $tab    = [];
        foreach ($iterator as $line) {
            $tab[] = [
                'id'   => $line['id'],
                'link' => $line['completename'],
            ];
        }
        return $tab;
    }


    /**
     * Display a line for an object
     *
     * @since 0.85 (befor in each object with differents parameters)
     *
     * @param $id                 Integer  ID of the object
     * @param $options            array of options
     *      output_type            : Default output type (see Search class / default Search::HTML_OUTPUT)
     *      row_num                : row num used for display
     *      type_for_massiveaction : itemtype for massive action
     *      id_for_massaction      : default 0 means no massive action
     *
     * @since 10.0.0 "followups" option has been dropped
     */
    public static function showShort($id, $options = [])
    {
        /** @var \DBmysql $DB */
        global $DB;

        $p = [
            'output_type'            => Search::HTML_OUTPUT,
            'row_num'                => 0,
            'type_for_massiveaction' => 0,
            'id_for_massiveaction'   => 0,
            'followups'              => false,
            'ticket_stats'           => false,
        ];

        if (count($options)) {
            foreach ($options as $key => $val) {
                $p[$key] = $val;
            }
        }

        $rand = mt_rand();

        /// TODO to be cleaned. Get datas and clean display links

        // Prints a job in short form
        // Should be called in a <table>-segment
        // Print links or not in case of user view
        // Make new job object and fill it from database, if success, print it
        $item         = new static();

        $candelete   = static::canDelete();
        $canupdate   = Session::haveRight(static::$rightname, UPDATE);
        $showprivate = Session::haveRight('followup', ITILFollowup::SEEPRIVATE);
        $align       = "class='left'";
        $align_desc  = "class='left'";

        if ($item->getFromDB($id)) {
            $item_num = 1;
            $bgcolor  = $_SESSION["glpipriority_" . $item->fields["priority"]];
            echo Search::showNewLine($p['output_type'], $p['row_num'] % 2, $item->isDeleted());

            $check_col = '';
            if (
                ($candelete || $canupdate)
                && ($p['output_type'] == Search::HTML_OUTPUT)
                && $p['id_for_massiveaction']
            ) {
                $check_col = Html::getMassiveActionCheckBox($p['type_for_massiveaction'], $p['id_for_massiveaction']);
            }
            echo Search::showItem($p['output_type'], $check_col, $item_num, $p['row_num'], $align);

            // First column ID
            echo Search::showItem($p['output_type'], $item->fields["id"], $item_num, $p['row_num'], $align);

            // Second column TITLE
            $second_column = "<span class='b'>" . $item->getName() . "</span>&nbsp;";
            if ($p['output_type'] == Search::HTML_OUTPUT && $item->canViewItem()) {
                $second_column  = sprintf(
                    __('%1$s (%2$s)'),
                    "<a id='" . $item->getType() . $item->fields["id"] . "$rand' href=\"" . $item->getLinkURL() . "\">$second_column</a>",
                    sprintf(
                        __('%1$s - %2$s'),
                        $item->numberOfFollowups($showprivate),
                        $item->numberOfTasks($showprivate)
                    )
                );
                $second_column = sprintf(
                    __('%1$s %2$s'),
                    $second_column,
                    Html::showToolTip(
                        RichText::getEnhancedHtml($item->fields['content']),
                        ['display' => false,
                            'applyto' => $item->getType() . $item->fields["id"] .
                        $rand,
                        ]
                    )
                );
            }
            echo Search::showItem($p['output_type'], $second_column, $item_num, $p['row_num'], $align);

            // third column
            if ($p['output_type'] == Search::HTML_OUTPUT) {
                $third_col = static::getStatusIcon($item->fields["status"]);
            } else {
                $third_col = static::getStatus($item->fields["status"]);
            }
            echo Search::showItem($p['output_type'], $third_col, $item_num, $p['row_num'], $align);


            // fourth column
            if ($item->fields['status'] == static::CLOSED) {
                $fourth_col = sprintf(
                    __('Closed on %s'),
                    ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : '') .
                    Html::convDateTime($item->fields['closedate'])
                );
            } elseif ($item->fields['status'] == static::SOLVED) {
                $fourth_col = sprintf(
                    __('Solved on %s'),
                    ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : '') .
                    Html::convDateTime($item->fields['solvedate'])
                );
            } elseif ($item->fields['begin_waiting_date']) {
                $fourth_col = sprintf(
                    __('Put on hold on %s'),
                    ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : '') .
                    Html::convDateTime($item->fields['begin_waiting_date'])
                );
            } elseif ($item->fields['time_to_resolve']) {
                $fourth_col = sprintf(
                    __('%1$s: %2$s'),
                    __('Time to resolve'),
                    ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : '') .
                    Html::convDateTime($item->fields['time_to_resolve'])
                );
            } else {
                $fourth_col = sprintf(
                    __('Opened on %s'),
                    ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : '') .
                    Html::convDateTime($item->fields['date'])
                );
            }
            echo Search::showItem($p['output_type'], $fourth_col, $item_num, $p['row_num'], $align . " width=130");

            // fifth column
            $fifth_col = Html::convDateTime($item->fields["date_mod"]);
            echo Search::showItem($p['output_type'], $fifth_col, $item_num, $p['row_num'], $align . " width=90");

            // sixth column
            if (count($_SESSION["glpiactiveentities"]) > 1) {
                $sixth_col = Dropdown::getDropdownName('glpi_entities', $item->fields['entities_id']);
                echo Search::showItem(
                    $p['output_type'],
                    $sixth_col,
                    $item_num,
                    $p['row_num'],
                    $align . " width=100"
                );
            }

            // seventh Column
            echo Search::showItem(
                $p['output_type'],
                "<span class='b'>" . static::getPriorityName($item->fields["priority"]) . "</span>",
                $item_num,
                $p['row_num'],
                "$align bgcolor='$bgcolor'"
            );

            // eighth Column
            $eighth_col = "";
            foreach ($item->getUsers(CommonITILActor::REQUESTER) as $d) {
                $userdata    = getUserName($d["users_id"], 2);
                $eighth_col .= sprintf(
                    __('%1$s %2$s'),
                    "<span class='b'>" . $userdata['name'] . "</span>",
                    Html::showToolTip(
                        $userdata["comment"],
                        ['link'    => $userdata["link"],
                            'display' => false,
                        ]
                    )
                );
                $eighth_col .= "<br>";
            }

            foreach ($item->getGroups(CommonITILActor::REQUESTER) as $d) {
                $eighth_col .= Dropdown::getDropdownName("glpi_groups", $d["groups_id"]);
                $eighth_col .= "<br>";
            }

            echo Search::showItem($p['output_type'], $eighth_col, $item_num, $p['row_num'], $align);

            // ninth column
            $ninth_col = "";
            foreach ($item->getUsers(CommonITILActor::ASSIGN) as $d) {
                if (
                    Session::getCurrentInterface() == 'helpdesk'
                    && !empty($anon_name = User::getAnonymizedNameForUser(
                        $d['users_id'],
                        $item->getEntityID()
                    ))
                ) {
                    $ninth_col .= $anon_name;
                } else {
                    $userdata   = getUserName($d["users_id"], 2);
                    $ninth_col .= sprintf(
                        __('%1$s %2$s'),
                        "<span class='b'>" . $userdata['name'] . "</span>",
                        Html::showToolTip(
                            $userdata["comment"],
                            ['link'    => $userdata["link"],
                                'display' => false,
                            ]
                        )
                    );
                }
                $ninth_col .= "<br>";
            }

            foreach ($item->getGroups(CommonITILActor::ASSIGN) as $d) {
                if (
                    Session::getCurrentInterface() == 'helpdesk'
                    && !empty($anon_name = Group::getAnonymizedName($item->getEntityID()))
                ) {
                    $ninth_col .= $anon_name;
                } else {
                    $ninth_col .= Dropdown::getDropdownName("glpi_groups", $d["groups_id"]);
                }
                $ninth_col .= "<br>";
            }

            foreach ($item->getSuppliers(CommonITILActor::ASSIGN) as $d) {
                $ninth_col .= Dropdown::getDropdownName("glpi_suppliers", $d["suppliers_id"]);
                $ninth_col .= "<br>";
            }
            echo Search::showItem($p['output_type'], $ninth_col, $item_num, $p['row_num'], $align);


            if (!$p['ticket_stats']) {
                // tenth Colum
                // Ticket : simple link to item
                $tenth_col  = "";
                $is_deleted = false;
                $item_ticket = new Item_Ticket();
                $data = $item_ticket->find(['tickets_id' => $item->fields['id']]);

                if ($item->getType() == 'Ticket') {
                    if (!empty($data)) {
                        foreach ($data as $val) {
                            if (!empty($val["itemtype"]) && ($val["items_id"] > 0)) {
                                if ($object = getItemForItemtype($val["itemtype"])) {
                                    if ($object->getFromDB($val["items_id"])) {
                                        $is_deleted = $object->isDeleted();

                                        $tenth_col .= $object->getTypeName();
                                        $tenth_col .= " - <span class='b'>";
                                        if ($item->canView()) {
                                            $tenth_col .= $object->getLink();
                                        } else {
                                            $tenth_col .= $object->getNameID();
                                        }
                                        $tenth_col .= "</span><br>";
                                    }
                                }
                            }
                        }
                    } else {
                        $tenth_col = __('General');
                    }

                    echo Search::showItem($p['output_type'], $tenth_col, $item_num, $p['row_num'], ($is_deleted ? " class='center deleted' " : $align));
                }

                // Seventh column
                echo Search::showItem(
                    $p['output_type'],
                    "<span class='b'>" .
                                    Dropdown::getDropdownName(
                                        'glpi_itilcategories',
                                        $item->fields["itilcategories_id"]
                                    ) .
                                 "</span>",
                    $item_num,
                    $p['row_num'],
                    $align
                );

                //eleventh column
                $eleventh_column  = '';
                $planned_infos = '';

                $tasktype      = $item->getType() . "Task";
                $plan          = new $tasktype();
                $items         = [];

                $result = $DB->request(
                    [
                        'FROM'  => $plan->getTable(),
                        'WHERE' => [
                            $item->getForeignKeyField() => $item->fields['id'],
                        ],
                    ]
                );
                foreach ($result as $plan) {
                    if (isset($plan['begin']) && $plan['begin']) {
                        $items[$plan['id']] = $plan['id'];
                        $planned_infos .= sprintf(
                            __('From %s') .
                                            ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : ''),
                            Html::convDateTime($plan['begin'])
                        );
                        $planned_infos .= sprintf(
                            __('To %s') .
                                            ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : ''),
                            Html::convDateTime($plan['end'])
                        );
                        if ($plan['users_id_tech']) {
                            $planned_infos .= sprintf(
                                __('By %s') .
                                          ($p['output_type'] == Search::HTML_OUTPUT ? '<br>' : ''),
                                getUserName($plan['users_id_tech'])
                            );
                        }
                        $planned_infos .= "<br>";
                    }
                }

                $eleventh_column = count($items);
                if ($eleventh_column) {
                    $eleventh_column = "<span class='pointer'
                                 id='" . $item->getType() . $item->fields["id"] . "planning$rand'>" .
                                 $eleventh_column . '</span>';
                    $eleventh_column = sprintf(
                        __('%1$s %2$s'),
                        $eleventh_column,
                        Html::showToolTip(
                            $planned_infos,
                            ['display' => false,
                                'applyto' => $item->getType() .
                                                                              $item->fields["id"] .
                            "planning" . $rand,
                            ]
                        )
                    );
                }

                echo Search::showItem(
                    $p['output_type'],
                    $eleventh_column,
                    $item_num,
                    $p['row_num'],
                    $align_desc . " width='150'"
                );
            } else {
                echo Search::showItem($p['output_type'], $second_column, $item_num, $p['row_num'], $align_desc . " width='200'");

                $takeintoaccountdelay_column = "";
                // Show only for tickets taken into account
                if ($item->fields['takeintoaccount_delay_stat'] > 0) {
                    $takeintoaccountdelay_column = Html::timestampToString($item->fields['takeintoaccount_delay_stat']);
                }
                echo Search::showItem($p['output_type'], $takeintoaccountdelay_column, $item_num, $p['row_num'], $align_desc . " width='150'");

                $solvedelay_column = "";
                // Show only for solved tickets
                if ($item->fields['solve_delay_stat'] > 0) {
                    $solvedelay_column = Html::timestampToString($item->fields['solve_delay_stat']);
                }
                echo Search::showItem($p['output_type'], $solvedelay_column, $item_num, $p['row_num'], $align_desc . " width='150'");

                $waiting_duration_column = Html::timestampToString($item->fields['waiting_duration']);
                echo Search::showItem($p['output_type'], $waiting_duration_column, $item_num, $p['row_num'], $align_desc . " width='150'");
            }

            // Finish Line
            echo Search::showEndLine($p['output_type']);
        } else {
            echo "<tr class='tab_bg_2'>";
            echo "<td colspan='6' ><i>" . __('No item in progress.') . "</i></td></tr>";
        }
    }

    /**
     * @param integer $output_type Output type
     * @param string  $mass_id     id of the form to check all
     */
    public static function commonListHeader(
        $output_type = Search::HTML_OUTPUT,
        $mass_id = '',
        array $params = []
    ) {
        $ticket_stats = $params['ticket_stats'] ?? false;

        // New Line for Header Items Line
        echo Search::showNewLine($output_type);
        // $show_sort if
        $header_num                      = 1;

        $items                           = [];
        $items[(empty($mass_id) ? '&nbsp' : Html::getCheckAllAsCheckbox($mass_id))] = '';

        $items[__('ID')]           = "id";
        $items[__('Title')]        = "name";
        $items[__('Status')]             = "status";
        $items[_n('Date', 'Dates', 1)]               = "date";
        $items[__('Last update')]        = "date_mod";

        if (count($_SESSION["glpiactiveentities"]) > 1) {
            $items[Entity::getTypeName(Session::getPluralNumber())] = "glpi_entities.completename";
        }

        $items[__('Priority')]           = "priority";
        $items[_n('Requester', 'Requesters', 1)]          = "users_id";
        $items[__('Assigned')]           = "users_id_assign";

        if (!$ticket_stats) {
            if (static::getType() == 'Ticket') {
                $items[_n('Associated element', 'Associated elements', Session::getPluralNumber())] = "";
            }
            $items[_n('Category', 'Categories', 1)]           = "glpi_itilcategories.completename";
            $items[__('Planification')]      = "glpi_tickettasks.begin";
        } else {
            $items[__('Take into account')] = "takeintoaccount_delay_stat";
            $items[__('Resolution')]        = "solve_delay_stat";
            $items[__('Pending')]           = "waiting_duration";
        }

        foreach (array_keys($items) as $key) {
            $link   = "";
            echo Search::showHeaderItem($output_type, $key, $header_num, $link);
        }

        // End Line for column headers
        echo Search::showEndLine($output_type);
    }


    /**
     * Get correct Calendar: Entity or Sla
     *
     * @since 0.90.4
     *
     **/
    public function getCalendar()
    {
        return Entity::getUsedConfig(
            'calendars_strategy',
            $this->fields['entities_id'],
            'calendars_id',
            0
        );
    }


    /**
     * Summary of getTimelinePosition
     * Returns the position of the $sub_type for the $user_id in the timeline
     *
     * @param int $items_id is the id of the ITIL object
     * @param string $sub_type is ITILFollowup, Document_Item, TicketTask, TicketValidation or Solution
     * @param int $users_id
     * @since 9.2
     */
    public static function getTimelinePosition($items_id, $sub_type, $users_id)
    {
        $itilobject = new static();
        $itilobject->fields['id'] = $items_id;
        $actors = $itilobject->getITILActors();

        // 1) rule for followups, documents, tasks and validations:
        //    Matrix for position of timeline objects
        //    R O A (R=Requester, O=Observer, A=AssignedTo)
        //    0 0 0 -> depending on the interface: central -> right, helpdesk -> left
        //    0 0 1 -> Right
        //    0 1 0 -> Left
        //    0 1 1 -> R
        //    1 0 0 -> L
        //    1 0 1 -> L
        //    1 1 0 -> L
        //    1 1 1 -> L
        //    if users_id is not in the actor list, then pos is left
        // 2) rule for solutions: always on the right side

        // default position is left
        $pos = self::TIMELINE_LEFT;

        $pos_matrix = [];
        $pos_matrix[0][0][0] = Session::getCurrentInterface() == "central"
            ? self::TIMELINE_RIGHT
            : self::TIMELINE_LEFT;
        $pos_matrix[0][0][1] = self::TIMELINE_RIGHT;
        $pos_matrix[0][1][1] = self::TIMELINE_RIGHT;

        switch ($sub_type) {
            case 'ITILFollowup':
            case 'Document_Item':
            case static::class . 'Task':
            case static::class . 'Validation':
                if (isset($actors[$users_id])) {
                    $r = in_array(CommonITILActor::REQUESTER, $actors[$users_id]) ? 1 : 0;
                    $o = in_array(CommonITILActor::OBSERVER, $actors[$users_id]) ? 1 : 0;
                    $a = in_array(CommonITILActor::ASSIGN, $actors[$users_id]) ? 1 : 0;
                    if (isset($pos_matrix[$r][$o][$a])) {
                        $pos = $pos_matrix[$r][$o][$a];
                    }
                }
                break;
            case 'Solution': // FIXME Remove it in GLPI 10.1, it may be still used in some edge cases in GLPI 10.0
            case ITILSolution::class:
                $pos = self::TIMELINE_RIGHT;
                break;
        }

        return $pos;
    }


    public function getTimelineItemtypes(): array
    {
        /** @var array $PLUGIN_HOOKS */
        global $PLUGIN_HOOKS;

        $obj_type = static::getType();
        $foreign_key = static::getForeignKeyField();

        //check sub-items rights
        $tmp = [$foreign_key => $this->getID()];
        $fup = new ITILFollowup();
        $fup->getEmpty();
        $fup->fields['itemtype'] = $obj_type;
        $fup->fields['items_id'] = $this->getID();

        $task_class = $obj_type . "Task";
        $task = new $task_class();

        $solved_statuses = static::getSolvedStatusArray();
        $closed_statuses = static::getClosedStatusArray();
        $solved_closed_statuses = array_merge($solved_statuses, $closed_statuses);

        $canadd_fup = $fup->can(-1, CREATE, $tmp) && !in_array($this->fields["status"], $solved_closed_statuses, true) || isset($_GET['_openfollowup']);
        $canadd_task = $task->can(-1, CREATE, $tmp) && !in_array($this->fields["status"], $solved_closed_statuses, true);
        $canadd_document = $canadd_fup || ($this->canAddItem('Document') && !in_array($this->fields["status"], $solved_closed_statuses, true));
        $canadd_solution = $obj_type::canUpdate() && $this->canSolve() && !in_array($this->fields["status"], $solved_statuses, true);

        $validation = $this->getValidationClassInstance();
        $canadd_validation = $validation !== null
            && $validation->can(-1, CREATE, $tmp)
            && !in_array($this->fields["status"], $solved_closed_statuses, true);

        $itemtypes = [];

        $itemtypes['answer'] = [
            'type'          => 'ITILFollowup',
            'class'         => 'ITILFollowup',
            'icon'          => ITILFollowup::getIcon(),
            'label'         => _x('button', 'Answer'),
            'short_label'   => _x('button', 'Answer'),
            'template'      => 'components/itilobject/timeline/form_followup.html.twig',
            'item'          => $fup,
            'hide_in_menu'  => !$canadd_fup,
        ];
        $itemtypes['task'] = [
            'type'          => 'ITILTask',
            'class'         => $task_class,
            'icon'          => CommonITILTask::getIcon(),
            'label'         => _x('button', 'Create a task'),
            'short_label'   => _x('button', 'Task'),
            'template'      => 'components/itilobject/timeline/form_task.html.twig',
            'item'          => $task,
            'hide_in_menu'  => !$canadd_task,
        ];
        $itemtypes['solution'] = [
            'type'          => 'ITILSolution',
            'class'         => 'ITILSolution',
            'icon'          => ITILSolution::getIcon(),
            'label'         => _x('button', 'Add a solution'),
            'short_label'   => _x('button', 'Solution'),
            'template'      => 'components/itilobject/timeline/form_solution.html.twig',
            'item'          => new ITILSolution(),
            'hide_in_menu'  => !$canadd_solution,
        ];
        $itemtypes['document'] = [
            'type'          => 'Document_Item',
            'class'         => Document_Item::class,
            'icon'          => Document_Item::getIcon(),
            'label'         => _x('button', 'Add a document'),
            'short_label'   => _x('button', 'Document'),
            'template'      => 'components/itilobject/timeline/form_document_item.html.twig',
            'item'          => new Document_Item(),
            'hide_in_menu'  => !$canadd_document,
        ];
        if ($validation !== null) {
            $itemtypes['validation'] = [
                'type'          => 'ITILValidation',
                'class'         => $validation::getType(),
                'icon'          => CommonITILValidation::getIcon(),
                'label'         => _x('button', 'Ask for validation'),
                'short_label'   => _x('button', 'Validation'),
                'template'      => 'components/itilobject/timeline/form_validation.html.twig',
                'item'          => $validation,
                'hide_in_menu'  => !$canadd_validation,
            ];
        }

        if (isset($PLUGIN_HOOKS[Hooks::TIMELINE_ANSWER_ACTIONS])) {
            /**
             * @var string $plugin
             * @var array|callable $hook_callable
             */
            foreach ($PLUGIN_HOOKS[Hooks::TIMELINE_ANSWER_ACTIONS] as $plugin => $hook_callable) {
                if (!Plugin::isPluginActive($plugin)) {
                    continue;
                }
                if (is_callable($hook_callable)) {
                    $hook_itemtypes = $hook_callable(['item' => $this]);
                } else {
                    $hook_itemtypes = $hook_callable;
                }
                if (is_array($hook_itemtypes)) {
                    $itemtypes = array_merge($itemtypes, $hook_itemtypes);
                } else {
                    trigger_error(
                        sprintf('"%s" hook callback result should be an array, "%s" returned.', Hooks::TIMELINE_ANSWER_ACTIONS, gettype($hook_itemtypes)),
                        E_USER_WARNING
                    );
                }
            }
        }

        return $itemtypes;
    }

    /**
     * Get an HTML string of all timeline actions/buttons provided by plugins via the {@link Hooks::TIMELINE_ACTIONS}  hook.
     * @return string
     * @since 10.0.0
     */
    public function getLegacyTimelineActionsHTML(): string
    {
        /** @var array $PLUGIN_HOOKS */
        global $PLUGIN_HOOKS;

        $legacy_actions = '';

        ob_start();
        Plugin::doHook(Hooks::TIMELINE_ACTIONS, [
            'rand'   => mt_rand(),
            'item'   => $this,
        ]);
        $legacy_actions .= ob_get_clean() ?? '';

        return $legacy_actions;
    }

    /**
     * Retrieves all timeline items for this ITILObject
     *
     * @param array $options Possible options:
     * - with_documents     : include documents elements
     * - with_logs          : include log entries
     * - with_validations   : include validation elements
     * - sort_by_date_desc  : sort timeline items by date
     * - check_view_rights  : indicates whether current session rights should be checked for view rights
     * - hide_private_items : force hiding private items (followup/tasks), even if session allow it
     * @since 9.4.0
     *
     * @return mixed[] Timeline items
     */
    public function getTimelineItems(array $options = [])
    {

        $params = [
            'with_documents'     => true,
            'with_logs'          => true,
            'with_validations'   => true,
            'sort_by_date_desc'  => $_SESSION['glpitimeline_order'] == CommonITILObject::TIMELINE_ORDER_REVERSE,

            // params used by notifications process (as session cannot be used there)
            'check_view_rights'  => true,
            'hide_private_items' => false,
        ];

        if (array_key_exists('bypass_rights', $options) && $options['bypass_rights']) {
            Toolbox::deprecated('Using `bypass_rights` parameter is deprecated.');
            $params['check_view_rights'] = false;
        }
        if (array_key_exists('expose_private', $options) && $options['expose_private']) {
            Toolbox::deprecated('Using `expose_private` parameter is deprecated.');
            $params['hide_private_items'] = false;
        }
        if (array_key_exists('is_self_service', $options) && $options['is_self_service']) {
            Toolbox::deprecated('Using `is_self_service` parameter is deprecated.');
            $params['hide_private_items'] = false;
        }

        if (is_array($options) && count($options)) {
            foreach ($options as $key => $val) {
                $params[$key] = $val;
            }
        }

        if ($this->isNewItem()) {
            return [];
        }

        if ($params['check_view_rights'] && !$this->canViewItem()) {
            return [];
        }

        $objType    = static::getType();
        $foreignKey = static::getForeignKeyField();
        $timeline = [];

        $canupdate_parent = $this->canUpdateItem() && !in_array($this->fields['status'], $this->getClosedStatusArray());

        //checks rights
        $restrict_fup = $restrict_task = [];
        if (
            $params['hide_private_items']
            || ($params['check_view_rights'] && !Session::haveRight("followup", ITILFollowup::SEEPRIVATE))
        ) {
            if (!$params['check_view_rights']) {
                // notification case, we cannot rely on session
                $restrict_fup = [
                    'is_private' => 0,
                ];
            } else {
                $restrict_fup = [
                    'OR' => [
                        'is_private' => 0,
                        'users_id'   => Session::getCurrentInterface() === "central" ? (int) Session::getLoginUserID() : 0,
                    ],
                ];
            }
        }

        $restrict_fup['itemtype'] = static::getType();
        $restrict_fup['items_id'] = $this->getID();

        $taskClass = $objType . "Task";
        $task_obj  = new $taskClass();
        if (
            $task_obj->maybePrivate()
            && (
                $params['hide_private_items']
                || ($params['check_view_rights'] && !Session::haveRight($task_obj::$rightname, CommonITILTask::SEEPRIVATE))
            )
        ) {
            if (!$params['check_view_rights']) {
                // notification case, we cannot rely on session
                $restrict_task = [
                    'is_private' => 0,
                ];
            } else {
                $restrict_task = [
                    'OR' => [
                        'is_private' => 0,
                        'users_id'   => Session::getCurrentInterface() === "central" ? (int) Session::getLoginUserID() : 0,
                        'users_id_tech' => Session::getCurrentInterface() === "central" ? (int) Session::getLoginUserID() : 0,
                    ],
                ];
            }
        }

        // Add followups to timeline
        $followup_obj = new ITILFollowup();
        if (!$params['check_view_rights'] || $followup_obj->canview()) {
            $followups = $followup_obj->find(
                ['items_id'  => $this->getID()] + $restrict_fup,
                ['date_creation DESC', 'id DESC']
            );

            foreach ($followups as $followups_id => $followup_row) {
                // Safer to use a clean object to load our data
                $followup = new ITILFollowup();
                $followup->setParentItem($this);
                $followup->fields = $followup_row;
                $followup->post_getFromDB();

                if (!$params['check_view_rights'] || $followup->canViewItem()) {
                    $followup_row['can_edit'] = $followup->canUpdateItem();
                    $followup_row['can_promote'] =
                        Session::getCurrentInterface() === 'central'
                        && $this instanceof Ticket
                        && Ticket::canCreate()
                    ;
                    $timeline["ITILFollowup_" . $followups_id] = [
                        'type'     => ITILFollowup::class,
                        'item'     => $followup_row,
                        'object'   => $followup,
                        'itiltype' => 'Followup',
                    ];
                }
            }
        }

        // Add tasks to timeline
        if (!$params['check_view_rights'] || $task_obj->canview()) {
            $tasks = $task_obj->find(
                [$foreignKey => $this->getID()] + $restrict_task,
                'date_creation DESC'
            );

            foreach ($tasks as $tasks_id => $task_row) {
                // Safer to use a clean object to load our data
                $task = new $taskClass();
                $task->fields = $task_row;
                $task->post_getFromDB();

                if (!$params['check_view_rights'] || $task->canViewItem()) {
                    $task_row['can_edit'] = $task->canUpdateItem();
                    $task_row['can_promote'] =
                        Session::getCurrentInterface() === 'central'
                        && $this instanceof Ticket
                        && Ticket::canCreate()
                    ;
                    $timeline[$task::getType() . "_" . $tasks_id] = [
                        'type'     => $taskClass,
                        'item'     => $task_row,
                        'object'   => $task,
                        'itiltype' => 'Task',
                    ];
                }
            }
        }

        // Add solutions to timeline
        $solution_obj   = new ITILSolution();
        $solution_items = $solution_obj->find([
            'itemtype'  => static::getType(),
            'items_id'  => $this->getID(),
        ]);

        foreach ($solution_items as $solution_item) {
            // Safer to use a clean object to load our data
            $solution = new ITILSolution();
            $solution->setParentItem($this);
            $solution->fields = $solution_item;
            $solution->post_getFromDB();

            $timeline["ITILSolution_" . $solution_item['id'] ] = [
                'type'     => ITILSolution::class,
                'itiltype' => 'Solution',
                'item'     => [
                    'id'                 => $solution_item['id'],
                    'content'            => $solution_item['content'],
                    'date'               => $solution_item['date_creation'],
                    'users_id'           => $solution_item['users_id'],
                    'solutiontypes_id'   => $solution_item['solutiontypes_id'],
                    'can_edit'           => $objType::canUpdate() && $this->canSolve(),
                    'timeline_position'  => self::TIMELINE_RIGHT,
                    'users_id_editor'    => $solution_item['users_id_editor'],
                    'date_creation'      => $solution_item['date_creation'],
                    'date_mod'           => $solution_item['date_mod'],
                    'users_id_approval'  => $solution_item['users_id_approval'],
                    'date_approval'      => $solution_item['date_approval'],
                    'status'             => $solution_item['status'],
                ],
                'object' => $solution,
            ];
        }

        // Add validation to timeline
        $validation_class = $objType . "Validation";
        if (
            class_exists($validation_class) && $params['with_validations']
            && (!$params['check_view_rights'] || $validation_class::canView())
        ) {
            $valitation_obj = new $validation_class();
            $validations = $valitation_obj->find([
                $foreignKey => $this->getID(),
            ]);

            foreach ($validations as $validations_id => $validation_row) {
                // Safer to use a clean object to load our data
                $validation = new $validation_class();
                $validation->fields = $validation_row;
                $validation->post_getFromDB();

                $canedit = $valitation_obj->can($validations_id, UPDATE);
                $cananswer = ($validation_row['users_id_validate'] === Session::getLoginUserID() &&
                $validation_row['status'] == CommonITILValidation::WAITING &&
                !in_array($this->fields['status'], $this->getClosedStatusArray()));
                $user = new User();
                $user->getFromDB($validation_row['users_id_validate']);

                $request_key = $valitation_obj::getType() . '_' . $validations_id
                    . (empty($validation_row['validation_date']) ? '' : '_request'); // If no answer, no suffix to see attached documents on request
                $timeline[$request_key] = [
                    'type' => $validation_class,
                    'item' => [
                        'id'        => $validations_id,
                        'date'      => $validation_row['submission_date'],
                        'content'   => __('Validation request') . " <i class='ti ti-arrow-right'></i><i class='ti ti-user text-muted me-1'></i>" . $user->getlink(),
                        'comment_submission' => $validation_row['comment_submission'],
                        'users_id'  => $validation_row['users_id'],
                        'can_edit'  => $canedit,
                        'can_answer'   => $cananswer,
                        'users_id_validate'  => $validation_row['users_id_validate'],
                        'timeline_position' => $validation_row['timeline_position'],
                    ],
                    'itiltype' => 'Validation',
                    'class'    => 'validation-request ' .
                    ($validation_row['status'] == CommonITILValidation::WAITING ? "validation-waiting" : "") .
                    ($validation_row['status'] == CommonITILValidation::ACCEPTED ? "validation-accepted" : "") .
                    ($validation_row['status'] == CommonITILValidation::REFUSED ? "validation-refused" : ""),
                    'item_action' => 'validation-request',
                    'object'      => $validation,
                ];

                if (!empty($validation_row['validation_date'])) {
                    $timeline[$valitation_obj::getType() . "_" . $validations_id] = [
                        'type' => $validation_class,
                        'item' => [
                            'id'        => $validations_id,
                            'date'      => $validation_row['validation_date'],
                            'content'   => __('Validation request answer') . " : " .
                            _sx('status', ucfirst($validation_class::getStatus($validation_row['status']))),
                            'comment_validation' => $validation_row['comment_validation'],
                            'users_id'  => $validation_row['users_id_validate'],
                            'status'    => "status_" . $validation_row['status'],
                            'can_edit'  => $validation_row['users_id_validate'] === Session::getLoginUserID(),
                            'timeline_position' => $validation_row['timeline_position'],
                        ],
                        'class'       => 'validation-answer',
                        'itiltype'    => 'Validation',
                        'item_action' => 'validation-answer',
                        'object'      => $validation,
                    ];
                }
            }
        }

        // Add documents to timeline
        if ($params['with_documents']) {
            $document_item_obj = new Document_Item();
            $document_obj      = new Document();
            $document_items    = $document_item_obj->find([
                $this->getAssociatedDocumentsCriteria(!$params['check_view_rights']),
                'timeline_position'  => ['>', self::NO_TIMELINE],
            ]);
            foreach ($document_items as $document_item) {
                if (!$document_obj->getFromDB($document_item['documents_id'])) {
                    // Orphan `Document_Item`
                    continue;
                }

                $item = $document_obj->fields;
                $item['date'] = $document_item['date'] ?? $document_item['date_creation'];
                // #1476 - set date_creation, date_mod and owner to attachment ones
                $item['date_creation'] = $document_item['date_creation'];
                $item['date_mod'] = $document_item['date_mod'];
                $item['users_id'] = $document_item['users_id'];
                $item['documents_item_id'] = $document_item['id'];

                $item['timeline_position'] = $document_item['timeline_position'];
                $item['_can_edit'] = Document::canUpdate() && $document_obj->canUpdateItem();
                $item['_can_delete'] = Document::canDelete() && $document_obj->canDeleteItem() && $canupdate_parent;

                $timeline_key = $document_item['itemtype'] . "_" . $document_item['items_id'];
                if ($document_item['itemtype'] == static::getType()) {
                    // document associated directly to itilobject
                    $timeline["Document_" . $document_item['documents_id']] = [
                        'type' => 'Document_Item',
                        'item' => $item,
                        'object' => $document_obj,
                    ];
                } elseif (isset($timeline[$timeline_key])) {
                    // document associated to a sub item of itilobject
                    if (!isset($timeline[$timeline_key]['documents'])) {
                        $timeline[$timeline_key]['documents'] = [];
                    }

                    $docpath = GLPI_DOC_DIR . "/" . $item['filepath'];
                    $is_image = Document::isImage($docpath);
                    $sub_document = [
                        'type' => 'Document_Item',
                        'item' => $item,
                    ];
                    if ($is_image) {
                        $sub_document['_is_image'] = true;
                        $sub_document['_size'] = getimagesize($docpath);
                    }
                    $timeline[$timeline_key]['documents'][] = $sub_document;
                }
            }
        }

        // Add logs to timeline
        if ($params['with_logs'] && Session::getCurrentInterface() == "central") {
            //add logs to timeline
            $log_items = Log::getHistoryData($this, 0, 0, [
                'OR' => [
                    'id_search_option' => ['>', 0],
                    'itemtype_link'    => ['User', 'Group', 'Supplier'],
                ],
            ]);

            foreach ($log_items as $log_row) {
                // Safer to use a clean object to load our data
                $log = new Log();
                $log->fields = $log_row;
                $log->post_getFromDB();

                $content = $log_row['change'];
                if (strlen($log_row['field']) > 0) {
                    $content = sprintf(__("%s: %s"), $log_row['field'], $content);
                }
                $content = "<i class='fas fa-history me-1' title='" . __("Log entry") . "' data-bs-toggle='tooltip'></i>" . $content;
                $timeline["Log_" . $log_row['id'] ] = [
                    'type'     => 'Log',
                    'class'    => 'text-muted d-none',
                    'item'     => [
                        'id'                 => $log_row['id'],
                        'content'            => $content,
                        'date'               => $log_row['date_mod'],
                        'users_id'           => 0,
                        'can_edit'           => false,
                        'timeline_position'  => self::TIMELINE_LEFT,
                    ],
                    'object' => $log,
                ];
            }
        }

        Plugin::doHook(Hooks::SHOW_IN_TIMELINE, ['item' => $this, 'timeline' => &$timeline]);

        //sort timeline items by date. If items have the same date, sort by id
        $reverse = $params['sort_by_date_desc'];
        usort($timeline, function ($a, $b) use ($reverse) {
            $date_a = $a['item']['date_creation'] ?? $a['item']['date'];
            $date_b = $b['item']['date_creation'] ?? $b['item']['date'];
            $diff = strtotime($date_a) - strtotime($date_b);
            if ($diff === 0) {
                $diff = $a['item']['id'] - $b['item']['id'];
            }
            return $reverse ? 0 - $diff : $diff;
        });

        return $timeline;
    }


    /**
     * @since 9.4.0
     *
     * @param CommonDBTM $item The item whose form should be shown
     * @param integer $id ID of the item
     * @param mixed[] $params Array of extra parameters
     *
     * @return void
     */
    public static function showSubForm(CommonDBTM $item, $id, $params)
    {

        if ($item instanceof Document_Item) {
            Document_Item::showAddFormForItem($params['parent'], '');
        } elseif ($item->getType() == $params['parent']->getType()) {
            return self::showEditDescriptionForm($params['parent']);
        } elseif (
            method_exists($item, "showForm")
                 && $item->can(-1, CREATE, $params)
        ) {
            $item->showForm($id, $params);
        }
    }

    public static function showEditDescriptionForm(CommonITILObject $item)
    {
        $can_requester = true;
        if (method_exists($item, "canRequesterUpdateItem")) {
            $can_requester = $item->canRequesterUpdateItem();
        }
        TemplateRenderer::getInstance()->display('components/itilobject/timeline/simple_form.html.twig', [
            'item'          => $item,
            'canupdate'     => (Session::getCurrentInterface() == "central" && $item->canUpdateItem()),
            'can_requester' => $can_requester,
        ]);
    }

    /**
     * Summary of getITILActors
     * Get the list of actors for the current Change
     * will return an assoc array of users_id => array of roles.
     *
     * @since 9.4.0
     *
     * @return array[] of array[] of users and roles
     */
    public function getITILActors()
    {
        /** @var \DBmysql $DB */
        global $DB;

        $users_table = $this->getTable() . '_users';
        switch ($this->getType()) {
            case 'Ticket':
                $groups_table = 'glpi_groups_tickets';
                break;
            case 'Problem':
                $groups_table = 'glpi_groups_problems';
                break;
            default:
                $groups_table = $this->getTable() . '_groups';
                break;
        }
        $fk = $this->getForeignKeyField();

        $subquery1 = new \QuerySubQuery([
            'SELECT'    => [
                'usr.id AS users_id',
                'tu.type AS type',
            ],
            'FROM'      => "$users_table AS tu",
            'LEFT JOIN' => [
                User::getTable() . ' AS usr' => [
                    'ON' => [
                        'tu'  => 'users_id',
                        'usr' => 'id',
                    ],
                ],
            ],
            'WHERE'     => [
                "tu.$fk" => $this->getID(),
            ],
        ]);

        $subquery2 = new \QuerySubQuery([
            'SELECT'    => [
                'usr.id AS users_id',
                'gt.type AS type',
            ],
            'FROM'      => "$groups_table AS gt",
            'LEFT JOIN' => [
                Group_User::getTable() . ' AS gu'   => [
                    'ON' => [
                        'gu'  => 'groups_id',
                        'gt'  => 'groups_id',
                    ],
                ],
                User::getTable() . ' AS usr'        => [
                    'ON' => [
                        'gu'  => 'users_id',
                        'usr' => 'id',
                    ],
                ],
            ],
            'WHERE'     => [
                "gt.$fk" => $this->getID(),
            ],
        ]);

        $union = new \QueryUnion([$subquery1, $subquery2], false, 'allactors');
        $iterator = $DB->request([
            'SELECT'          => [
                'users_id',
                'type',
            ],
            'DISTINCT'        => true,
            'FROM'            => $union,
        ]);

        $users_keys = [];
        foreach ($iterator as $current_tu) {
            $users_keys[$current_tu['users_id']][] = $current_tu['type'];
        }

        return $users_keys;
    }


    /**
     * Number of followups of the object
     *
     * @param boolean $with_private true : all followups / false : only public ones (default 1)
     *
     * @return integer followup count
     **/
    public function numberOfFollowups($with_private = true)
    {
        /** @var \DBmysql $DB */
        global $DB;

        $RESTRICT = [];
        if ($with_private !== true) {
            $RESTRICT['is_private'] = 0;
        }

        // Set number of followups
        $result = $DB->request([
            'COUNT'  => 'cpt',
            'FROM'   => 'glpi_itilfollowups',
            'WHERE'  => [
                'itemtype'  => $this->getType(),
                'items_id'  => $this->fields['id'],
            ] + $RESTRICT,
        ])->current();

        return $result['cpt'];
    }

    /**
     * Number of tasks of the object
     *
     * @param boolean $with_private true : all followups / false : only public ones (default 1)
     *
     * @return integer
     **/
    public function numberOfTasks($with_private = true)
    {
        /** @var \DBmysql $DB */
        global $DB;

        $table = 'glpi_' . strtolower($this->getType()) . 'tasks';

        $RESTRICT = [];
        if ($with_private !== true && $this->getType() == 'Ticket') {
            //No private tasks for Problems and Changes
            $RESTRICT['is_private'] = 0;
        }

        // Set number of tasks
        $row = $DB->request([
            'COUNT'  => 'cpt',
            'FROM'   => $table,
            'WHERE'  => [
                $this->getForeignKeyField()   => $this->fields['id'],
            ] + $RESTRICT,
        ])->current();
        return (int) $row['cpt'];
    }

    /**
     * Check if input contains a flag set to prevent 'takeintoaccount' delay computation.
     *
     * @param array $input
     *
     * @return boolean
     */
    public function isTakeIntoAccountComputationBlocked($input)
    {
        return array_key_exists('_do_not_compute_takeintoaccount', $input)
         && $input['_do_not_compute_takeintoaccount'];
    }


    /**
     * Check if input contains a flag set to prevent status computation.
     *
     * @param array $input
     *
     * @return boolean
     */
    public function isStatusComputationBlocked(array $input)
    {
        return array_key_exists('_do_not_compute_status', $input)
         && $input['_do_not_compute_status'];
    }


    public function addDefaultFormTab(array &$ong)
    {

        $timeline    = $this->getTimelineItems(['with_logs' => false]);
        $nb_elements = count($timeline);
        $label = $this->getTypeName(1);
        if ($nb_elements > 0) {
            $label .= " <span class='badge'>$nb_elements</span>";
        }

        $ong[$this->getType() . '$main'] = $label;
        return $this;
    }

    public static function getAdditionalMenuOptions()
    {
        $tplclass = self::getTemplateClass();
        if ($tplclass::canView()) {
            $menu = [
                $tplclass => [
                    'title' => $tplclass::getTypeName(Session::getPluralNumber()),
                    'page'  => $tplclass::getSearchURL(false),
                    'icon'  => $tplclass::getIcon(),
                    'links' => [
                        'search' => $tplclass::getSearchURL(false),
                    ],
                ],
            ];

            if ($tplclass::canCreate()) {
                $menu[$tplclass]['links']['add'] = $tplclass::getFormURL(false);
            }
            return $menu;
        }
        return false;
    }


    /**
     * @see CommonGLPI::getAdditionalMenuLinks()
     *
     * @since 9.5.0
     **/
    public static function getAdditionalMenuLinks()
    {
        $links = [];
        $tplclass = self::getTemplateClass();
        if ($tplclass::canView()) {
            $links['template'] = $tplclass::getSearchURL(false);
        }
        $links['summary_kanban'] = static::getFormURL(false) . '?showglobalkanban=1';

        return $links;
    }


    /**
     * Get template to use
     * Use force_template first, then try on template define for type and category
     * then use default template of active profile of connected user and then use default entity one
     *
     * @param integer      $force_template     itiltemplate_id to use (case of preview for example)
     * @param integer|null $type               type of the ticket
     *                                         (use Ticket::INCIDENT_TYPE or Ticket::DEMAND_TYPE constants value)
     * @param integer      $itilcategories_id  ticket category
     * @param integer      $entities_id
     *
     * @return ITILTemplate
     *
     * @since 9.5.0
     **/
    public function getITILTemplateToUse(
        $force_template = 0,
        $type = null,
        $itilcategories_id = 0,
        $entities_id = -1
    ) {
        if (!$type && $this->getType() != Ticket::getType()) {
            $type = true;
        }
        // Load template if available :
        $tplclass = static::getTemplateClass();
        $tt              = new $tplclass();
        $template_loaded = false;

        if ($force_template) {
            // with type and categ
            if ($tt->getFromDBWithData($force_template, true)) {
                $template_loaded = true;
            }
        }

        if (
            !$template_loaded
            && $type
            && $itilcategories_id
        ) {
            $categ = new ITILCategory();
            if ($categ->getFromDB($itilcategories_id)) {
                $field = $this->getTemplateFieldName($type);

                if (!empty($categ->fields[$field]) && $categ->fields[$field]) {
                    // without type and categ
                    if ($tt->getFromDBWithData($categ->fields[$field], false)) {
                        $template_loaded = true;
                    }
                }
            }
        }

        // If template loaded from type and category do not check after
        if ($template_loaded) {
            return $tt;
        }

        //Get template from profile
        if (!$template_loaded && $type) {
            $field = $this->getTemplateFieldName($type);
            $field = str_replace(['_incident', '_demand'], ['', ''], $field);
            // load default profile one if not already loaded
            if (
                isset($_SESSION['glpiactiveprofile'][$field])
                && $_SESSION['glpiactiveprofile'][$field]
            ) {
                // with type and categ
                if (
                    $tt->getFromDBWithData(
                        $_SESSION['glpiactiveprofile'][$field],
                        true
                    )
                ) {
                    $template_loaded = true;
                }
            }
        }

        //Get template from entity
        if (
            !$template_loaded
            && ($entities_id >= 0)
        ) {
            // load default entity one if not already loaded
            $template_id = Entity::getUsedConfig(
                strtolower($this->getType()) . 'templates_strategy',
                $entities_id,
                strtolower($this->getType()) . 'templates_id',
                0
            );
            if ($template_id > 0 && $tt->getFromDBWithData($template_id, true)) {
                $template_loaded = true;
            }
        }

        // Check if profile / entity set type and category and try to load template for these values
        if ($template_loaded) { // template loaded for profile or entity
            $newtype              = $type;
            $newitilcategories_id = $itilcategories_id;
            // Get predefined values for ticket template
            if (isset($tt->predefined['itilcategories_id']) && $tt->predefined['itilcategories_id']) {
                $newitilcategories_id = $tt->predefined['itilcategories_id'];
            }
            if (isset($tt->predefined['type']) && $tt->predefined['type']) {
                $newtype = $tt->predefined['type'];
            }
            if (
                $newtype != $type
                && $newitilcategories_id != $itilcategories_id
            ) {
                $categ = new ITILCategory();
                if ($categ->getFromDB($newitilcategories_id)) {
                    $field = $this->getTemplateFieldName($newtype);

                    if (isset($categ->fields[$field]) && $categ->fields[$field]) {
                        // without type and categ
                        if ($tt->getFromDBWithData($categ->fields[$field], false)) {
                            $template_loaded = true;
                        }
                    }
                }
            }
        }
        return $tt;
    }

    /**
     * Get template field name
     *
     * @param string $type Type, if any
     *
     * @return string
     */
    public function getTemplateFieldName($type = null): string
    {
        $field = strtolower(static::getType()) . 'templates_id';
        if (static::getType() === Ticket::getType()) {
            switch ($type) {
                case Ticket::INCIDENT_TYPE:
                    $field .= '_incident';
                    break;

                case Ticket::DEMAND_TYPE:
                    $field .= '_demand';
                    break;

                case true:
                    //for changes and problem, or from profiles
                    break;

                default:
                    $field = '';
                    trigger_error('Missing type for Ticket template!', E_USER_WARNING);
                    break;
            }
        }

        return $field;
    }

    /**
     * @since 9.5.0
     *
     * @param integer $entity entities_id usefull if function called by cron (default 0)
     **/
    abstract public static function getDefaultValues($entity = 0);

    /**
     * Get template class name.
     *
     * @since 9.5.0
     *
     * @return string
     */
    public static function getTemplateClass()
    {
        return static::getType() . 'Template';
    }

    /**
     * Get template form field name
     *
     * @since 9.5.0
     *
     * @return string
     */
    public static function getTemplateFormFieldName()
    {
        return '_' . strtolower(static::getType()) . 'template';
    }

    /**
     * Get common request criteria
     *
     * @since 9.5.0
     *
     * @return array
     */
    public static function getCommonCriteria()
    {
        $fk = self::getForeignKeyField();
        $gtable = str_replace('glpi_', 'glpi_groups_', static::getTable());
        $itable = str_replace('glpi_', 'glpi_items_', static::getTable());
        if (self::getType() == 'Change') {
            $gtable = 'glpi_changes_groups';
            $itable = 'glpi_changes_items';
        }
        $utable = static::getTable() . '_users';
        $stable = static::getTable() . '_suppliers';
        if (self::getType() == 'Ticket') {
            $stable = 'glpi_suppliers_tickets';
        }
        $table = static::getTable();
        $criteria = [
            'SELECT'          => [
                "$table.*",
                'glpi_itilcategories.completename AS catname',
            ],
            'DISTINCT'        => true,
            'FROM'            => $table,
            'LEFT JOIN'       => [
                $gtable  => [
                    'ON' => [
                        $table   => 'id',
                        $gtable  => $fk,
                    ],
                ],
                $utable  => [
                    'ON' => [
                        $table   => 'id',
                        $utable  => $fk,
                    ],
                ],
                $stable  => [
                    'ON' => [
                        $table   => 'id',
                        $stable  => $fk,
                    ],
                ],
                'glpi_itilcategories'      => [
                    'ON' => [
                        $table                  => 'itilcategories_id',
                        'glpi_itilcategories'   => 'id',
                    ],
                ],
                $itable  => [
                    'ON' => [
                        $table   => 'id',
                        $itable  => $fk,
                    ],
                ],
            ],
            'ORDERBY'            => "$table.date_mod DESC",
        ];
        if (count($_SESSION["glpiactiveentities"]) > 1) {
            $criteria['LEFT JOIN']['glpi_entities'] = [
                'ON' => [
                    'glpi_entities'   => 'id',
                    $table            => 'entities_id',
                ],
            ];
            $criteria['SELECT'] = array_merge(
                $criteria['SELECT'],
                [
                    'glpi_entities.completename AS entityname',
                    "$table.entities_id AS entityID",
                ]
            );
        }
        return $criteria;
    }

    public function getForbiddenSingleMassiveActions()
    {
        $excluded = parent::getForbiddenSingleMassiveActions();

        if (isset($this->fields['global_validation']) && $this->fields['global_validation'] != CommonITILValidation::NONE) {
            //a validation has already been requested/done
            $excluded[] = 'TicketValidation:submit_validation';
        }

        $excluded[] = '*:add_actor';
        $excluded[] = '*:add_task';
        $excluded[] = '*:add_followup';

        return $excluded;
    }

    /**
     * Returns criteria that can be used to get documents related to current instance.
     *
     * @return array
     */
    public function getAssociatedDocumentsCriteria($bypass_rights = false): array
    {
        $task_class = $this->getType() . 'Task';
        /** @var DBmysql $DB */
        global $DB; // Used to get subquery results - better performance

        $or_crits = [
            // documents associated to ITIL item directly
            [
                Document_Item::getTableField('itemtype') => $this->getType(),
                Document_Item::getTableField('items_id') => $this->getID(),
            ],
        ];

        // documents associated to followups
        if ($bypass_rights || ITILFollowup::canView()) {
            $fup_crits = [
                ITILFollowup::getTableField('itemtype') => $this->getType(),
                ITILFollowup::getTableField('items_id') => $this->getID(),
            ];
            if (!$bypass_rights && !Session::haveRight(ITILFollowup::$rightname, ITILFollowup::SEEPRIVATE)) {
                $fup_crits[] = [
                    'OR' => ['is_private' => 0, 'users_id' => Session::getLoginUserID()],
                ];
            }
            // Run the subquery separately. It's better for huge databases
            $iterator_tmp = $DB->request([
                'SELECT' => 'id',
                'FROM'   => ITILFollowup::getTable(),
                'WHERE'  => $fup_crits,
            ]);
            $arr_values = array_column(iterator_to_array($iterator_tmp, false), 'id');
            if (count($arr_values) > 0) {
                $or_crits[] = [
                    Document_Item::getTableField('itemtype') => ITILFollowup::getType(),
                    Document_Item::getTableField('items_id') => $arr_values,
                ];
            }
        }

        // documents associated to solutions
        if ($bypass_rights || ITILSolution::canView()) {
            // Run the subquery separately. It's better for huge databases
            $iterator_tmp = $DB->request([
                'SELECT' => 'id',
                'FROM'   => ITILSolution::getTable(),
                'WHERE'  => [
                    ITILSolution::getTableField('itemtype') => $this->getType(),
                    ITILSolution::getTableField('items_id') => $this->getID(),
                ],
            ]);
            $arr_values = array_column(iterator_to_array($iterator_tmp, false), 'id');
            if (count($arr_values) > 0) {
                $or_crits[] = [
                    Document_Item::getTableField('itemtype') => ITILSolution::getType(),
                    Document_Item::getTableField('items_id') => $arr_values,
                ];
            }
        }

        // documents associated to ticketvalidation
        $validation_class = static::getType() . 'Validation';
        if (class_exists($validation_class) && ($bypass_rights ||  $validation_class::canView())) {
            // Run the subquery separately. It's better for huge databases
            $iterator_tmp = $DB->request([
                'SELECT' => 'id',
                'FROM'   => $validation_class::getTable(),
                'WHERE'  => [
                    $validation_class::getTableField($validation_class::$items_id) => $this->getID(),
                ],
            ]);
            $arr_values = array_column(iterator_to_array($iterator_tmp, false), 'id');
            if (count($arr_values) > 0) {
                $or_crits[] = [
                    Document_Item::getTableField('itemtype') => $validation_class::getType(),
                    Document_Item::getTableField('items_id') => $arr_values,
                ];
            }
        }

        // documents associated to tasks
        if ($bypass_rights || $task_class::canView()) {
            $tasks_crit = [
                $this->getForeignKeyField() => $this->getID(),
            ];
            if (!$bypass_rights && !Session::haveRight($task_class::$rightname, CommonITILTask::SEEPRIVATE)) {
                $tasks_crit[] = [
                    'OR' => ['is_private' => 0, 'users_id' => Session::getLoginUserID()],
                ];
            }
            // Run the subquery separately. It's better for huge databases
            $iterator_tmp = $DB->request([
                'SELECT' => 'id',
                'FROM'   => $task_class::getTable(),
                'WHERE'  => $tasks_crit,
            ]);
            $arr_values = array_column(iterator_to_array($iterator_tmp, false), 'id');
            if (count($arr_values) > 0) {
                $or_crits[] = [
                    'glpi_documents_items.itemtype' => $task_class::getType(),
                    'glpi_documents_items.items_id' => $arr_values,
                ];
            }
        }
        return ['OR' => $or_crits];
    }

    /**
     * Check if this item is new
     *
     * @return bool
     */
    protected function isNew()
    {
        if (isset($this->input['status'])) {
            $status = $this->input['status'];
        } elseif (isset($this->fields['status'])) {
            $status = $this->fields['status'];
        } else {
            throw new \LogicException("Can't get status value: no object loaded");
        }

        return $status == CommonITILObject::INCOMING;
    }

    /**
     * Retrieve linked items table name
     *
     * @since 9.5.0
     *
     * @return string
     */
    public static function getItemsTable()
    {
        switch (static::getType()) {
            case 'Change':
                return 'glpi_changes_items';
            case 'Problem':
                return 'glpi_items_problems';
            case 'Ticket':
                return 'glpi_items_tickets';
            default:
                throw new \RuntimeException('Unknown ITIL type ' . static::getType());
        }
    }


    public function getLinkedItems(): array
    {
        /** @var \DBmysql $DB */
        global $DB;

        $assets = $DB->request([
            'SELECT' => ["itemtype", "items_id"],
            'FROM'   => static::getItemsTable(),
            'WHERE'  => [$this->getForeignKeyField() => $this->getID()],
        ]);

        $assets = iterator_to_array($assets);

        $tab = [];
        foreach ($assets as $asset) {
            if (!class_exists($asset['itemtype'])) {
                //ignore if class does not exists (maybe a plugin)
                continue;
            }
            $tab[$asset['itemtype']][$asset['items_id']] = $asset['items_id'];
        }

        return $tab;
    }

    /**
     * Should impact tab be displayed? Check if there is a valid linked item
     *
     * @return boolean
     */
    protected function hasImpactTab()
    {
        foreach ($this->getLinkedItems() as $itemtype => $items) {
            $class = $itemtype;
            if (Impact::isEnabled($class) && Session::getCurrentInterface() === "central") {
                return true;
            }
        }
        return false;
    }

    /**
     * Get criteria needed to match objets with an "open" status (= not resolved
     * or closed)
     *
     * @return array
     */
    public static function getOpenCriteria(): array
    {
        $table = static::getTable();

        return [
            'NOT' => [
                "$table.status" => array_merge(
                    static::getSolvedStatusArray(),
                    static::getClosedStatusArray()
                ),
            ],
        ];
    }

    public function handleItemsIdInput(): void
    {
        if (!empty($this->input['items_id'])) {
            $item_link_class = static::getItemLinkClass();
            $item_link = new $item_link_class();
            foreach ($this->input['items_id'] as $itemtype => $items) {
                foreach ($items as $items_id) {
                    $item_link->add([
                        'items_id'                    => $items_id,
                        'itemtype'                    => $itemtype,
                        static::getForeignKeyField()  => $this->fields['id'],
                        '_disablenotif'               => true,
                    ]);
                }
            }
        }
    }

    abstract public static function getItemLinkClass(): string;
    /**
     * Handle "_tasktemplates_id" special input
     */
    public function handleTaskTemplateInput()
    {
        // Check input is valid
        if (
            !isset($this->input['_tasktemplates_id'])
            || !is_array($this->input['_tasktemplates_id'])
            || !count($this->input['_tasktemplates_id'])
        ) {
            return;
        }

        // Add tasks in tasktemplates if defined in itiltemplate
        $itiltask_class = $this->getType() . 'Task';
        $itiltask   = new $itiltask_class();
        foreach ($this->input['_tasktemplates_id'] as $tasktemplates_id) {
            $itiltask->add([
                '_tasktemplates_id'           => $tasktemplates_id,
                $this->getForeignKeyField()   => $this->fields['id'],
                'date'                        => $this->fields['date'],
                '_disablenotif'               => true,
            ]);
        }
    }

    /**
     * Handle "_itilfollowuptemplates_id" special input
     */
    public function handleITILFollowupTemplateInput(): void
    {
        // Check input is valid
        if (
            !isset($this->input['_itilfollowuptemplates_id'])
            || !is_array($this->input['_itilfollowuptemplates_id'])
            || !count($this->input['_itilfollowuptemplates_id'])
        ) {
            return;
        }

        // Add tasks in itilfollowup template if defined in itiltemplate
        foreach ($this->input['_itilfollowuptemplates_id'] as $fup_templates_id) {
            $values = [
                '_itilfollowuptemplates_id' => $fup_templates_id,
                'itemtype'                  => $this->getType(),
                'items_id'                  => $this->getID(),
                '_disablenotif'             => true,
                '_do_not_compute_status' => $this->input['_do_not_compute_status'] ?? 0,
                '_do_not_compute_takeintoaccount' => $this->input['_do_not_compute_takeintoaccount'] ?? 0,
            ];
            // Insert new followup from template
            $fup = new ITILFollowup();
            $fup->add($values);
        }
    }

    /**
     * Handle "_solutiontemplates_id" special input
     */
    public function handleSolutionTemplateInput(): void
    {
        // Check input is valid
        if (!isset($this->input['_solutiontemplates_id'])) {
            return;
        }

        $solution = new ITILSolution();
        $input = [
            '_solutiontemplates_id' => $this->input['_solutiontemplates_id'],
            'itemtype'              => $this->getType(),
            'items_id'              => $this->getID(),
        ];
        if (isset($this->input['_do_not_compute_status'])) {
            $input['_do_not_compute_status'] = $this->input['_do_not_compute_status'];
        }
        $solution->add($input);
    }

    /**
     * Handle notifications to be sent after item creation.
     *
     * @return void
     */
    public function handleNewItemNotifications(): void
    {
        /** @var array $CFG_GLPI */
        global $CFG_GLPI;

        if (!isset($this->input['_disablenotif']) && $CFG_GLPI['use_notifications']) {
            $this->getFromDB($this->fields['id']); // Reload item to get actual status

            NotificationEvent::raiseEvent('new', $this);

            $status = $this->fields['status'] ?? null;

            //Check if a waiting ITIL solution has been posted to avoid sending duplicate notifications.
            $has_waiting_solution = countElementsInTable(
                ITILSolution::getTable(),
                [
                    'itemtype' => Ticket::class,
                    'items_id' => $this->getID(),
                    'status'   => CommonITILValidation::WAITING,
                ]
            ) > 0;

            if (in_array($status, $this->getSolvedStatusArray()) && !$has_waiting_solution) {
                NotificationEvent::raiseEvent('solved', $this);
            }
            if (in_array($status, $this->getClosedStatusArray())) {
                NotificationEvent::raiseEvent('closed', $this);
            }
        }
    }

    /**
     * Manage Validation add from input (form and rules)
     *
     * @param array $input
     *
     * @return boolean
     */
    public function manageValidationAdd($input)
    {
        $validation = $this->getValidationClassInstance();

        if ($validation === null) {
            return true;
        }

        $self_fk = $this->getForeignKeyField();

        //Action for send_validation rule
        if (isset($input["_add_validation"])) {
            if (isset($input['entities_id'])) {
                $entid = $input['entities_id'];
            } elseif (isset($this->fields['entities_id'])) {
                $entid = $this->fields['entities_id'];
            } else {
                return false;
            }

            $validations_to_send = [];
            if (!is_array($input["_add_validation"])) {
                $input["_add_validation"] = [$input["_add_validation"]];
            }

            foreach ($input["_add_validation"] as $key => $value) {
                switch ($value) {
                    case 'requester_supervisor':
                        if (
                            isset($input['_groups_id_requester'])
                            && $input['_groups_id_requester']
                        ) {
                            $users = Group_User::getGroupUsers(
                                $input['_groups_id_requester'],
                                ['is_manager' => 1]
                            );
                            foreach ($users as $data) {
                                $validations_to_send[] = $data['id'];
                            }
                        }
                        // Add to already set groups
                        foreach ($this->getGroups(CommonITILActor::REQUESTER) as $d) {
                            $users = Group_User::getGroupUsers(
                                $d['groups_id'],
                                ['is_manager' => 1]
                            );
                            foreach ($users as $data) {
                                $validations_to_send[] = $data['id'];
                            }
                        }
                        break;

                    case 'assign_supervisor':
                        if (
                            isset($input['_groups_id_assign'])
                            && $input['_groups_id_assign']
                        ) {
                            $users = Group_User::getGroupUsers(
                                $input['_groups_id_assign'],
                                ['is_manager' => 1]
                            );
                            foreach ($users as $data) {
                                $validations_to_send[] = $data['id'];
                            }
                        }
                        foreach ($this->getGroups(CommonITILActor::ASSIGN) as $d) {
                            $users = Group_User::getGroupUsers(
                                $d['groups_id'],
                                ['is_manager' => 1]
                            );
                            foreach ($users as $data) {
                                $validations_to_send[] = $data['id'];
                            }
                        }
                        break;

                    case 'requester_responsible':
                        if (isset($input['_users_id_requester'])) {
                            if (is_array($input['_users_id_requester'])) {
                                foreach ($input['_users_id_requester'] as $users_id) {
                                    $user = new User();
                                    if ($user->getFromDB($users_id)) {
                                        $validations_to_send[] = $user->getField('users_id_supervisor');
                                    }
                                }
                            } else {
                                $user = new User();
                                if ($user->getFromDB($input['_users_id_requester'])) {
                                    $validations_to_send[] = $user->getField('users_id_supervisor');
                                }
                            }
                        }
                        break;

                    default:
                        // Group case from rules
                        if ($key === 'group') {
                            foreach ($value as $groups_id) {
                                $validation_right = 'validate';
                                if ($this->getType() === Ticket::class) {
                                    $validation_right = isset($input['type']) && $input['type'] == Ticket::DEMAND_TYPE
                                        ? 'validate_request'
                                        : 'validate_incident';
                                }
                                $opt = [
                                    'groups_id' => $groups_id,
                                    'right'     => $validation_right,
                                    'entity'    => $entid,
                                ];

                                $data_users = $validation->getGroupUserHaveRights($opt);

                                foreach ($data_users as $user) {
                                    $validations_to_send[] = $user['id'];
                                }
                            }
                        } else {
                            $validations_to_send[] = $value;
                        }
                }
            }

            // Validation user added on ticket form
            if (isset($input['users_id_validate'])) {
                if (array_key_exists('groups_id', $input['users_id_validate'])) {
                    foreach ($input['users_id_validate'] as $key => $validation_to_add) {
                        if (is_numeric($key)) {
                            $validations_to_send[] = $validation_to_add;
                        }
                    }
                } else {
                    foreach ($input['users_id_validate'] as $key => $validation_to_add) {
                        if (is_numeric($key)) {
                            $validations_to_send[] = $validation_to_add;
                        }
                    }
                }
            }

            // Keep only one
            $validations_to_send = array_unique($validations_to_send);

            if (count($validations_to_send)) {
                $values            = [];
                $values[$self_fk]  = $this->fields['id'];
                if (isset($input['id']) && $input['id'] != $this->fields['id']) {
                    $values['_itilobject_add'] = true;
                }

                // to know update by rules
                if (isset($input["_rule_process"])) {
                    $values['_rule_process'] = $input["_rule_process"];
                }
                // if auto_import, tranfert it for validation
                if (isset($input['_auto_import'])) {
                    $values['_auto_import'] = $input['_auto_import'];
                }

                // Cron or rule process of hability to do
                if (
                    Session::isCron()
                    || isset($input["_auto_import"])
                    || isset($input["_rule_process"])
                    || $validation->can(-1, CREATE, $values)
                ) { // cron or allowed user
                    $add_done = false;
                    foreach ($validations_to_send as $user) {
                        // Do not auto add twice same validation
                        if (!$validation->alreadyExists($values[$self_fk], $user)) {
                            $values["users_id_validate"] = $user;
                            if ($validation->add($values)) {
                                $add_done = true;
                            }
                        }
                    }
                    if ($add_done) {
                        Event::log(
                            $this->fields['id'],
                            strtolower($this->getType()),
                            4,
                            "tracking",
                            sprintf(
                                __('%1$s updates the item %2$s'),
                                $_SESSION["glpiname"],
                                $this->fields['id']
                            )
                        );
                    }
                }
            }
        }
        return true;
    }

    /**
     * Manage actors posted by itil form
     * New way to do it with a general array containing all item actors.
     * We compare to old actors (in case of items's update) to know which we need to remove/add/update
     *
     * @param bool $disable_notifications
     *
     * @since 10.0.0
     *
     * @return void
     */
    protected function updateActors(bool $disable_notifications = false)
    {
        // Reload actors to be able to categorize users as added/updated/deleted.
        $this->loadActors();

        $common_actor_input = [
            '_do_not_compute_takeintoaccount' => $this->isTakeIntoAccountComputationBlocked($this->input),
            '_from_object'                    => true,
        ];
        if ($disable_notifications) {
            $common_actor_input['_disablenotif'] = true;
        }

        $actor_itemtypes = [
            User::class,
            Group::class,
            Supplier::class,
        ];
        $actor_types = [
            'requester',
            'assign',
            'observer',
        ];

        foreach ($actor_types as $actor_type) {
            $actor_type_value = constant(CommonITILActor::class . '::' . strtoupper($actor_type));

            // List actors from all input keys
            $actors = [];
            foreach ($actor_itemtypes as $actor_itemtype) {
                $actor_fkey = getForeignKeyFieldForItemType($actor_itemtype);

                $actors_id_input_key      = sprintf('_%s_%s', $actor_fkey, $actor_type);
                $actors_notif_input_key   = sprintf('%s_notif', $actors_id_input_key);
                $actors_id_add_input_key  = $actor_itemtype === User::class
                    ? sprintf('_additional_%ss', $actor_type)
                    : sprintf('_additional_%ss_%ss', strtolower($actor_itemtype), $actor_type);

                $get_unique_key = function (array $actor) use ($actors_id_input_key): string {
                    // Use alternative_email in value key for "email" actors
                    return sprintf('%s_%s', $actors_id_input_key, $actor['items_id'] ?: $actor['alternative_email'] ?? '');
                };

                if (array_key_exists($actors_id_input_key, $this->input)) {
                    if (is_array($this->input[$actors_id_input_key])) {
                        foreach ($this->input[$actors_id_input_key] as $actor_key => $actor_id) {
                            if (!is_numeric($actor_id)) {
                                trigger_error(
                                    sprintf(
                                        'Invalid value "%s" found for actor in "%s".',
                                        $actor_id,
                                        $actors_id_input_key
                                    ),
                                    E_USER_WARNING
                                );
                            }
                            $actor_id = (int) $actor_id;
                            $actor = [
                                'itemtype' => $actor_itemtype,
                                'items_id' => $actor_id,
                                'type'     => $actor_type_value,
                            ];
                            if ($actor_itemtype !== Group::class && array_key_exists($actors_notif_input_key, $this->input)) {
                                // Expected format
                                // '_users_id_requester_notif' => [
                                //     'use_notification'  => [1, 0],
                                //     'alternative_email' => ['user1@example.com', 'user2@example.com'],
                                // ]
                                $notification_params = $this->input[$actors_notif_input_key];
                                $unexpected_format = false;
                                if (
                                    !is_array($notification_params)
                                    || (
                                        !array_key_exists('use_notification', $notification_params)
                                        && !array_key_exists('alternative_email', $notification_params)
                                    )
                                ) {
                                    $unexpected_format = true;
                                    $notification_params = [];
                                }
                                foreach ($notification_params as $key => $values) {
                                    if (!is_array($values)) {
                                        $unexpected_format = true;
                                        continue;
                                    }
                                    if (is_array($values) && array_key_exists($actor_key, $values)) {
                                        $actor[$key] = $values[$actor_key];
                                    }
                                }
                                if ($unexpected_format) {
                                    trigger_error(
                                        sprintf('Unexpected format found in "%s".', $actors_notif_input_key),
                                        E_USER_WARNING
                                    );
                                }
                            }
                            $actors[$get_unique_key($actor)] = $actor;
                        }
                    } elseif (is_numeric($this->input[$actors_id_input_key])) {
                        $actor_id = (int) $this->input[$actors_id_input_key];
                        $actor = [
                            'itemtype' => $actor_itemtype,
                            'items_id' => $actor_id,
                            'type'     => $actor_type_value,
                        ];
                        if (array_key_exists($actors_notif_input_key, $this->input)) {
                            // Expected formats
                            //
                            // Value provided by Change::getDefaultValues()
                            // '_users_id_requester_notif' => [
                            //     'use_notification'  => 1,
                            //     'alternative_email' => 'user1@example.com',
                            // ]
                            //
                            // OR
                            //
                            // Value provided by Ticket::getDefaultValues()
                            // '_users_id_requester_notif' => [
                            //     'use_notification'  => [1, 0],
                            //     'alternative_email' => ['user1@example.com', 'user2@example.com'],
                            // ]
                            $notification_params = $this->input[$actors_notif_input_key];
                            if (
                                !is_array($notification_params)
                                || (
                                    !array_key_exists('use_notification', $notification_params)
                                    && !array_key_exists('alternative_email', $notification_params)
                                )
                            ) {
                                trigger_error(
                                    sprintf('Unexpected format found in "%s".', $actors_notif_input_key),
                                    E_USER_WARNING
                                );
                                $notification_params = [];
                            }
                            foreach ($notification_params as $key => $values) {
                                if (is_array($values) && array_key_exists(0, $values)) {
                                    $actor[$key] = $values[0];
                                } elseif (!is_array($values)) {
                                    $actor[$key] = $values;
                                }
                            }
                        }
                        $actors[$get_unique_key($actor)] = $actor;
                    } elseif ($this->input[$actors_id_input_key] !== '') {
                        trigger_error(
                            sprintf(
                                'Invalid value "%s" found for actor in "%s".',
                                $this->input[$actors_id_input_key],
                                $actors_id_input_key
                            ),
                            E_USER_WARNING
                        );
                    }
                }
                if (array_key_exists($actors_id_add_input_key, $this->input)) {
                    foreach ($this->input[$actors_id_add_input_key] as $actor) {
                        $actor_id = null;
                        if (is_array($actor) && array_key_exists($actor_fkey, $actor)) {
                            $actor_id = $actor[$actor_fkey];
                        } else {
                            $actor_id = $actor;
                        }
                        if (!is_numeric($actor_id)) {
                            trigger_error(
                                sprintf(
                                    'Invalid value "%s" found for additional actor in "%s".',
                                    var_export($actor_id, true),
                                    $actors_id_add_input_key
                                ),
                                E_USER_WARNING
                            );
                            continue;
                        }
                        $actor_id = (int) $actor_id;
                        $actor = [
                            'itemtype' => $actor_itemtype,
                            'items_id' => $actor_id,
                            'type'     => $actor_type_value,
                        ];
                        $unique_key = $get_unique_key($actor);
                        if (!array_key_exists($unique_key, $actors)) {
                            $actors[$unique_key] = $actor;
                        }
                    }
                }
            }

            // Search for added/updated actors
            $existings = $this->getActorsForType($actor_type_value);
            $added     = [];
            $updated   = [];

            foreach ($actors as $actor) {
                if (
                    $actor['items_id'] === 0
                    && (
                        ($actor['itemtype'] === User::class && empty($actor['alternative_email'] ?? ''))
                        || $actor['itemtype'] !== User::class
                    )
                ) {
                    // Empty values, probably provided by static::getDefaultValues()
                    continue;
                }

                $found = false;
                foreach ($existings as $existing) {
                    if (
                        $actor['itemtype'] === User::class
                        && $actor['items_id'] == 0
                        && $actor['itemtype'] == $existing['itemtype']
                    ) {
                        // "email" actor found
                        if ($actor['alternative_email'] == $existing['alternative_email']) {
                            // The anonymous actor matches an existing one, update it
                            $updated[] = $actor + ['id' => $existing['id']];
                            $found = true;
                            break;
                        } else {
                            // Do not check for modifications on "email" actors (they should be deleted then re-added on email change)
                            continue;
                        }
                    }

                    if ($actor['itemtype'] != $existing['itemtype'] || $actor['items_id'] != $existing['items_id']) {
                        continue;
                    }
                    $found = true;

                    if ($actor['itemtype'] === Group::class) {
                        // Do not check for modifications on "group" actors (they do not have notification settings to update)
                        continue;
                    }

                    // check if modifications exists
                    if (
                        (
                            array_key_exists('use_notification', $actor)
                            && $actor['use_notification'] != $existing['use_notification']
                        )
                        || (
                            array_key_exists('alternative_email', $actor)
                            && $actor['alternative_email'] != $existing['alternative_email']
                        )
                    ) {
                        $updated[] = $actor + ['id' => $existing['id']];
                    }

                    break; // As actor is found, do not continue to list existings
                }

                if ($found === false) {
                    $added[] = $actor;
                }
            }

            // Add new actors
            foreach ($added as $actor) {
                $actor_obj = $this->getActorObjectForItem($actor['itemtype']);
                $actor_obj->add($common_actor_input + $actor + [
                    $actor_obj->getItilObjectForeignKey() => $this->fields['id'],
                    $actor_obj->getActorForeignKey()      => $actor['items_id'],
                ]);
                if (
                    $actor['type'] === CommonITILActor::ASSIGN
                    && (
                        (!isset($this->input['status']) && in_array($this->fields['status'], $this->getNewStatusArray()))
                        || (isset($this->input['status']) && in_array($this->input['status'], $this->getNewStatusArray()))
                    )
                    && in_array(self::ASSIGNED, array_keys($this->getAllStatusArray()))
                    && !$this->isStatusComputationBlocked($this->input)
                ) {
                    $self = new static();
                    $self->update(
                        [
                            'id'                              => $this->getID(),
                            'status'                          => self::ASSIGNED,
                            '_do_not_compute_takeintoaccount' => $this->isTakeIntoAccountComputationBlocked($this->input),
                            '_from_assignment'                => true,
                        ]
                    );
                    $this->fields['status'] = $self->fields['status'];
                }
            }
            // Update existing actors
            foreach ($updated as $actor) {
                $actor_obj = $this->getActorObjectForItem($actor['itemtype']);
                $actor_obj->update($common_actor_input + $actor);
            }
        }

        // Process deleted actors
        foreach ($actor_types as $actor_type) {
            foreach ($actor_itemtypes as $actor_itemtype) {
                $actor_fkey = getForeignKeyFieldForItemType($actor_itemtype);
                $actors_deleted_input_key = sprintf('_%s_%s_deleted', $actor_fkey, $actor_type);

                $deleted = array_key_exists($actors_deleted_input_key, $this->input)
                    ? $this->input[$actors_deleted_input_key]
                    : [];
                foreach ($deleted as $actor) {
                    $actor_obj = $this->getActorObjectForItem($actor['itemtype']);
                    $actor_obj->delete(['id' => $actor['id']]);
                }
            }
        }

        // We just updated actors, clear any cached data
        $this->clearLazyLoadedActors();
    }


    protected function getActorObjectForItem(string $itemtype = ""): CommonITILActor
    {
        switch ($itemtype) {
            case 'User':
                $actor = new $this->userlinkclass();
                break;
            case 'Group':
                $actor = new $this->grouplinkclass();
                break;
            case 'Supplier':
                $actor = new $this->supplierlinkclass();
                break;
            default:
                throw new \RuntimeException('Unexpected actor type.');
                break;
        }
        return $actor;
    }


    /**
     * Fill the tech and the group from the category
     * @param array $input
     * @return array
     */
    protected function setTechAndGroupFromItilCategory($input)
    {
        $cat = new ITILCategory();
        $has_user_assigned  = $this->hasValidActorInInput($input, User::class, CommonITILActor::ASSIGN);
        $has_group_assigned = $this->hasValidActorInInput($input, Group::class, CommonITILActor::ASSIGN);
        if (
            $input['itilcategories_id'] > 0
            && (!$has_user_assigned || !$has_group_assigned)
            && $cat->getFromDB($input['itilcategories_id'])
        ) {
            if (!$has_user_assigned && $cat->fields['users_id'] > 0) {
                $input['_users_id_assign'] = $cat->fields['users_id'];
            }
            if (!$has_group_assigned && $cat->fields['groups_id'] > 0) {
                $input['_groups_id_assign'] = $cat->fields['groups_id'];
            }
        }

        return $input;
    }


    /**
     * Fill the tech and the group from the hardware
     * @param array $input
     * @return array
     */
    protected function setTechAndGroupFromHardware($input, $item)
    {
        if ($item != null) {
            // Auto assign tech from item
            $has_user_assigned  = $this->hasValidActorInInput($input, User::class, CommonITILActor::ASSIGN);
            if (!$has_user_assigned && $item->isField('users_id_tech') && $item->fields['users_id_tech'] > 0) {
                $input['_users_id_assign'] = $item->fields['users_id_tech'];
            }

            // Auto assign group from item
            $has_group_assigned = $this->hasValidActorInInput($input, Group::class, CommonITILActor::ASSIGN);
            if (!$has_group_assigned && $item->isField('groups_id_tech') && $item->fields['groups_id_tech'] > 0) {
                $input['_groups_id_assign'] = $item->fields['groups_id_tech'];
            }
        }

        return $input;
    }

    /**
     * Replay setting auto assign if set in rules engine or by auto_assign_mode
     * Do not force status if status has been set by rules
     *
     * @param array $input
     *
     * @return array
     */
    protected function assign(array $input)
    {
        // FIXME Deprecate this method in GLPI 10.1.
        if (!in_array(self::ASSIGNED, array_keys($this->getAllStatusArray()))) {
            return $input;
        }

        if (
            (
                $this->hasValidActorInInput($input, User::class, CommonITILActor::ASSIGN)
                || $this->hasValidActorInInput($input, Group::class, CommonITILActor::ASSIGN)
                || $this->hasValidActorInInput($input, Supplier::class, CommonITILActor::ASSIGN)
            )
            && (in_array($input['status'], $this->getNewStatusArray()))
            && !$this->isStatusComputationBlocked($input)
        ) {
            $input["status"] = self::ASSIGNED;
        }

        return $input;
    }

    /**
     * Check if input contains a valid actor for given itemtype / actortype.
     *
     * @param array $input
     * @param string $itemtype
     * @param string $actortype
     *
     * @return bool
     */
    private function hasValidActorInInput(array $input, string $itemtype, string $actortype): bool
    {
        $input_id_key = sprintf(
            '_%s_%s',
            getForeignKeyFieldForItemType($itemtype),
            self::getActorFieldNameType($actortype)
        );
        $input_notif_key = sprintf(
            '%s_notif',
            $input_id_key
        );

        $has_valid_actor = false;
        if (array_key_exists($input_id_key, $input)) {
            if (is_array($input[$input_id_key]) && !empty($input[$input_id_key])) {
                foreach ($input[$input_id_key] as $key => $actor_id) {
                    if (
                        // actor with valid ID
                        (int) $actor_id > 0
                        // or "email" actor
                        || (
                            $itemtype === User::class
                            && (int) $actor_id === 0
                            && array_key_exists($input_notif_key, $input)
                            && (bool) ($input[$input_notif_key]['use_notification'][$key] ?? false) === true
                            && !empty($input[$input_notif_key]['alternative_email'][$key])
                        )
                    ) {
                        $has_valid_actor = true;
                        break;
                    }
                }
            } elseif (is_numeric($input[$input_id_key])) {
                $actor_id = (int) $input[$input_id_key];
                if (
                    // actor with valid ID
                    $actor_id > 0
                    // or "email" actor
                    || (
                        $itemtype === User::class
                        && $actor_id === 0
                        // Expected formats
                        //
                        // Value provided by Change::getDefaultValues()
                        // '_users_id_requester_notif' => [
                        //     'use_notification'  => 1,
                        //     'alternative_email' => 'user1@example.com',
                        // ]
                        //
                        // OR
                        //
                        // Value provided by Ticket::getDefaultValues()
                        // '_users_id_requester_notif' => [
                        //     'use_notification'  => [1, 0],
                        //     'alternative_email' => ['user1@example.com', 'user2@example.com'],
                        // ]
                        && array_key_exists($input_notif_key, $input)
                        && (
                            array_key_exists('use_notification', $input[$input_notif_key])
                            && (
                                (
                                    is_array($input[$input_notif_key]['use_notification'])
                                    && (bool) ($input[$input_notif_key]['use_notification'][0] ?? false) === true
                                )
                                || (bool) ($input[$input_notif_key]['use_notification'] ?? false) === true
                            )
                        )
                        && (
                            array_key_exists('alternative_email', $input[$input_notif_key])
                            && (
                                (
                                    is_array($input[$input_notif_key]['alternative_email'])
                                    && !empty($input[$input_notif_key]['alternative_email'][0])
                                )
                                || !empty($input[$input_notif_key]['alternative_email'])
                            )
                        )
                    )
                ) {
                    $has_valid_actor = true;
                }
            }
        }

        return $has_valid_actor;
    }

    /**
     * Parameter class to be use for this item (user templates)
     * @return string class name
     */
    abstract public static function getContentTemplatesParametersClass(): string;

    public static function getDataToDisplayOnKanban($ID, $criteria = [])
    {
        /**
         * @var \DBmysql $DB
        */
        global $DB;

        // List of items to return
        $items = [];

        // Common variables
        $self_item = new static();
        $can_update = static::canUpdate();
        $itemtype = static::class;
        $self_fk_field = static::getForeignKeyField();
        $linked_actors = [];

        // Build base query
        $WHERE = ['is_deleted' => 0];
        $WHERE += $criteria;
        $WHERE += getEntitiesRestrictCriteria();
        // visibility check hack so we don't have to load the complete DB info for every item
        $visiblity_criteria = Search::addDefaultWhere(static::class);
        if (!empty($visiblity_criteria)) {
            $WHERE[] = new QueryExpression(Search::addDefaultWhere(static::class));
        }
        $base_common_itil_query = [
            'SELECT' => [static::getTableField('id')],
            'FROM'   => static::getTable(),
            'WHERE'  => $WHERE,
        ];

        // Add JOIN
        $linked_tables = [];
        $default_joint = Search::addDefaultJoin(
            $itemtype,
            getTableForItemType($itemtype),
            $linked_tables, // Passed by reference, must be a defined variable even if empty
        );
        if (!empty($default_joint)) {
            $base_common_itil_query['LEFT JOIN'] = [new QueryExpression($default_joint)];
        }

        // Load common_itil
        $common_itil_query = $base_common_itil_query;
        $common_itil_query['SELECT'][] = static::getTableField('name');
        $common_itil_query['SELECT'][] = static::getTableField('status');
        $common_itil_query['SELECT'][] = static::getTableField('itilcategories_id');
        $common_itil_query['SELECT'][] = static::getTableField('content');
        $common_itil_iterator = $DB->request($common_itil_query);

        // Load actors (users)
        $user_link_class = $self_item->userlinkclass;
        if (is_a($user_link_class, CommonITILActor::class, true)) {
            $user_link_table = getTableForItemType($user_link_class);
            $linked_user_iterator = $DB->request([
                'SELECT' => [
                    $user_link_class::getTableField($self_fk_field),
                    $user_link_class::getTableField('users_id'),
                    User::getTableField('firstname'),
                    User::getTableField('realname'),
                    User::getTableField('name'),
                ],
                'FROM'   => $user_link_table,
                'INNER JOIN' => [
                    User::getTable() => [
                        'ON'  => [
                            $user_link_table => 'users_id',
                            User::getTable() => 'id',
                        ],
                    ],
                ],
                'WHERE'  => [
                    'type' => CommonITILActor::ASSIGN,
                    $self_fk_field => new QuerySubQuery($base_common_itil_query),
                ],
            ]);
            foreach ($linked_user_iterator as $linked_user_row) {
                $common_itil_id = $linked_user_row[$self_fk_field];

                // Init array
                if (!isset($linked_actors[$common_itil_id])) {
                    $linked_actors[$common_itil_id] = [];
                }

                // Push users
                $linked_actors[$common_itil_id][] = [
                    'itemtype'  => User::getType(),
                    'id'        => $linked_user_row['users_id'],
                    'firstname' => $linked_user_row['firstname'],
                    'realname'  => $linked_user_row['realname'],
                    'name'      => formatUserName(
                        $linked_user_row['users_id'],
                        $linked_user_row['name'],
                        $linked_user_row['realname'],
                        $linked_user_row['firstname']
                    ),
                ];
            }
        }

        // Load actors (groups)
        $group_link_class = $self_item->grouplinkclass;
        if (is_a($group_link_class, CommonITILActor::class, true)) {
            $group_link_table = getTableForItemType($group_link_class);
            $linked_group_iterator = $DB->request([
                'SELECT' => [
                    $group_link_class::getTableField($self_fk_field),
                    $group_link_class::getTableField('groups_id'),
                    Group::getTableField('name'),
                ],
                'FROM'   => $group_link_table,
                'INNER JOIN' => [
                    Group::getTable() => [
                        'ON'  => [
                            $group_link_table => 'groups_id',
                            Group::getTable() => 'id',
                        ],
                    ],
                ],
                'WHERE'  => [
                    'type' => CommonITILActor::ASSIGN,
                    $self_fk_field => new QuerySubQuery($base_common_itil_query),
                ],
            ]);
            foreach ($linked_group_iterator as $linked_group_row) {
                $common_itil_id = $linked_group_row[$self_fk_field];

                // Init array
                if (!isset($linked_actors[$common_itil_id])) {
                    $linked_actors[$common_itil_id] = [];
                }

                // Push groups
                $linked_actors[$common_itil_id][] = [
                    'itemtype' => Group::getType(),
                    'id'       => $linked_group_row['groups_id'],
                    'name'     => $linked_group_row['name'],
                ];
            }
        }

        // Load actors (supplier)
        $supplier_link_class = $self_item->supplierlinkclass;
        if (is_a($supplier_link_class, CommonITILActor::class, true)) {
            $suplier_link_table = getTableForItemType($supplier_link_class);
            $linked_supplier_iterator = $DB->request([
                'SELECT' => [
                    $supplier_link_class::getTableField($self_fk_field),
                    $supplier_link_class::getTableField('suppliers_id'),
                    Supplier::getTableField('name'),
                ],
                'FROM'   => $suplier_link_table,
                'INNER JOIN' => [
                    Supplier::getTable() => [
                        'ON'  => [
                            $suplier_link_table => 'suppliers_id',
                            Supplier::getTable() => 'id',
                        ],
                    ],
                ],
                'WHERE'  => [
                    'type' => CommonITILActor::ASSIGN,
                    $self_fk_field => new QuerySubQuery($base_common_itil_query),
                ],
            ]);
            foreach ($linked_supplier_iterator as $linked_supplier_row) {
                $common_itil_id = $linked_supplier_row[$self_fk_field];

                // Init array
                if (!isset($linked_actors[$common_itil_id])) {
                    $linked_actors[$common_itil_id] = [];
                }

                // Push groups
                $linked_actors[$common_itil_id][] = [
                    'itemtype' => Supplier::getType(),
                    'id'       => $linked_supplier_row['suppliers_id'],
                    'name'     => $linked_supplier_row['name'],
                ];
            }
        }

        // Load linked tickets (only for tickets)
        if (static::class === Ticket::class) {
            $linked_tickets = [];
            $linked_tickets_iterator = $DB->request(new \QueryUnion([
                // Get parents tickets
                [
                    'SELECT' => [
                        Ticket_Ticket::getTableField('tickets_id_1 AS tickets_id_parent'),
                        Ticket_Ticket::getTableField('tickets_id_2 AS tickets_id_child'),
                        Ticket::getTableField('status'),
                    ],
                    'FROM' => Ticket_Ticket::getTable(),
                    'LEFT JOIN' => [
                        Ticket::getTable() => [
                            'ON'  => [
                                Ticket_Ticket::getTable() => 'tickets_id_2',
                                Ticket::getTable() => 'id',
                            ],
                        ],
                    ],
                    'WHERE'  => [
                        'link' => Ticket_Ticket::PARENT_OF,
                        'tickets_id_1' => new QuerySubQuery($base_common_itil_query),
                    ],
                ],
                // Get children tickets
                [
                    'SELECT' => [
                        Ticket_Ticket::getTableField('tickets_id_1 AS tickets_id_child'),
                        Ticket_Ticket::getTableField('tickets_id_2 AS tickets_id_parent'),
                        Ticket::getTableField('status'),
                    ],
                    'FROM' => Ticket_Ticket::getTable(),
                    'LEFT JOIN' => [
                        Ticket::getTable() => [
                            'ON'  => [
                                Ticket_Ticket::getTable() => 'tickets_id_1',
                                Ticket::getTable() => 'id',
                            ],
                        ],
                    ],
                    'WHERE'  => [
                        'link' => Ticket_Ticket::SON_OF,
                        'tickets_id_2' => new QuerySubQuery($base_common_itil_query),
                    ],
                ],
            ]));

            foreach ($linked_tickets_iterator as $linked_ticket_row) {
                $tickets_id_parent = $linked_ticket_row['tickets_id_parent'];

                // Init array
                if (!isset($linked_tickets[$tickets_id_parent])) {
                    $linked_tickets[$tickets_id_parent] = [];
                }

                // Push links
                $linked_tickets[$tickets_id_parent][] = [
                    'tickets_id' => $linked_ticket_row['tickets_id_child'],
                    'status'     => $linked_ticket_row['status'],
                ];
            }
        }

        foreach ($common_itil_iterator as $data) {
            $data = [
                'id'        => $data['id'],
                'name'      => $data['name'],
                'category'  => $data['itilcategories_id'],
                'content'   => $data['content'],
                'status'    => $data['status'],
                '_itemtype' => $itemtype,
                '_team'     => $linked_actors[$data['id']] ?? [],
                // Only use global update right here because checking item right
                // is too expensive (need to load full item just to check right)
                '_readonly' => !$can_update,
            ];

            if (static::class === Ticket::class) {
                $data['_steps'] = $linked_tickets[$data['id']] ?? [];
            }

            $items[$data['id']] = $data;
        }

        return $items;
    }

    public static function getKanbanColumns($ID, $column_field = null, $column_ids = [], $get_default = false)
    {
        if (!in_array($column_field, ['status'])) {
            return [];
        }

        $columns = [];
        $criteria = [];
        if (empty($column_ids)) {
            return [];
        }
        // Fill columns with empty arrays for each column id to avoid missing columns in the kanban
        foreach ($column_ids as $column_id) {
            $columns[$column_id] = [];
        }
        // Never try getting cards in drop-only columns
        $columns_defined = self::getAllKanbanColumns('status');
        $statuses_from_db = array_filter($column_ids, static function ($id) use ($columns_defined) {
            $id = (int) $id;
            return isset($columns_defined[$id]) && (!isset($columns_defined[$id]['drop_only']) || $columns_defined[$id]['drop_only'] === false);
        });
        if (count($statuses_from_db)) {
            $criteria = [
                static::getTableField('status') => $statuses_from_db,
            ];
        }

        // Avoid fetching everything when nothing is needed
        if (isset($criteria[static::getTableField('status')])) {
            $items = self::getDataToDisplayOnKanban($ID, $criteria);
        } else {
            $items = [];
        }


        $extracolumns = self::getAllKanbanColumns($column_field, $column_ids, $get_default);
        foreach ($extracolumns as $column_id => $column) {
            $columns[$column_id] = $column;
        }

        foreach ($items as $item) {
            if (!array_key_exists($item[$column_field], $columns)) {
                continue;
            }
            $itemtype = $item['_itemtype'];
            $card = [
                'id'              => "{$itemtype}-{$item['id']}",
                'title'           => '<span class="pointer">' . $item['name'] . '</span>',
                'title_tooltip'   => Html::resume_text(RichText::getTextFromHtml($item['content'], false, true, true), 100),
                'is_deleted'      => $item['is_deleted'] ?? false,
            ];

            $content = "<div class='kanban-plugin-content'>";
            $plugin_content_pre = Plugin::doHookFunction('pre_kanban_content', [
                'itemtype' => $itemtype,
                'items_id' => $item['id'],
            ]);
            if (!empty($plugin_content_pre['content'])) {
                $content .= $plugin_content_pre['content'];
            }
            $content .= "</div>";
            // Core content
            $content .= "<div class='kanban-core-content'>";
            if (isset($item['_steps']) && count($item['_steps'])) {
                $done = count(array_filter($item['_steps'], static function ($l) {
                    return in_array($l['status'], static::getClosedStatusArray());
                }));
                $total = count($item['_steps']);
                $content .= "<div class='flex-break'></div>";
                $content .= sprintf(__('%s / %s tasks complete'), $done, $total);
            }
            $content .= "<div class='flex-break'></div>";

            $content .= "</div>";
            $content .= "<div class='kanban-plugin-content'>";
            $plugin_content_post = Plugin::doHookFunction('post_kanban_content', [
                'itemtype' => $itemtype,
                'items_id' => $item['id'],
            ]);
            if (!empty($plugin_content_post['content'])) {
                $content .= $plugin_content_post['content'];
            }
            $content .= "</div>";

            $card['content'] = $content;
            $card['_team'] = $item['_team'];
            $card['_readonly'] = $item['_readonly'];
            $card['_form_link'] = $itemtype::getFormUrlWithID($item['id']);
            $card['_metadata'] = [];
            $metadata_values = ['name', 'content'];
            foreach ($metadata_values as $metadata_value) {
                if (isset($item[$metadata_value])) {
                    $card['_metadata'][$metadata_value] = $item[$metadata_value];
                }
            }
            if (isset($card['_metadata']['content']) && is_string($card['_metadata']['content'])) {
                $card['_metadata']['content'] = Glpi\RichText\RichText::getTextFromHtml($card['_metadata']['content'], false, true);
            } else {
                $card['_metadata']['content'] = '';
            }
            $card['_metadata']['category'] = $item['category'];
            $card['_metadata'] = Plugin::doHookFunction(Hooks::KANBAN_ITEM_METADATA, [
                'itemtype' => $itemtype,
                'items_id' => $item['id'],
                'metadata' => $card['_metadata'],
            ])['metadata'];
            $columns[$item[$column_field]]['items'][] = $card;
        }

        $category_ids = [];
        foreach ($columns as $column_id => $column) {
            if (
                $column_id !== 0 && !in_array($column_id, $column_ids) &&
                (!isset($column['items']) || !count($column['items']))
            ) {
                // If no specific columns were asked for, drop empty columns.
                // If specific columns were asked for, such as when loading a user's Kanban view, we must preserve them.
                // We always preserve the 'No Status' column.
                unset($columns[$column_id]);
            } elseif (isset($column['items'])) {
                foreach ($column['items'] as $item) {
                    if (isset($item['_metadata']['category'])) {
                        $category_ids[] = $item['_metadata']['category'];
                    }
                }
            }
        }
        $category_ids = array_filter(array_unique($category_ids), static function ($id) {
            return $id > 0;
        });

        $categories = [];
        if (!empty($category_ids)) {
            /** @var \DBmysql $DB */
            global $DB;

            $cat_table = ITILCategory::getTable();
            $trans_table = DropdownTranslation::getTable();
            $name_select = new QueryExpression('IFNULL(' . $DB::quoteName("$trans_table.value") . ',' . $DB::quoteName("$cat_table.name") . ') AS ' . $DB::quoteName('name'));
            $it = $DB->request([
                'SELECT' => ["$cat_table.id", $name_select],
                'FROM' => $cat_table,
                'LEFT JOIN' => [
                    $trans_table => [
                        'ON' => [
                            $trans_table => 'items_id',
                            $cat_table => 'id',
                            [
                                'AND' => [
                                    $trans_table . '.itemtype' => ITILCategory::getType(),
                                    $trans_table . '.field' => 'name',
                                    $trans_table . '.language' => $_SESSION['glpilanguage'],
                                ],
                            ],
                        ],
                    ],
                ],
                'WHERE' => ["$cat_table.id" => $category_ids],
            ]);
            foreach ($it as $row) {
                $categories[$row['id']] = $row['name'];
            }
            // Add uncategorized category
            $categories[0] = '';
        }

        // Replace category ids with category names in items metadata
        foreach ($columns as &$column) {
            if (!isset($column['items'])) {
                continue;
            }
            foreach ($column['items'] as &$item) {
                $item['_metadata']['category'] = $categories[$item['_metadata']['category']] ?? '';
            }
        }

        return $columns;
    }

    public static function showKanban($ID)
    {
        $itilitem = new static();

        if (($ID > 0 && !$itilitem->getFromDB($ID)) || !$itilitem::canView()) {
            return false;
        }

        $team_role_ids = static::getTeamRoles();
        $team_roles = [];

        foreach ($team_role_ids as $role_id) {
            $team_roles[$role_id] = static::getTeamRoleName($role_id);
        }

        $supported_itemtypes = [];
        $supported_itemtypes[static::class] = [
            'name' => static::getTypeName(1),
            'icon' => static::getIcon(),
            'fields' => [
                'name'   => [
                    'placeholder'  => __('Name'),
                ],
                'content'   => [
                    'placeholder'  => __('Content'),
                    'type'         => 'textarea',
                ],
                'users_id'  => [
                    'type'         => 'hidden',
                    'value'        => $_SESSION['glpiID'],
                ],
            ],
            'team_itemtypes'  => static::getTeamItemtypes(),
            'team_roles'      => $team_roles,
            'allow_create'    => static::canCreate(),
        ];
        $column_field = [
            'id' => 'status',
            'extra_fields' => [],
        ];

        $itemtype = static::class;
        $rights = [
            'create_item'                    => self::canCreate(),
            'delete_item'                    => self::canDelete(),
            'create_column'                  => false,
            'modify_view'                    => true,
            'order_card'                     => true,
            'create_card_limited_columns'    => [],
        ];

        TemplateRenderer::getInstance()->display('components/kanban/kanban.html.twig', [
            'kanban_id'                   => 'kanban',
            'rights'                      => $rights,
            'supported_itemtypes'         => $supported_itemtypes,
            'max_team_images'             => 3,
            'column_field'                => $column_field,
            'item'                        => [
                'itemtype'  => $itemtype,
                'items_id'  => $ID,
            ],
            'supported_filters'           => [
                'title' => [
                    'description' => _x('filters', 'The title of the item'),
                    'supported_prefixes' => ['!', '#'], // Support exclusions and regex
                ],
                'type' => [
                    'description' => _x('filters', 'The type of the item'),
                    'supported_prefixes' => ['!'],
                ],
                'category' => [
                    'description' => _x('filters', 'The category of the item'),
                    'supported_prefixes' => ['!', '#'],
                ],
                'content' => [
                    'description' => _x('filters', 'The content of the item'),
                    'supported_prefixes' => ['!', '#'], // Support exclusions and regex
                ],
                'team' => [
                    'description' => _x('filters', 'A team member for the item'),
                    'supported_prefixes' => ['!'],
                ],
                'user' => [
                    'description' => _x('filters', 'A user in the team of the item'),
                    'supported_prefixes' => ['!'],
                ],
                'group' => [
                    'description' => _x('filters', 'A group in the team of the item'),
                    'supported_prefixes' => ['!'],
                ],
                'supplier' => [
                    'description' => _x('filters', 'A supplier in the team of the item'),
                    'supported_prefixes' => ['!'],
                ],
            ] + self::getKanbanPluginFilters(static::getType()),
        ]);
    }

    public static function getAllForKanban($active = true, $current_id = -1)
    {
        // ITIL items only have a global view
        $items = [
            -1 => __('Global'),
        ];
        return $items;
    }

    public static function getAllKanbanColumns($column_field = null, $column_ids = [], $get_default = false)
    {

        if ($column_field === null) {
            $column_field = 'status';
        }
        $columns = [];
        if ($column_field === null || $column_field === 'status') {
            $all_statuses = static::getAllStatusArray();
            foreach ($all_statuses as $status_id => $status) {
                $columns['status'][$status_id] = [
                    'id'           => $status_id,
                    'name'         => $status,
                    'color_class'  => 'itilstatus ' . static::getStatusKey($status_id),
                    'drop_only'    => (int) $status_id === self::CLOSED,
                ];
            }
        } else {
            return [];
        }
        return $columns[$column_field];
    }

    public static function getTeamRoles(): array
    {
        return [
            Team::ROLE_REQUESTER,
            Team::ROLE_OBSERVER,
            Team::ROLE_ASSIGNED,
        ];
    }

    public static function getTeamRoleName(int $role, int $nb = 1): string
    {
        switch ($role) {
            case Team::ROLE_REQUESTER:
                return _n('Requester', 'Requesters', $nb);
            case Team::ROLE_OBSERVER:
                return _n('Watcher', 'Watchers', $nb);
            case Team::ROLE_ASSIGNED:
                return _n('Assignee', 'Assignees', $nb);
        }
        return '';
    }

    public static function getTeamItemtypes(): array
    {
        return ['User', 'Group', 'Supplier'];
    }

    public function addTeamMember(string $itemtype, int $items_id, array $params = []): bool
    {
        if (
            array_key_exists('role', $params)
            && is_string($params['role'])
            && defined(CommonITILActor::class . '::' . strtoupper($params['role']))
        ) {
            $params['role'] = constant(CommonITILActor::class . '::' . strtoupper($params['role']));
        }
        $role = $params['role'] ?? CommonITILActor::ASSIGN;

        /** @var CommonDBTM $link_class */
        $link_class = null;
        switch ($itemtype) {
            case 'User':
                $link_class = $this->userlinkclass;
                break;
            case 'Group':
                $link_class = $this->grouplinkclass;
                break;
            case 'Supplier':
                $link_class = $this->supplierlinkclass;
                break;
        }

        if ($link_class === null) {
            return false;
        }

        $link_item = new $link_class();
        /** @var CommonDBTM $itemtype */
        $result = $link_item->add([
            static::getForeignKeyField()     => $this->getID(),
            $itemtype::getForeignKeyField()  => $items_id,
            'type'                           => $role,
        ]);
        return (bool) $result;
    }

    public function deleteTeamMember(string $itemtype, int $items_id, array $params = []): bool
    {
        $role = $params['role'] ?? CommonITILActor::ASSIGN;

        /** @var CommonDBTM $link_class */
        $link_class = null;
        switch ($itemtype) {
            case 'User':
                $link_class = $this->userlinkclass;
                break;
            case 'Group':
                $link_class = $this->grouplinkclass;
                break;
            case 'Supplier':
                $link_class = $this->supplierlinkclass;
                break;
        }

        if ($link_class === null) {
            return false;
        }

        $link_item = new $link_class();
        /** @var CommonDBTM $itemtype */
        $result = $link_item->deleteByCriteria([
            static::getForeignKeyField()     => $this->getID(),
            $itemtype::getForeignKeyField()  => $items_id,
            'type'                           => $role,
        ]);
        return (bool) $result;
    }

    public function getTeam(): array
    {
        /** @var \DBmysql $DB */
        global $DB;

        $team = [];

        $team_itemtypes = static::getTeamItemtypes();

        /** @var CommonDBTM $itemtype */
        foreach ($team_itemtypes as $itemtype) {
            /** @var CommonDBTM $link_class */
            $link_class = null;
            switch ($itemtype) {
                case 'User':
                    $link_class = $this->userlinkclass;
                    break;
                case 'Group':
                    $link_class = $this->grouplinkclass;
                    break;
                case 'Supplier':
                    $link_class = $this->supplierlinkclass;
                    break;
            }

            if ($link_class === null) {
                continue;
            }

            $select = [];
            if ($itemtype === 'User') {
                $select = [$link_class::getTable() . '.' . $itemtype::getForeignKeyField(), 'type', 'name', 'realname', 'firstname'];
            } else {
                $select = [
                    $link_class::getTable() . '.' . $itemtype::getForeignKeyField(), 'type', 'name',
                    new QueryExpression('NULL as realname'),
                    new QueryExpression('NULL as firstname'),
                ];
            }

            $it = $DB->request([
                'SELECT' => $select,
                'FROM'   => $link_class::getTable(),
                'WHERE'  => [static::getForeignKeyField() => $this->getID()],
                'INNER JOIN' => [
                    $itemtype::getTable() => [
                        'ON'  => [
                            $itemtype::getTable()   => 'id',
                            $link_class::getTable() => $itemtype::getForeignKeyField(),
                        ],
                    ],
                ],
            ]);
            foreach ($it as $data) {
                $items_id = $data[$itemtype::getForeignKeyField()];
                $member = [
                    'itemtype'     => $itemtype,
                    'items_id'     => $items_id,
                    'role'         => $data['type'],
                    'name'         => $data['name'],
                    'realname'     => $data['realname'],
                    'firstname'    => $data['firstname'],
                    'display_name' => formatUserName($items_id, $data['name'], $data['realname'], $data['firstname']),
                ];
                $team[] = $member;
            }
        }

        usort(
            $team,
            fn(array $member_1, array $member_2) => strcasecmp($member_1['display_name'], $member_2['display_name'])
        );

        return $team;
    }

    public function getTimelineStats(): array
    {
        /** @var \DBmysql $DB */
        global $DB;

        $stats = [
            'total_duration' => 0,
            'percent_done'   => 0,
        ];

        // compute itilobject duration
        $task_class  = $this->getType() . "Task";
        $task_table  = getTableForItemType($task_class);
        $foreign_key = $this->getForeignKeyField();

        $criteria = [
            'SELECT' => ['SUM' => 'actiontime AS actiontime'],
            'FROM'   => $task_table,
            'WHERE'  => [$foreign_key => $this->fields['id']],
        ];

        $req = $DB->request($criteria);
        if ($row = $req->current()) {
            $stats['total_duration'] = $row['actiontime'];
        }

        // compute itilobject percent done
        $criteria    = [
            $foreign_key => $this->fields['id'],
            'state'     => [Planning::TODO, Planning::DONE],
        ];
        $total_tasks = countElementsInTable($task_table, $criteria);
        $criteria    = [
            $foreign_key => $this->fields['id'],
            'state'      => Planning::DONE,
        ];
        $done_tasks = countElementsInTable($task_table, $criteria);
        if ($total_tasks != 0) {
            $stats['percent_done'] = floor(100 * $done_tasks / $total_tasks);
        }

        return $stats;
    }

    /**
     * Returns an instance of validation class, if it exists.
     *
     * @return CommonITILValidation|null
     */
    public static function getValidationClassInstance(): ?CommonITILValidation
    {
        $validation_class = static::class . 'Validation';
        if (class_exists($validation_class)) {
            return new $validation_class();
        }
        return null;
    }

    /**
     * Instead of "{itemtype} - {name}" we will use {itemtype} ({id}) - {name}
     * as the ID of a Ticket/Change/Problem is an important information
     *
     * @return string
     */
    public function getBrowserTabName(): string
    {
        return sprintf(
            __('%1$s (#%2$s) - %3$s'),
            static::getTypeName(1),
            $this->getID(),
            $this->getHeaderName()
        );
    }

    /**
     * Transfer "_actors" input (introduced in 10.0.0) into historical input keys.
     *
     * @param array $input
     *
     * @return array
     */
    protected function transformActorsInput(array $input): array
    {
        // Reload actors to be able to identify deleted users.
        if (!$this->isNewItem()) {
            $this->loadActors();
        }

        if (
            array_key_exists('_actors', $input)
            && is_array($input['_actors'])
            && count($input['_actors'])
        ) {
            foreach (['requester', 'observer', 'assign'] as $actor_type) {
                $actor_type_value = constant(CommonITILActor::class . '::' . strtoupper($actor_type));
                if ($actor_type_value === CommonITILActor::ASSIGN && !$this->canAssign()) {
                    continue;
                }
                if ($actor_type_value !== CommonITILActor::ASSIGN && !$this->isNewItem() && !$this->canUpdateItem()) {
                    continue;
                }

                $get_input_key = function (string $actor_itemtype, string $actor_type): string {
                    return sprintf(
                        '_%s_%s',
                        getForeignKeyFieldForItemType($actor_itemtype),
                        $actor_type
                    );
                };

                // Normalize all keys.
                foreach ([User::class, Group::class, Supplier::class] as $actor_itemtype) {
                    $input_key = $get_input_key($actor_itemtype, $actor_type);
                    $notif_key = sprintf('%s_notif', $input_key);

                    if (!array_key_exists($input_key, $input) || !is_array($input[$input_key])) {
                        $input[$input_key] = !empty($input[$input_key]) ? [$input[$input_key]] : [];
                    }

                    if ($actor_itemtype !== Group::class) {
                        if (!array_key_exists($notif_key, $input) || !is_array($input[$notif_key])) {
                            $input[$notif_key] = [
                                'use_notification'  => [],
                                'alternative_email' => [],
                            ];
                        }
                        foreach (['use_notification', 'alternative_email'] as $param_key) {
                            if (
                                !array_key_exists($param_key, $input[$notif_key])
                                || $input[$notif_key][$param_key] === ''
                            ) {
                                $input[$notif_key][$param_key] = [];
                            } elseif (!is_array($input[$notif_key][$param_key])) {
                                $input[$notif_key][$param_key] = [$input[$notif_key][$param_key]];
                            }
                        }
                    }
                    $input[sprintf('%s_deleted', $input_key)] = [];
                }

                $actors = array_key_exists($actor_type, $input['_actors']) && is_array($input['_actors'][$actor_type])
                    ? $input['_actors'][$actor_type]
                    : [];

                // Extract actors from new actors list
                foreach ($actors as $actor) {
                    $input_key = $get_input_key($actor['itemtype'], $actor_type);
                    $notif_key = sprintf('%s_notif', $input_key);

                    // Use alternative_email in value key for "email" actors
                    $value_key = sprintf('_actors_%s', $actor['items_id'] ?: $actor['alternative_email'] ?? '');

                    if (array_key_exists($value_key, $input[$input_key])) {
                        continue;
                    }

                    $input[$input_key][$value_key] = $actor['items_id'];

                    if ($actor_itemtype !== Group::class && array_key_exists('use_notification', $actor)) {
                        $input[$notif_key]['use_notification'][$value_key]  = $actor['use_notification'];
                        $input[$notif_key]['alternative_email'][$value_key] = $actor['alternative_email'] ?? '';
                    }
                }

                // Identify deleted actors
                if (!$this->isNewItem()) {
                    $existings = $this->getActorsForType($actor_type_value);
                    foreach ($existings as $existing) {
                        $found = false;
                        foreach ($actors as $actor) {
                            if (
                                (
                                    // "email" actor match
                                    $actor['itemtype'] === User::class
                                    && $actor['items_id'] == 0
                                    && $actor['itemtype'] == $existing['itemtype']
                                    && $actor['alternative_email'] == $existing['alternative_email']
                                )
                                || (
                                    // other actor match
                                    $actor['items_id'] != 0
                                    && $actor['itemtype'] == $existing['itemtype']
                                    && $actor['items_id'] == $existing['items_id']
                                )
                            ) {
                                $found = true;
                                break;
                            }
                        }
                        if ($found === false) {
                            $input_key = $get_input_key($existing['itemtype'], $actor_type);
                            $input[sprintf('%s_deleted', $input_key)][] = $existing;
                        }
                    }
                }
            }
            unset($input['_actors']);
        }

        return $input;
    }

    public function prepareInputForClone($input)
    {
        unset($input['actiontime']);
        return $input;
    }

    public static function getMessageReferenceEvent(string $event): ?string
    {
        // All actions should be attached to thread instanciated by `new` event
        return 'new';
    }

    /**
     * Is the current user have right to update the current ITIL object ?
     *
     * @return boolean
     **/
    public function canUpdateItem()
    {
        if (!$this->checkEntity()) {
            return false;
        }

        return self::canUpdate();
    }

    public function canDeleteItem()
    {

        if (!$this->checkEntity()) {
            return false;
        }
        return self::canDelete();
    }

    public function canAddItem($type)
    {
        if ($type == Document::class) {
            return $this->canAddDocuments();
        }

        return parent::canAddItem($type);
    }


    /**
     * Check whether the current user can add documents.
     */
    final protected function canAddDocuments(): bool
    {
        if (in_array($this->fields['status'], $this->getClosedStatusArray())) {
            return false;
        }

        if ($this->canAddFollowups()) {
            return true;
        }

        return false;
    }

    public static function getTeamMemberForm(CommonITILObject $item): string
    {
        $itiltemplate = $item->getITILTemplateToUse(
            0,
            $item instanceof Ticket ? $item->fields['type'] : null,
            $item->fields['itilcategories_id'],
            $item->fields['entities_id']
        );
        $field_options = [
            'full_width' => true,
            'fields_template' => $itiltemplate,
            'add_field_class' => 'col-sm-12',
        ];
        return TemplateRenderer::getInstance()->render('components/itilobject/actors/main.html.twig', [
            'item' => $item,
            'entities_id' => 0,
            'canupdate' => true,
            'canassign' => true,
            'params' => $item->fields + ['load_actors' => false],
            'itiltemplate' => $itiltemplate,
            'canassigntome' => false,
            'field_options' => $field_options,
            'allow_auto_submit' => false,
            'main_rand' => mt_rand(),
        ]);
    }
}
