<?php

namespace Velis;

use Collator;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use ReflectionException;
use RuntimeException;
use Velis\Acl\Priv;
use Velis\Acl\PrivsReloader;
use Velis\Acl\Role;
use Velis\App\MobileNotice;
use Velis\App\Setting;
use Velis\App\User\Authentication;
use Velis\App\User as AppUser;
use Velis\Exception\BusinessLogicException;
use Velis\Model\Cacheable;
use Velis\Model\DataObject\NameTrait;
use Velis\Model\DataObject;
use Velis\Model\ItemCacheable;
use Velis\Model\Sanitizable;
use Velis\Mvc\Controller\AccessException;
use Velis\Notification\Recipient;
use Velis\Notification\SmsRecipient;
use Velis\Timezone\DateTimeZoneProvider;
use Velis\User\AnonymizeTrait;
use Velis\User\ApeNotifier;

/**
 * Basic user model
 * @author Olek Procki <olo@velis.pl>
 */
class User extends DataObject implements Sanitizable, Recipient, SmsRecipient, PersonInterface
{
    use AnonymizeTrait;
    use NameTrait;

    public const ORDER_NAME = 'u.last_name, u.name';
    public const ORDER_CUSTOMER = 'customer_name NULLS FIRST, last_name, name';


    /**
     * Default list order
     * @var string
     */
    protected static $_listDefaultOrder = self::ORDER_NAME;
    protected static $_filterListParams = true;


    /**
     * @var Collator
     */
    protected static $_collator;


    /**
     * User's saved queries
     * @var Query[]
     */
    protected $_queries;


    /**
     * Application settings
     * @var array
     */
    protected $_settings;


    /**
     * Returns related table name
     * @return string
     */
    protected function _getTableName()
    {
        return 'acl.user_tab';
    }


    /**
     * Returns list source
     * @return string
     */
    protected function _getListDatasource()
    {
        return 'acl.user u';
    }


    /**
     * Returns object API fields
     * @return array
     */
    protected function _getApiFields()
    {
        $fields = parent::_getObjectFields();
        $unset = [
            'password',
            'reset_password_token',
            'mobile_pass',
            'password_date_expiry',
            'hash_algorithm',
            'no_expire',
            'support_login_token',
        ];

        if (!$this->isSuper() || !App::isSuper()) {
            $unset[] = 'node_token';
        }

        return array_diff($fields, $unset);
    }

    /**
     * Returns lang suffix
     */
    public function getLangVariant(): ?string
    {
        if (session_id()) {
            return App::$config->settings->langVariant ?: App::$session->langVariant;
        }

        return App::$config->settings->langVariant;
    }

    /**
     * Returns login
     * @return string
     */
    public function getLogin()
    {
        return $this['login'];
    }


    /**
     * Returns first name
     * @return string
     */
    public function getName()
    {
        return $this['name'];
    }


    /**
     * Returns last name
     * @return string
     */
    public function getLastName()
    {
        return $this['last_name'];
    }


    /**
     * Returns short user name
     *
     * @param bool $reversed
     * @return string
     */
    public function getShortName($reversed = false)
    {
        if ($reversed) {
            return $this->getLastName() . ' ' . mb_substr($this->getName(), 0, 1) . '.';
        } else {
            return mb_substr($this->getName(), 0, 1) . '.' . $this->getLastName();
        }
    }


    /**
     * Returns user's initials
     * @return string
     */
    public function getInitials()
    {
        if (self::hasField('initials') && !empty($this['initials'])) {
            return $this['initials'];
        }
        return mb_substr($this->getName(), 0, 1, 'UTF-8') .  mb_substr($this->getLastName(), 0, 1, 'UTF-8');
    }


    /**
     * Generates default user login
     *
     * @param PersonInterface $user
     * @param bool $short
     * @return string
     */
    public static function getDefaultLogin($user, $short = false)
    {
        $login = '';

        if ($user->company_short_name) {
            $login = $user->company_short_name . '_';
        }

        if ($short) {
            $login .= substr($user->first_name, 0, 1) . substr($user->last_name, 0, 1);
        } else {
            $login .= substr($user->first_name, 0, 1) . $user->last_name;
        }

        return trim(mb_strtolower(Output::stripPolishChars($login)));
    }


    /**
     * Returns object string representation
     * @see \Velis\User::getShortName()
     *
     * @return string
     */
    public function __toString()
    {
        return $this->getShortName(true);
    }


    /**
     * @param DataObject $other
     * @return bool
     */
    public function less(DataObject $other)
    {
        return 0 > $this->compareTo($other);
    }


    /**
     * @param DataObject $other
     * @return bool
     */
    public function greater(DataObject $other)
    {
        return 0 < $this->compareTo($other);
    }


    /**
     * @param DataObject $other
     * @return bool
     */
    public function equals(DataObject $other)
    {
        return 0 == $this->compareTo($other);
    }


    /**
     * Compares object with another instance
     * @param DataObject $other
     * @return int
     */
    public function compareTo(DataObject $other)
    {
        if ($other instanceof PersonInterface) {
            $otherName = $other->getFullName(true);
        } else {
            $otherName = (string) $other;
        }

        $collator = self::_getCollator();

        return $collator->compare($this->getFullName(true), $otherName);
    }


    /**
     * @return Collator
     */
    protected static function _getCollator()
    {
        if (!self::$_collator) {
            self::$_collator = new Collator(setlocale(LC_COLLATE, '0'));
        }

        return self::$_collator;
    }


    /**
     * Returns email address
     * @return string
     */
    public function getEmail()
    {
        return $this['email'];
    }


    /**
     * Returns user's phone no
     * @return string
     */
    public function getMobile()
    {
        if ($this->offsetExists('mobile_phone_no')) {
            return $this['mobile_phone_no'];
        } else {
            return ($this->offsetExists('area_number') ? $this['area_number'] : '') . $this['phone_no'];
        }
    }


    /**
     * Returns true if user is active
     * @return bool
     */
    public function isActive()
    {
        return $this['active'] == 1;
    }

    /**
     * Returns true if user is activated
     * @return bool
     */
    public function isActivated()
    {
        return $this['activated'] == 1;
    }


    /**
     * Returns true if user is present
     * @return bool
     */
    public function isPresent()
    {
        return !$this['absent_till'];
    }


    /**
     * Returns true if user is enabled
     * @return bool
     */
    public function isEnabled()
    {
        return $this['enabled'] == 1;
    }


    /**
     * Returns password as hash md5
     *
     * @param string $password
     * @return string
     */
    protected function _getPasswordHash($password)
    {
        if (!$this['login']) {
            $this->load();
        }

        $md5 = md5($this->getLogin() . '|' . $password);

        if ($this->_hasField('hash_algorithm') && $this->hash_algorithm) {
            return password_hash($password, PASSWORD_BCRYPT);
        } else {
            return $md5;
        }
    }

    /**
     * {@inheritDoc}
     */
    public function add($updateObjectId = true)
    {
        if (self::loginExists($this->getLogin())) {
            throw new RuntimeException(Lang::get('GENERAL_LOGIN_EXISTS'), '666');
        }

        $this->checkIfLoginIsForbidden();

        if ($this['plain_password']) {
            $this['hash_algorithm'] = Authentication::HASH_BCRYPT;
            $this['password'] = $this->_getPasswordHash($this['plain_password']);
        }

        if (!isset($this['active'])) {
            $this['active'] = 1;
        }

        $this->sanitize();

        parent::add($updateObjectId);

        return $this;
    }

    /**
     * @return void
     * @throws RuntimeException
     */
    private function checkIfLoginIsForbidden()
    {
        $forbiddenLogins = Arrays::toArray(App::$config->settings->forbiddenLogins);

        if ($this->id()) {
            $currentInstance = $this->getCurrentInstance();
            $loginChanged = ($currentInstance['login'] != $this->getLogin());
        } else {
            $loginChanged = true;
        }

        if ($loginChanged && in_array($this->getLogin(), $forbiddenLogins)) {
            throw new RuntimeException(Lang::get('USER_USERNAME_NOT_ALLOWED'));
        }
    }

    /**
     * {@inheritDoc}
     */
    public function modify($checkDiff = false)
    {
        $this->checkIfLoginIsForbidden();

        return parent::modify($checkDiff);
    }

    /**
     * Creates user account for person
     * @return User
     * @throws Exception
     */
    public function createAccount()
    {
        $commit = self::$_db->startTrans();
        try {
            $params = [
                'user_id' => $this->id(),
                'company_id' => $this['company_id'],
                'lang_id' => $this['lang_id'] ?: App::$config->defaultLanguage
            ];

            if ($this['login']) {
                $params['login'] = $this['login'];
            }

            self::$_db->insert('acl.user_tab', $params);
            if ($commit) {
                self::$_db->commit();
            }
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }

        return $this;
    }


    /**
     * {@inheritDoc}
     */
    public static function getList($page = 1, $params = null, $order = null, $limit = self::ITEMS_PER_PAGE, $fields = null)
    {
        $params = new ParameterBag($params);

        if ($params['role_id'] && !is_array($params['role_id'])) {
            $params['role_id'] = [$params['role_id']];
        }

        if ($params['is_internal']) {
            $internalRoles = array_keys(Role::listInternal());
            if ($params['role_id']) {
                $commonRoles = array_intersect($params['role_id'], $internalRoles);
                if ($commonRoles) {
                    $params['role_id'] = $commonRoles;
                } else {
                    $params['role_id'] = $internalRoles;
                }
            } else {
                $params['role_id'] = $internalRoles;
            }
        }

        if ($params['role_id']) {
            self::$_listConditions[] = 'EXISTS (
                SELECT 1 FROM acl.user_role_tab ur
                WHERE ur.user_id=u.user_id AND ur.role_id IN' . (Role::hasField('acro') ? ' (' . implode(',', Filter::filterInts($params['role_id'])) . ')' : " ('" . implode("','", $params['role_id']) . "')")
            . ')';
        }

        if ($params['role']) {
            if (!is_array($params['role'])) {
                $params['role'] = explode(',', $params['role']);
            }
            $key = Role::hasField('acro') ? "r.acro" : "r.role_id";
            self::$_listConditions[] = "EXISTS(
                SELECT 1 FROM acl.user_role_tab ur JOIN acl.role_tab r USING(role_id)
                WHERE ur.user_id = u.user_id AND $key IN('" . implode("','", $params['role']) . "')
            )";
        }

        if ($params['excluded_role']) {
            if (strpos($params['excluded_role'], ',')) {
                $params['role_excluded'] = explode(',', $params['excluded_role']);
            } elseif (!is_array($params['excluded_role'])) {
                $params['excluded_role'] = [$params['excluded_role']];
            }
            $key = Role::hasField('acro') ? 'r.acro' : 'r.role_id';
            self::$_listConditions[] = "NOT EXISTS(
                SELECT 1 FROM acl.user_role_tab ur JOIN acl.role_tab r USING(role_id)
                WHERE ur.user_id = u.user_id AND $key IN('" . implode("','", $params['excluded_role']) . "')
            )";
        }

        $result = parent::getList($page, $params, $order, $limit, $fields);

        $class = get_called_class();
        $list = [];

        foreach ($result as $row) {
            $row['acl_roles'] = explode(',', $row['acl_roles'] ?? '');
            $list[$row['user_id']] = new $class($row);
        }

        return $list;
    }


    /**
     * Returns users on acl
     * @param bool $active
     * @param bool $cache
     * @return User[]
     * @throws DataObject\NoColumnsException
     */
    public static function listByGroup($active = true, $cache = false)
    {
        $query = '
            SELECT * FROM acl.user u
            WHERE active = :active
            ORDER BY ' . static::ORDER_CUSTOMER . '
        ';

        $params = [
            'active' => $active ? 1 : 0,
        ];

        $users = [];
        $result = $cache ? self::$_db->cacheGetAll($query, $params, null, App::CACHE_24H) : self::$_db->getAll($query, $params);

        foreach ($result as $row) {
            $users[$row['user_id']] = new self($row);
        }

        return $users;
    }


    /**
     * Returns active users list
     * @param bool $enabled
     * @return User[]
     */
    public static function listActive($enabled = false)
    {
        $activeUsers = [];
        foreach (self::listCached() as $user) {
            if ($enabled && !$user->enabled) {
                continue;
            }
            if ($user->active == 1) {
                $activeUsers[$user->user_id] = $user;
            }
        }

        return $activeUsers;
    }


    /**
     * Checks if login exists (and belongs to active user)
     *
     * @param string $login
     * @return bool
     */
    public static function loginExists($login)
    {
        if (!$login) {
            return false;
        }

        $query = 'SELECT COUNT(*) FROM acl.user_tab WHERE login = :login AND active = 1';

        return self::$_db->getOne($query, ['login' => $login]) > 0;
    }


    /**
     * Returns user by login
     * @param string $login
     * @return static
     * @throws ReflectionException
     */
    public static function byLogin($login)
    {
        if (static::isCacheable()) {
            foreach (self::listCached() as $user) {
                if ($login == $user->login) {
                    return $user;
                }
            }
        } else {
            return Arrays::getFirst(static::listAll(['login' => $login]));
        }

        return null;
    }


    /**
     * Returns user application setting(s)
     *
     * @param string $setting
     * @param bool $reload
     * @return mixed
     */
    public function settings($setting = null, $reload = false)
    {
        if ($setting && App::$config->settings->aliases[$setting]) {
            $setting = App::$config->settings->aliases[$setting];
        }

        if (!isset($this->_settings) || $reload) {
            $this->_settings = Setting::getUserSettings($this);

            if (!$this->_settings) {
                $this->_settings = [];
            }
        }

        return $setting ? $this->_settings[$setting] : $this->_settings;
    }


    /**
     * Save the user settings
     *
     * @param array $settings
     * @throws Exception
     */
    public function saveSettings($settings)
    {
        $settings = Setting::setUserSettings($settings, $this);
        $_settings = $this->_settings && is_array($this->_settings) ? $this->_settings : [];

        $this->_settings = array_merge($_settings, $settings);
    }


    /**
     * Returns user's stored queries
     *
     * @param string $module
     * @param string $controller
     * @param string $action
     * @return Query[]
     */
    public function getQueries($module = null, $controller = null, $action = null)
    {
        if (!isset($this->_queries)) {
            $cachedQueries = Query::listCached();
            $this->_queries = Arrays::byValue($cachedQueries, 'user_id', $this->id());
        }

        $queries = [];

        foreach ($this->_queries as $query) {
            if (!$module || $query['module'] == $module) {
                if (!$controller || $query['controller'] == $controller) {
                    if (!$action || $query['action'] == $action) {
                        $queries[$query->id()] = $query;
                    }
                }
            }
        }

        return $queries;
    }


    /**
     * Returns true if user has $priv in $module
     *
     * @param string $module
     * @param string $priv
     * @return bool
     */
    public function hasPriv()
    {
        if (func_num_args() == 1 && is_array(func_get_arg(0))) {
            list($module, $priv) = func_get_arg(0);
        } else {
            list($module, $priv) = func_get_args();
        }

        if (App::$session && App::$session->debugMode) {
            $privsLogger = App::$di->get('privsLogger');
            $file = null;
            $backtrace = debug_backtrace(false, 2);

            if (isset($backtrace[1])) {
                $file = $backtrace[1]['file'];
            }

            if (!isset($backtrace[1]['class'])) {
                $file = $backtrace[0]['file'];
            }

            $privsLogger->log($module, $priv, $file);
        }

        return $this->checkPriv((string) $module, (string) $priv);
    }


    /**
     * @param string $module
     * @param ?string $priv
     * @return bool
     */
    protected function checkPriv(string $module, string $priv = null): bool
    {
        $bindParams = [
            'priv_acro' => $priv,
            'user_id' => $this->id(),
        ];

        if (Priv::hasField('priv_acro')) {
            $bindParams['app_module_id'] = $module;
        } else {
            $bindParams['app_module_acro'] = $module;
        }

        $dataSource = Acl::getUserPrivsDatasource();
        $conditions = self::$_db->conditions($bindParams);

        $result = self::$_db->cacheGetOne("
            SELECT count(*)
            FROM $dataSource
            WHERE 1=1 $conditions
        ", $bindParams);

        return $result > 0;
    }

    /**
     * @param string[][] $privs - array of `['module', 'priv acro']` pairs
     * @return bool
     */
    public function hasAnyPriv(array $privs): bool
    {
        if (empty($privs)) {
            return false;
        }

        $fieldName = 'app_module_acro';
        if (Priv::hasField('priv_acro')) {
            $fieldName = 'app_module_id';
        }

        $conditions = [];
        $bindParams = [
            'user_id' => $this->id(),
        ];
        $conditionsString = self::_getDb()->conditions($bindParams);

        foreach ($privs as $index => $priv) {
            [$module, $acro] = $priv;
            $conditions[] = "($fieldName = :app_module_id_$index AND priv_acro = :priv_acro_$index)";
            $bindParams[$fieldName . '_' . $index] = $module;
            $bindParams["priv_acro_$index"] = $acro;
        }

        $conditionsString .= ' AND (' . implode(' OR ', $conditions) . ')';

        $dataSource = Acl::getUserPrivsDatasource();

        $result = self::_getDb()->cacheGetOne("
            SELECT count(*)
            FROM $dataSource
            WHERE 1=1 $conditionsString
        ", $bindParams);

        return $result > 0;
    }

    /**
     * @param string[][] $privs - array of `['module', 'priv acro']` pairs
     * @throws AccessException
     */
    public function checkAnyPriv(array $privs): void
    {
        if (!$this->hasAnyPriv($privs)) {
            throw new AccessException();
        }
    }


    /**
     * Returns true if user has role
     * @param string $roleAcro
     * @return bool
     */
    public function hasRole($roleAcro)
    {
        if ($this->offsetExists('acl_roles')) {
            if (!is_array($this['acl_roles'])) {
                $this['acl_roles'] = explode(',', $this['acl_roles']);
            }

            return in_array($roleAcro, $this['acl_roles']);
        } else {
            $params = [
                'user_id' => $this->id(),
            ];
            $key = Role::hasField('acro') ? "acro" : "role_id";
            $params[$key] = $roleAcro;

            return (bool) self::$_db->cacheGetOne(
                "SELECT COUNT(*) FROM acl.user_role_tab JOIN acl.role_tab USING(role_id) WHERE 1=1 " . self::$_db->conditions($params),
                $params
            );
        }
    }


    /**
     * Returns true if user has any role provided in function args
     * @return bool
     */
    public function hasAnyRole()
    {
        foreach (func_get_args() as $role) {
            if ($this->hasRole($role)) {
                return true;
            }
        }

        return false;
    }


    /**
     * Return user's ape channel
     * @return string
     */
    public function getApeChannel()
    {
        return ApeNotifier::getUserChannel($this);
    }


    /**
     * Init role changelog
     */
    public static function initRoleChangeLog()
    {
        self::$_db->execDML(
            "CREATE TEMPORARY TABLE user_role_current_tab
             AS SELECT * FROM acl.user_role_tab"
        );
    }


    /**
     * Calculate role changelog
     */
    public static function calculateRoleChangeLog()
    {
        Acl::logUserRolesChange();
        self::$_db->execDML("DROP TABLE user_role_current_tab");
    }


    /**
     * Attach role to user
     *
     * @param array<int>|array<string> $roleIds
     * @throws Exception
     * @throws BusinessLogicException
     */
    public function saveRoles($roleIds, $onlySave = false)
    {
        $roles = Role::get($roleIds);

        if ($roles && !is_array($roles)) {
            $roles = [$roles];
        }

        try {
            $commit = self::$_db->startTrans();

            if (!$onlySave) {
                if (App::settings('AclChangelog')) {
                    self::initRoleChangeLog();
                }

                if (App::$config->session->adapter == 'Redis') {
                    $privsReloader = new PrivsReloader();
                    $privsReloader->addUsers($this->user_id);
                }
            }

            $params = $this->_getPrimaryKeyParam();
            $values = [];
            $rolesToKeep = [];
            $rolesExternal = null;

            $insertQuery = 'INSERT INTO acl.user_role_tab (role_id, user_id) VALUES ';
            $deleteQuery = 'DELETE FROM acl.user_role_tab WHERE user_id = :user_id AND role_id NOT IN (';

            $key = 1;
            foreach ($roles as $role) {
                $values[] = '(:role_id_' . $key . ', :user_id)';
                $rolesToKeep[] = ':role_id_' . $key;
                $params['role_id_' . $key] = $role->id();
                $key++;

                if (null === $rolesExternal) {
                    $rolesExternal = (bool) $role->is_external;
                }

                if ((bool) $role->is_external !== $rolesExternal) {
                    throw new BusinessLogicException(Lang::get('USER_CANNOT_BOTH_INTERNAL_EXTERNAL_ROLES'));
                }
            }

            $insertQuery .= implode(', ', $values) . ' ON CONFLICT DO NOTHING ';
            $deleteQuery .= implode(', ', $rolesToKeep) . ')';

            self::$_db->execDML($insertQuery, $params);
            self::$_db->execDML($deleteQuery, $params);

            if (!$onlySave) {
                if (App::settings('AclChangelog')) {
                    self::calculateRoleChangeLog();
                }

                if (App::$config->session->adapter == 'Redis') {
                    $privsReloader->reload();
                }
            }
            if ($commit) {
                self::$_db->commit();
            }

            if (App::hasModule('Mobile')) {
                $mobileNotice = new MobileNotice(MobileNotice::USER_EDIT);
                $mobileNotice
                    ->addUsers($this->user_id)
                    ->send();
            }
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }

        if ($this instanceof Cacheable) {
            self::unsetListCache();
        }
    }


    /**
     * Returns true if user is superUser
     * @return bool
     */
    public function isSuper(): bool
    {
        return in_array($this->id(), App::getSuperUsers());
    }


    /**
     * Returns true if user is supportUser
     * @return bool
     */
    public function isSupport(): bool
    {
        return in_array($this->id(), App::getSupportUsers());
    }


    /**
     * Returns demo account expiry date
     * @return string
     */
    public function getExpiryDate()
    {
        return $this['date_expiry'];
    }


    /**
     * Returns user timezone
     */
    public function getTimezone(): string
    {
        // if we have it then just return
        if (isset($this['timezone_id'])) {
            return $this['timezone_id'];
        }

        // prevent db connection infinite loop
        if (self::$_db->isConnected() || !$this instanceof AppUser) {
            if ($this->_hasField('timezone_id', true) && $this->timezone_id) {
                return $this->timezone_id;
            }
        }

        if ($timezone = Timezone::getDefaultTimezone()) {
            return $timezone->timezone_id;
        }

        return 'Europe/Warsaw';
    }

    /**
     * Returns DateTimeZone object for user timezone
     *
     * @param bool $safe - if true (default) will return matching or default timezone if user timezone invalid
     */
    public function getDateTimeZone(bool $safe = true): DateTimeZone
    {
        $provider = new DateTimeZoneProvider($safe);
        $identifier = $this->getTimezone();

        return $provider->get($identifier);
    }

    /**
     * Returns true if user is currently substitute for other users
     * @return bool
     */
    public function isSubstitute(): bool
    {
        return !!$this->getSubstitutedUserIds();
    }

    /**
     * Check whether the passed `userId` is in `substitute_for` list of this user model.
     * In another word: safely check whether given user substitutes this user model.
     */
    public function isSubstituteForUserId(int $userId): bool
    {
        return in_array($userId, $this->getSubstitutedUserIds());
    }

    /**
     * Returns substituted users
     * @return static|static[]
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function getSubstitutedUsers()
    {
        $substitutedUserIds = $this->getSubstitutedUserIds();

        if ($this instanceof ItemCacheable) {
            return static::get($substitutedUserIds);
        } else {
            return static::bufferedInstance($substitutedUserIds);
        }
    }

    /**
     * @return int[]
     */
    public function getSubstitutedUserIds(): array
    {
        // todo: one beautiful day, switch it to PHP 8.0 `match()`
        switch (true) {
            case is_string($this['substitute_for']):
                $substituteFor = explode(',', $this['substitute_for']);
                break;

            case is_array($this['substitute_for']):
                $substituteFor = $this['substitute_for'];
                break;

            case isset($this['substitute_for']) || is_null($this['substitute_for']):
            default:
                $substituteFor = [];
                break;
        }

        // Cache the result in the class instance.
        $this['substitute_for'] = $substituteFor;

        return Filter::filterInts($this['substitute_for']);
    }

    /**
     * Returns substitute user.
     * @return User|static|null
     */
    public function getSubstitute(): ?User
    {
        if (!$this['substitute_user_id']) {
            return null;
        }

        return static::get($this['substitute_user_id']);
    }
}
