<?php

/**
 * Resource.php - model class for a resource
 *
 * The Resource class is the base class of the new
 * Room and Resource management system in Stud.IP.
 * It provides core functionality for handling general resources
 * and can be derived for handling special resources.
 *
 * 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 2 of
 * the License, or (at your option) any later version.
 *
 * @author      Moritz Strohm <strohm@data-quest.de>
 * @copyright   2017-2019
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 * @package     resources
 * @since       4.5
 *
 * @property string $id database column
 * @property string $parent_id database column
 * @property string $category_id database column
 * @property int|null $level database column
 * @property string $name database column
 * @property I18NString|null $description database column
 * @property int $requestable database column
 * @property int $lockable database column
 * @property int $mkdate database column
 * @property int $chdate database column
 * @property int $sort_position database column
 * @property SimpleORMapCollection|ResourceProperty[] $properties has_many ResourceProperty
 * @property SimpleORMapCollection|ResourcePermission[] $permissions has_many ResourcePermission
 * @property SimpleORMapCollection|ResourceRequest[] $requests has_many ResourceRequest
 * @property SimpleORMapCollection|ResourceBooking[] $bookings has_many ResourceBooking
 * @property SimpleORMapCollection|Resource[] $children has_many Resource
 * @property ResourceCategory $category belongs_to ResourceCategory
 * @property Resource $parent belongs_to Resource
 * @property mixed $class_name additional field
 */
class Resource extends SimpleORMap implements StudipItem
{
    protected static function configure($config = [])
    {
        $config['db_table'] = 'resources';

        $config['belongs_to']['category'] = [
            'class_name'  => ResourceCategory::class,
            'foreign_key' => 'category_id',
            'assoc_func'  => 'find'
        ];

        $config['has_many']['properties'] = [
            'class_name'        => ResourceProperty::class,
            'assoc_foreign_key' => 'resource_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store'
        ];

        $config['has_many']['permissions'] = [
            'class_name'        => ResourcePermission::class,
            'assoc_foreign_key' => 'resource_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store'
        ];

        $config['has_many']['requests'] = [
            'class_name'        => ResourceRequest::class,
            'assoc_foreign_key' => 'resource_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store'
        ];

        $config['has_many']['bookings'] = [
            'class_name'        => ResourceBooking::class,
            'assoc_foreign_key' => 'resource_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store'
        ];

        $config['has_many']['children'] = [
            'class_name' => Resource::class,
            'assoc_func' => 'findChildren',
            'on_delete'  => 'delete',
            'on_store'   => 'store'
        ];

        $config['belongs_to']['parent'] = [
            'class_name'  => Resource::class,
            'foreign_key' => 'parent_id'
        ];

        $config['i18n_fields']['description'] = true;

        $config['additional_fields']['class_name']        = ['category', 'class_name'];
        $config['registered_callbacks']['before_store'][] = 'cbValidate';

        parent::configure($config);
    }

    /**
     * This is a cache for permissions that users have on resources.
     * It is meant to reduce the database requests in cases where the
     * same permission is queried a lot of times.
     */
    protected static $permission_cache;

    /**
     * Returns the children of a resource.
     * The children are converted to an instance of the derived class,
     * if they are not instances of the default Resource class.
     */
    public static function findChildren($resource_id)
    {
        $children = self::findBySql(
            'parent_id = :parent_id ORDER BY name ASC',
            ['parent_id' => $resource_id]
        );

        if (!$children) {
            return [];
        }

        foreach ($children as &$child) {
            $child = $child->getDerivedClassInstance();
        }
        return $children;
    }

    /**
     * Returns a translation of the resource class name.
     * The translated name can be singular or plural, depending
     * on the value of the parameter $item_count.
     *
     * @param int $item_count The amount of items the translation shall be
     *     made for. This is only used to determine, if a singular or a
     *     plural form shall be returned.
     *
     * @return string The translated form of the class name, either in
     *     singular or plural.
     *
     */
    public static function getTranslatedClassName($item_count = 1)
    {
        return ngettext(
            'Ressource',
            'Ressourcen',
            $item_count
        );
    }

    /**
     * Retrieves all resources which don't have a parent resource assigned.
     * Such resources are called root resources since they are roots of
     * a resource hierarchy (or a resource tree).
     *
     * @return Resource[] An array of Resource objects
     *     which are root resources.
     */
    public static function getRootResources()
    {
        return self::findBySql("parent_id = '' ORDER BY name");
    }

    /**
     * A method for overloaded classes so that they can define properties
     * that are required for that resource class.
     *
     * @return string[] An array with the names of the required properties.
     *     Example: The properties with the names "foo", "bar" and "baz"
     *     are required properties. The array would have the following content:
     *     [
     *         'foo',
     *         'bar',
     *         'baz'
     *     ]
     */
    public static function getRequiredProperties()
    {
        return [];
    }


    /**
     * Returns the part of the URL for getLink and getURL which will be
     * placed inside the calls to URLHelper::getLink and URLHelper::getURL
     * in these methods.
     *
     * @param string $action The action for the resource.
     * @param string $id The ID of the resource.
     *
     * @return string The URL path for the specified action.
     * @throws InvalidArgumentException If $resource_id is empty.
     *
     */
    protected static function buildPathForAction($action = 'show', $id = null)
    {
        $actions_without_id = ['add'];
        if (!$id && !in_array($action, $actions_without_id)) {
            throw new InvalidArgumentException(
                _('Zur Erstellung der URL fehlt eine Ressourcen-ID!')
            );
        }

        switch ($action) {
            case 'show':
                return 'dispatch.php/resources/resource/index/' . $id;
            case 'add':
                return 'dispatch.php/resources/resource/add';
            case 'edit':
                return 'dispatch.php/resources/resource/edit/' . $id;
            case 'files':
                return 'dispatch.php/resources/resource/files/' . $id . '/';
            case 'permissions':
                return 'dispatch.php/resources/resource/permissions/' . $id;
            case 'temporary_permissions':
                return 'dispatch.php/resources/resource/temporary_permissions/' . $id;
            case 'booking_plan':
                return 'dispatch.php/resources/room_planning/booking_plan/' . $id;
            case 'request_plan':
                return 'dispatch.php/resources/room_planning/request_plan/' . $id;
            case 'semester_plan':
                return 'dispatch.php/resources/room_planning/semester_plan/' . $id;
            case 'assign-undecided':
                return 'dispatch.php/resources/booking/add/' . $id;
            case 'assign':
                return 'dispatch.php/resources/booking/add/' . $id . '/0';
            case 'reserve':
                return 'dispatch.php/resources/booking/add/' . $id . '/1';
            case 'lock':
                return 'dispatch.php/resources/booking/add/' . $id . '/2';
            case 'delete_bookings':
                return 'dispatch.php/resources/resource/delete_bookings/' . $id;
            case 'export_bookings':
                return 'dispatch.php/resources/export/resource_bookings/' . $id;
            case 'delete':
                return 'dispatch.php/resources/resource/delete/' . $id;
            default:
                return 'dispatch.php/resources/resource/show/' . $id;
        }
    }

    /**
     * Returns the appropriate link for the resource action that shall be
     * executed on a resource.
     *
     * @param string $action The action which shall be executed.
     *     For default Resources the actions 'show', 'add', 'edit' and 'delete'
     *     are defined.
     * @param string $id The ID of the resource on which the specified
     *     action shall be executed.
     * @param array $link_parameters Optional parameters for the link.
     *
     * @return string The Link for the resource action.
     * @throws InvalidArgumentException If $resource_id is empty.
     *
     */
    public static function getLinkForAction(
        $action = 'show',
        $id = null,
        $link_parameters = []
    )
    {
        return URLHelper::getLink(
            self::buildPathForAction($action, $id),
            $link_parameters
        );
    }

    /**
     * Returns the appropriate URL for the resource action that shall be
     * executed on a resource.
     *
     * @param string $action The action which shall be executed.
     *     For default Resources the actions 'show', 'add', 'edit' and 'delete'
     *     are defined.
     * @param string $id The ID of the resource on which the specified
     *     action shall be executed.
     * @param array $url_parameters Optional parameters for the URL.
     *
     * @return string The URL for the resource action.
     * @throws InvalidArgumentException If $resource_id is empty.
     *
     */
    public static function getURLForAction(
        $action = 'show',
        $id = null,
        $url_parameters = []
    )
    {
        return URLHelper::getURL(
            self::buildPathForAction($action, $id),
            $url_parameters
        );
    }

    /**
     * The SORM store method is overloaded to assure that the right level
     * attribute is stored.
     */
    public function store()
    {
        //Set the level attribute according to the parent's
        //level attribute. If no parents are defined
        //set the level to zero.
        if ($this->parent_id && $this->parent) {
            $this->level = $this->parent->level + 1;
        } else {
            $this->level = 0;
        }

        //Store the folder, if it hasn't been stored before:

        $folder = $this->getFolder();
        if ($folder) {
            $folder->store();
        }

        return parent::store();
    }

    public function delete()
    {
        //Delete the folder:

        $folder = $this->getFolder(false);
        if ($folder) {
            $folder->delete();
        }

        return parent::delete();
    }

    public function cbValidate()
    {
        if (!$this->category_id) {
            throw new InvalidResourceException(
                sprintf(
                    _('Die Ressource %s ist keiner Ressourcenkategorie zugeordnet!'),
                    $this->name
                )
            );
        }
        return true;
    }


    /**
     * @see StudipItem::__toString
     */
    public function __toString()
    {
        return $this->getFullName();
    }


    /**
     * Retrieves the folder for this resource.
     *
     * @param bool $create_if_missing Whether to create a folder (true) or
     *     not (false) in case no folder exists for this resource.
     *     Defaults to true.
     *
     * @returns ResourceFolder|null Either a ResourceFolder instance or null
     *     in case no such instance can be retrieved or created.
     */
    public function getFolder($create_if_missing = true)
    {
        $folder = Folder::findOneByRange_id($this->id);

        if ($folder) {
            $folder = $folder->getTypedFolder();

            if ($folder instanceof ResourceFolder) {
                //Only return ResourceFolder instances.
                return $folder;
            }
        } elseif ($create_if_missing) {
            $folder = $this->createFolder();
            if ($folder instanceof ResourceFolder) {
                return $folder;
            }
        }
        //In all other cases return null:
        return null;
    }

    public function setFolder(ResourceFolder $folder)
    {
        if ($this->isNew()) {
            $this->store();
        }

        $folder->range_id   = $this->id;
        $folder->range_type = 'Resource';

        return $folder->store();
    }

    public function createFolder()
    {
        if ($this->isNew()) {
            $this->id = $this->getNewId();
        }

        $folder = Folder::createTopFolder(
            $this->id,
            'Resource',
            'ResourceFolder'
        );

        if ($folder) {
            $folder = $folder->getTypedFolder();
            if ($folder) {
                $folder->store();
                return $folder;
            }
        }

        return null;
    }

    /**
     * Returns a list of property names that are required
     * for the resource class.
     *
     * @return string[] An array with the property names.
     */
    public function getRequiredPropertyNames()
    {
        return [];
    }


    /**
     * This is a simplified version of the createBooking method.
     * @param User $user
     * @param DateTime $begin
     * @param DateTime $end
     * @param int $preparation_time
     * @param string $description
     * @param string $internal_comment
     * @param int $booking_type
     * @return ResourceBooking
     * @see Resource::createBooking
     */
    public function createSimpleBooking(
        User $user,
        DateTime $begin,
        DateTime $end,
        $preparation_time = 0,
        $description = '',
        $internal_comment = '',
        $booking_type = ResourceBooking::TYPE_NORMAL
    )
    {
        return $this->createBooking(
            $user,
            $user->id,
            [
                [
                    'begin' => $begin,
                    'end'   => $end
                ]
            ],
            null,
            0,
            null,
            $preparation_time,
            $description,
            $internal_comment,
            $booking_type
        );
    }

    /**
     * Creates bookings from a request.
     * @param User $user
     * @param ResourceRequest $request The request from which
     *     a resource booking shall be built.
     * @param int $preparation_time
     * @param string $description
     * @param string $internal_comment
     * @param int $booking_type
     * @param bool $prepend_preparation_time . If this is set to true,
     *     the preparation time will end before the start of the
     *     requested time. If $prepend_preparation_time is set to false
     *     (the default) the preparation time starts with the start of the
     *     requested time.
     * @param bool $notify_lecturers
     * @return ResourceBooking[] A list of resource bookings
     *     matching the request.
     * @throws ResourceRequestException if the request could not be marked
     *     as resolved.
     *
     * @throws ResourceUnavailableException if the resource cannot be assigned
     *     in at least one of the time ranges specified by the resource request.
     */
    public function createBookingFromRequest(
        User $user,
        ResourceRequest $request,
        $preparation_time = 0,
        $description = '',
        $internal_comment = '',
        $booking_type = ResourceBooking::TYPE_NORMAL,
        $prepend_preparation_time = false,
        $notify_lecturers = false
    )
    {
        $course_dates = $request->getAffectedDates();

        $bookings = [];
        if ($course_dates) {
            foreach ($course_dates as $course_date) {
                $booking = $this->createBooking(
                    $user,
                    $course_date->id,
                    [
                        [
                            'begin' => (
                            $prepend_preparation_time
                                ? $course_date->date - $preparation_time
                                : $course_date->date
                            ),
                            'end'   => $course_date->end_time
                        ]
                    ],
                    null,
                    0,
                    $course_date->end_time,
                    $preparation_time,
                    $description,
                    $internal_comment,
                    $booking_type
                );

                if ($booking instanceof ResourceBooking) {
                    $bookings[] = $booking;
                }
            }
        } elseif (count($request->appointments)) {
            //It is a request for multiple single dates.
            //Such requests are resolved into multiple bookings.
            foreach ($request->appointments as $appointment) {
                $begin = (
                $prepend_preparation_time
                    ? $appointment->appointment->date - $preparation_time
                    : $appointment->appointment->date
                );
                $end   = $appointment->appointment->end_time;

                $booking = $this->createBooking(
                    $user,
                    $appointment->appointment_id,
                    [
                        [
                            'begin' => $begin,
                            'end'   => $end
                        ]
                    ],
                    null,
                    0,
                    $end,
                    $preparation_time,
                    $description,
                    $internal_comment,
                    $booking_type
                );

                if ($booking instanceof ResourceBooking) {
                    $bookings[] = $booking;
                }
            }
        } else {
            //No date objects for the request.
            //It is a simple request:
            $booking = $this->createBooking(
                $user,
                $request->user->id,
                [
                    [
                        'begin' => (
                        $prepend_preparation_time
                            ? $request->begin - $preparation_time
                            : $request->begin
                        ),
                        'end'   => $request->end
                    ]
                ],
                null,
                0,
                $request->end,
                $preparation_time,
                $description,
                $internal_comment,
                $booking_type
            );

            if ($booking instanceof ResourceBooking) {
                $bookings[] = $booking;
            }
        }

        if (!$request->closeRequest($notify_lecturers)) {
            throw new ResourceRequestException(
                _('Die Anfrage konnte nicht als bearbeitet markiert werden!')
            );
        }

        return $bookings;
    }

    /**
     * A factory method for creating a ResourceBooking object
     * for this resource.
     *
     * @param User $user The user who wishes to create a resource booking.
     * @param string $range_id The ID of the user (or the Stud.IP object)
     *     which owns the ResourceBooking.
     * @param array[][] $time_ranges The time ranges for the booking.
     *     At least one time range has to be specified using unix timestamps
     *     or DateTime objects.
     *     This array has the following structure:
     *     [
     *         [
     *             'begin' => The begin timestamp or DateTime object.
     *             'end' => The end timestamp or DateTime object.
     *         ]
     *     ]
     * @param DateInterval|null $repetition_interval The repetition interval
     *     for the new booking. This must be a DateInterval object if
     *     repetitions shall be stored.
     *     Otherwise this parameter must be set to null.
     * @param int $repeat_amount The amount of repetitions.
     *     This parameter is only regarded if $repetition_interval contains
     *     a DateInterval object.
     *     In case repetitions are specified by their end date set this
     *     parameter to 0.
     * @param DateTime|string|null $repetition_end_date The end date of the
     *     repetition. This can either be an unix timestamp or a DateTime object
     *     and will only be regarded if $repetition_interval contains a
     *     DateInterval object.
     *     In case repetitions are specified by their amount set this
     *     parameter to null.
     * @param int $repetition_amount (obsolete, has no effect)
     * @param int $preparation_time The preparation time which is needed before
     *     the real start time. This will be substracted
     *     from the begin timestamp and stored in an extra column of the
     *     resource_bookings table.
     * @param string $description An optional description for the booking.
     *     This fields was previously known as "user_free_name".
     * @param string $internal_comment An optional comment for the
     *     booking which is intended to be used internally
     *     in the room and resource administration staff.
     * @param int $booking_type The booking type.
     *     0 = normal booking
     *     1 = reservation
     *     2 = lock booking
     * @param bool $force_booking If this parameter is set to true,
     *     overlapping bookings are removed before storing this booking.
     *
     * @return ResourceBooking object.
     * @throws InvalidArgumentException If no time ranges are specified
     *     or if there is an error regarding the time ranges.
     * @throws ResourceBookingRangeException If $range_id is not set.
     * @throws ResourceBookingOverlapException If the booking overlaps
     *     with another booking or a resource lock.
     * @throws ResourcePermissionException If the specified user does not
     *     have sufficient permissions to create a resource booking.
     * @throws ResourceBookingException If the repetition interval
     *     is invalid or if the resource booking cannot be stored.
     *
     */
    public function createBooking(
        User $user,
        $range_id = null,
        $time_ranges = [],
        $repetition_interval = null,
        $repetition_amount = 0,
        $repetition_end_date = null,
        $preparation_time = 0,
        $description = '',
        $internal_comment = '',
        $booking_type = ResourceBooking::TYPE_NORMAL,
        $force_booking = false
    )
    {
        if (!is_array($time_ranges)) {
            throw new InvalidArgumentException(
                _('Es wurden keine Zeitbereiche für die Buchung angegeben!')
            );
        }

        $booking_begin = null;
        $booking_end   = null;

        //Check if each entry of the $time_intervals array is in the right
        //format and if it contains either timestamps or DateTime objects.
        //After that the time ranges are checked for validity (begin > end)
        //and if there are locks or bookings in one of the time ranges.
        //Furthermore all reservations that are affected by this booking
        //are collected so that the persons who made the reservations
        //can be informed about the new booking.
        $affected_reservations = [];
        foreach ($time_ranges as $index => $time_range) {
            $begin = $time_range['begin'];
            $end   = $time_range['end'];

            if ($begin === null || $end === null) {
                throw new InvalidArgumentException(
                    _('Mindestens eines der Zeitintervalls ist im falschen Format!')
                );
            }

            if (!($begin instanceof DateTime)) {
                $b = new DateTime();
                $b->setTimestamp($begin);
                $begin = $b;
            }
            if (!($end instanceof DateTime)) {
                $e = new DateTime();
                $e->setTimestamp($end);
                $end = $e;
            }

            $real_begin = clone $begin;
            if ($preparation_time > 0) {
                $real_begin = $real_begin->sub(
                    new DateInterval('PT' . $preparation_time . 'S')
                );
            }

            if ($real_begin > $end) {
                throw new InvalidArgumentException(
                    _('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!')
                );
            }

            $duration     = $end->getTimestamp() - $begin->getTimestamp();
            $min_duration = Config::get()->RESOURCES_MIN_BOOKING_TIME;
            if ($min_duration && ($duration < ($min_duration * 60))) {
                throw new InvalidArgumentException(
                    sprintf(
                        _('Die minimale Buchungsdauer von %1$d Minuten wurde unterschritten!'),
                        $min_duration
                    )
                );
            }

            if ($index == array_keys($time_ranges)[0]) {
                $booking_begin = clone $begin;
                $booking_end   = clone $end;
            }

            if ($repetition_interval instanceof DateInterval) {
                //We must calculate the end of the repetition interval
                //by using $repetition_amount or $repetition_end_date.
                $repetition_end = null;
                if ($repetition_end_date instanceof DateTime) {
                    $repetition_end = $repetition_end_date;
                } elseif ($repetition_end_date) {
                    //convert $repetition_end_date to a DateTime object:
                    $red = new DateTime();
                    $red->setTimestamp($repetition_end_date);
                    $repetition_end = $red;
                } else {
                    //$repetition_end_date is not set: Use $repetition_amount.
                    //Add the repetition interval $repetition_amount times
                    //to the $real_begin DateTime object to get the end date
                    //of the repetition:
                    $repetition = clone $real_begin;
                    for ($i = 0; $i < $repetition_amount; $i++) {
                        $repetition = $repetition->add($repetition_interval);
                    }
                    $repetition_end = $repetition;
                }

                $current_date = clone $real_begin;

                //Check for each repetition if the resource is available
                //or locked:
                while ($current_date <= $repetition_end) {
                    $current_begin = clone $current_date;
                    $current_end   = clone $current_date;
                    $current_end->setTime(
                        intval($end->format('H')),
                        intval($end->format('i')),
                        intval($end->format('s'))
                    );

                    if ($current_begin < $current_end) {
                        $affected_reservations = array_merge(
                            ResourceBooking::findByResourceAndTimeRanges(
                                $this,
                                [
                                    [
                                        'begin' => $current_begin->getTimestamp(),
                                        'end'   => $current_end->getTimestamp(),
                                    ]
                                ],
                                [1, 3]
                            ),
                            $affected_reservations
                        );
                    }

                    $current_date = $current_date->add($repetition_interval);
                }
            } else {
                $affected_reservations = array_merge(
                    ResourceBooking::findByResourceAndTimeRanges(
                        $this,
                        [
                            [
                                'begin' => $real_begin->getTimestamp(),
                                'end'   => $end->getTimestamp(),
                            ]
                        ],
                        [1, 3]
                    ),
                    $affected_reservations
                );
            }
        }

        $booking                  = new ResourceBooking();
        $booking->resource_id     = $this->id;
        $booking->booking_user_id = $user->id;
        $booking->range_id        = $range_id;
        $booking->description     = $description;
        $booking->begin           = $booking_begin->getTimestamp();
        $booking->end             = $booking_end->getTimestamp();

        if ($repetition_interval instanceof DateInterval) {
            if ($repetition_end_date) {
                if ($repetition_end_date instanceof DateTime) {
                    $booking->repeat_end = $repetition_end_date->getTimestamp();
                } else {
                    $booking->repeat_end = $repetition_end_date;
                }
            }

            $booking->repetition_interval = $repetition_interval->format('P%YY%MM%DD');
        }

        if ($preparation_time) {
            $booking->preparation_time = $preparation_time;
        } else {
            $booking->preparation_time = '0';
        }
        $booking->internal_comment = $internal_comment;
        $booking->booking_type     = (int)$booking_type;

        //We can finally store the new booking.

        try {
            $booking->store($force_booking);
        } catch (ResourceBookingOverlapException $e) {
            if ($begin->format('Ymd') == $end->format('Ymd')) {
                throw new ResourceBookingException(
                    sprintf(
                        _('%1$s: Die Buchung vom %2$s bis %3$s konnte wegen Überlappungen nicht gespeichert werden: %4$s'),
                        $this->getFullName(),
                        $begin->format('d.m.Y H:i'),
                        $end->format('H:i'),
                        $e->getMessage()
                    )
                );
            } else {
                throw new ResourceBookingException(
                    sprintf(
                        _('%1$s: Die Buchung vom %2$s bis %3$s konnte wegen Überlappungen nicht gespeichert werden: %4$s'),
                        $this->getFullName(),
                        $begin->format('d.m.Y H:i'),
                        $end->format('d.m.Y H:i'),
                        $e->getMessage()
                    )
                );
            }
        } catch (Exception $e) {
            if ($begin->format('Ymd') == $end->format('Ymd')) {
                throw new ResourceBookingException(
                    sprintf(
                        _('%1$s: Die Buchung vom %2$s bis %3$s konnte aus folgendem Grund nicht gespeichert werden: %4$s'),
                        $this->getFullName(),
                        $begin->format('d.m.Y H:i'),
                        $end->format('H:i'),
                        $e->getMessage()
                    )
                );
            } else {
                throw new ResourceBookingException(
                    sprintf(
                        _('%1$s: Die Buchung vom %2$s bis %3$s konnte aus folgendem Grund nicht gespeichert werden: %4$s'),
                        $this->getFullName(),
                        $begin->format('d.m.Y H:i'),
                        $end->format('d.m.Y H:i'),
                        $e->getMessage()
                    )
                );
            }
        }

        return $booking;
    }

    /**
     * This method creates a simple request for this resource.
     * A simple request is not bound to a date, metadate
     * or course object and its time ranges. Instead the time
     * range is specified directly.
     * Note that simple resource requests do not support recurrence.
     *
     * @param User $user The user who wishes to create a simple request.
     * @param DateTime $begin The begin timestamp of the request.
     * @param DateTime $end The end timestamp of the request.
     * @param string $comment A comment for the resource request.
     * @param int $preparation_time The requested preparation time before
     *     the begin of the requested time range. This parameter must be
     *     specified in seconds. Only positive values are accepted.
     *
     * @return ResourceRequest A resource request object.
     * @throws AccessDeniedException If the user is not permitted
     *     to request this resource.
     * @throws InvalidArgumentException If the the timestamps provided by
     *     $begin and $end are invalid or if $begin is greater than or equal
     *     to $end which results in an invalid time range.
     * @throws ResourceUnavailableException If the resource is not available
     *     in the selected time range.
     * @throws ResourceRequestException If the resource request
     *     cannot be stored.
     *
     */
    public function createSimpleRequest(
        User $user,
        DateTime $begin,
        DateTime $end,
        $comment = '',
        $preparation_time = 0
    )
    {
        //All users are permitted to create a request,
        //if the resource is requestable.

        if (!$this->requestable) {
            throw new InvalidArgumentException(
                _('Diese Ressource kann nicht angefragt werden!')
            );
        }

        if ($begin > $end) {
            throw new InvalidArgumentException(
                _('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!')
            );
        } elseif ($begin == $end) {
            throw new InvalidArgumentException(
                _('Startzeitpunkt und Endzeitpunkt sind identisch!')
            );
        }

        if (!$this->isAvailable($begin, $end)) {
            throw new ResourceUnavailableException(
                sprintf(
                    _('Die Ressource %1$s ist im Zeitraum von %2$s bis %3$s nicht verfügbar!'),
                    $this->name,
                    $begin->format('d.m.Y H:i'),
                    $end->format('d.m.Y H:i')
                )
            );
        }

        $request              = new ResourceRequest();
        $request->resource_id = $this->id;
        $request->category_id = $this->category_id;
        $request->user_id     = $user->id;

        $request->begin            = $begin->getTimestamp();
        $request->end              = $end->getTimestamp();
        $request->preparation_time = (
        $preparation_time > 0
            ? $preparation_time
            : 0
        );

        $request->closed  = '0';
        $request->comment = $comment;

        if (!$request->store()) {
            throw new ResourceRequestException(
                sprintf(
                    _('Die Anfrage zur Ressource %s konnte nicht gespeichert werden!'),
                    $this->name
                )
            );
        }

        return $request;
    }


    /**
     * This method creates a resource request for this resource.
     *
     * @param User $user The user who wishes to create a request.
     * @param string|array $date_range_ids One or more IDs of Stud.IP objects
     *     which can provide at least one time range.
     *     Objects which fulfill this requirement are
     *     course dates (CourseDate objects),
     *     cycle dates (SeminarCycleDate objects)
     *     and courses (Course objects).
     *     If only one ID is provided it can be passed as string.
     *     If multiple IDs are provided they have to be passed as array.
     * @param string $comment A comment for the resource request.
     * @param mixed[] $properties The wishable properties
     *     for the resource request. The format of the array is as follows:
     *     [
     *         'property name' => 'property state'
     *     ]
     * @param int $preparation_time The requested preparation time before
     *     the begin of the requested time range. This parameter must be
     *     specified in seconds. Only positive values are accepted.
     *
     * @return ResourceRequest A resource request object.
     * @throws InvalidArgumentException If $date_range_id is not set.
     *     or no object which can provide at least one time range
     *     can be found with the specified ID.
     * @throws ResourceNoTimeRangeException If no time range can be found
     *     by looking at the object, specified by its ID in $date_range_id.
     * @throws ResourceUnavailableException If the resource is not available
     *     in the selected time range.
     * @throws ResourceRequestException If the resource request
     *     cannot be stored.
     *
     */
    public function createRequest(
        User $user,
        $date_range_ids = null,
        $comment = '',
        $properties = [],
        $preparation_time = 0
    )
    {
        if (!$date_range_ids) {
            throw new InvalidArgumentException(
                _('Es wurde keine ID eines Objektes angegeben, welches Zeiträume für eine Ressourcenanfrage liefern kann!')
            );
        }

        if (!$this->requestable) {
            throw new InvalidArgumentException(
                _('Diese Ressource kann nicht angefragt werden!')
            );
        }

        //We must get the date ranges by looking at $date_range_id
        //and the object which lies behind that ID.

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

        $time_ranges = [];
        foreach ($date_range_ids as $date_range_id) {
            $time_ranges = array_merge(
                $time_ranges,
                ResourceManager::getTimeRangesFromRangeId(
                    $date_range_id
                )
            );
        }

        if (!$time_ranges) {
            //We couldn't find any time range.
            throw new ResourceNoTimeRangeException(
                sprintf(
                    _('Es konnte kein Zeitbereich für die Anfrage der Ressource %s gefunden werden.'),
                    $this->name
                )
            );
        }

        //Default resource request handling:
        //Check if the resource is available in all requested time ranges.

        foreach ($time_ranges as $time_range) {
            if (!$this->isAvailable($time_range[0], $time_range[1])) {
                throw new ResourceUnavailableException(
                    sprintf(
                        _('Die Ressource %1$s ist im Zeitraum vom %2$s bis %3$s nicht verfügbar!'),
                        $this->name,
                        $time_range[0]->format('d.m.Y H:i'),
                        $time_range[1]->format('d.m.Y H:i')
                    )
                );
            }
        }

        //We must check, if all the properties exist:
        if ($properties and is_array($properties)) {
            foreach ($properties as $property_name => $property_state) {
                $property_object = ResourcePropertyDefinition::findByName(
                    $property_name
                );

                if (!$property_object) {
                    throw new ResourcePropertyException(
                        sprintf(
                            _('Die Ressourceneigenschaft %s ist nicht definiert!'),
                            $property_name
                        )
                    );
                } elseif (count($property_object) > 1) {
                    throw new ResourcePropertyException(
                        sprintf(
                            _('Es gibt mehrere Ressourceneigenschaften mit dem Namen %s!'),
                            $property_name
                        )
                    );
                }

                //$property_object is an array of ResourcePropertyDefinition objects:
                $property_data[] = [
                    'object' => $property_object[0],
                    'state'  => $property_state
                ];
            }
        }

        $request                   = new ResourceRequest();
        $request->resource_id      = $this->id;
        $request->category_id      = $this->category_id;
        $request->user_id          = $user->id;
        $request->comment          = $comment;
        $request->preparation_time = (
        $preparation_time > 0
            ? $preparation_time
            : 0
        );
        $request->closed           = '0';

        //Resolve the date range ID and set the
        //appropriate field in the request object:
        if (count($date_range_ids) <= 1) {
            $course_date = CourseDate::find($date_range_ids[0]);
            if ($course_date) {
                $request->termin_id = $course_date->id;
            } else {
                $cycle_date = SeminarCycleDate::find($date_range_ids[0]);
                if ($cycle_date) {
                    $request->metadate_id = $cycle_date->id;
                } else {
                    $course = Course::find($date_range_ids[0]);
                    if ($course) {
                        $request->course_id = $course->id;
                    }
                }
            }

            if (!$request->store()) {
                throw new ResourceRequestException(
                    sprintf(
                        _('Die Anfrage zur Ressource %s konnte nicht gespeichert werden!'),
                        $this->name
                    )
                );
            }
        } else {
            if (!$request->store()) {
                throw new ResourceRequestException(
                    sprintf(
                        _('Die Anfrage zur Ressource %s konnte nicht gespeichert werden!'),
                        $this->name
                    )
                );
            }

            //More than one entry:
            //We must use ResourceBookingAppointment objects.
            foreach ($date_range_ids as $date_range_id) {
                $appointment_id = null;
                $course_date    = CourseDate::find($date_range_id);
                if ($course_date) {
                    $appointment_id = $course_date->id;
                } else {
                    $cycle_date = SeminarCycleDate::find($date_range_id);
                    if ($cycle_date) {
                        $appointment_id = $cycle_date->id;
                    } else {
                        $course = Course::find($date_range_id);
                        if ($course) {
                            $appointment_id = $course->id;
                        }
                    }
                }

                if ($appointment_id) {
                    $rra                 = new ResourceRequestAppointment();
                    $rra->request_id     = $request->id;
                    $rra->appointment_id = $appointment_id;
                    if (!$rra->store()) {
                        throw new ResourceRequestException(
                            _('Die Terminzuordnungen zur Anfrage konnten nicht gespeichert werden!')
                        );
                    }
                }
            }
        }

        //The request has been created: Now we need to link the properties:
        if(!empty($property_data)) {
            foreach ($property_data as $property) {
                $rrp              = new ResourceRequestProperty();
                $rrp->request_id  = $request->id;
                $rrp->property_id = $property['object']->id;
                $rrp->state       = intval($property['state']);
                if (!$rrp->store()) {
                    throw new InvalidResourceRequestException(
                        sprintf(
                            _('%1$s: Die Eigenschaft %2$s zur Anfrage konnte nicht gespeichert werden!'),
                            $this->getFullName(),
                            $property['object']->name
                        )
                    );
                }
            }
        }
        return $request;
    }

    /**
     * Creates a lock booking for this resource.
     *
     * @param User $user The user who wishes to create a lock booking.
     * @param DateTime $begin The begin of the lock time range.
     * @param DateTime $end The end of the lock time range.
     * @param string $internal_comment An optional comment for the
     *     lock booking which is intended to be used internally
     *     in the room and resource administration staff.
     *
     * @return ResourceBooking A ResourceBooking object.
     * @throws ResourceUnavailableException If a lock booking already
     *     exists in the specified time range.
     *
     * @throws AccessDeniedException If the user does not have sufficient
     *     permissions to lock this resource.
     */
    public function createLock(
        User $user,
        DateTime $begin,
        DateTime $end,
        $internal_comment = ''
    )
    {
        if (!$this->userHasPermission($user, 'admin', [$begin, $end])) {
            throw new AccessDeniedException(
                sprintf(
                    _('%s: Unzureichende Berechtigungen zum Erstellen einer Sperrbuchung!'),
                    $this->getFullName()
                )
            );
        }

        if ($this->isLocked($begin, $end)) {
            throw new ResourceUnavailableException(
                sprintf(
                    _('%1$s: Im Zeitbereich von %2$s bis %3$s gibt es bereits Sperrbuchungen!'),
                    $this->getFullName(),
                    $begin->format('d.m.Y H:i'),
                    $end->format('d.m.Y H:i')
                )
            );
        }

        $lock                   = new ResourceBooking();
        $lock->booking_type     = ResourceBooking::TYPE_LOCK;
        $lock->range_id         = $user->id;
        $lock->resource_id      = $this->id;
        $lock->begin            = $begin->getTimestamp();
        $lock->end              = $end->getTimestamp();
        $lock->internal_comment = $internal_comment;

        if (!$lock->store()) {
            throw new ResourceBookingException(
                sprintf(
                    _('%1$s: Fehler beim Speichern der Sperrbuchung für den Zeitbereich von %2$s bis %3$s!'),
                    $begin->format('d.m.Y H:i'),
                    $end->format('d.m.Y H:i')
                )
            );
        }

        return $lock;
    }

    /**
     * Retrieves the properties grouped by their property groups
     * and in the order specified in that group.
     *
     * @param string[] excluded_properties An array with the names
     *     of the properties that shall be excluded from the result set.
     *
     * @return array An array with the group names as keys and the properties
     *     in the second array dimension. The structure of the array
     *     is as follows:
     *     [
     *          group1 name => [
     *              property1,
     *              property2,
     *              ...
     *          ],
     *          group2 name => [
     *              ...
     *          ]
     *     ]
     */
    public function getGroupedProperties($excluded_properties = [])
    {
        if (is_array($excluded_properties) && count($excluded_properties)) {
            $properties = ResourceProperty::findBySql(
                "INNER JOIN resource_property_definitions rpd
                USING (property_id)
                LEFT JOIN resource_property_groups rpg
                ON rpd.property_group_id = rpg.id
                WHERE
                resource_properties.resource_id = :resource_id
                AND
                rpd.name NOT IN ( :excluded_properties )
                ORDER BY
                rpg.position ASC, rpg.name ASC,
                rpd.property_group_pos ASC, rpd.name ASC",
                [
                    'resource_id'         => $this->id,
                    'excluded_properties' => $excluded_properties
                ]
            );
        } else {
            $properties = ResourceProperty::findBySql(
                "INNER JOIN resource_property_definitions rpd
                USING (property_id)
                LEFT JOIN resource_property_groups rpg
                ON rpd.property_group_id = rpg.id
                WHERE
                resource_properties.resource_id = :resource_id
                ORDER BY
                rpg.position ASC, rpg.name ASC,
                rpd.property_group_pos ASC, rpd.name ASC",
                [
                    'resource_id' => $this->id
                ]
            );
        }

        if (!$properties) {
            return [];
        }

        $property_groups = [];
        foreach ($properties as $property) {
            if (!$property->state) {
                continue;
            }
            $group_name = '';
            if (!empty($property->definition->group->name)) {
                $group_name = $property->definition->group->name;
            }
            if (empty($property_groups[$group_name]) || !is_array($property_groups[$group_name])) {
                $property_groups[$group_name] = [];
            }
            $property_groups[$group_name][] = $property;
        }

        return $property_groups;
    }


    /**
     * Determines wheter this resource has a property
     * with the specified name.
     *
     * @param string $name The name of the resource property.
     *
     * @return bool True, if this resource has a property with
     *     the specified name, false otherwise.
     */
    public function propertyExists($name = '')
    {
        if (!$name) {
            return false;
        }

        $db = DBManager::get();

        $exists_stmt = $db->prepare(
            "SELECT TRUE FROM resource_properties
            INNER JOIN resource_property_definitions rpd
            ON resource_properties.property_id = rpd.property_id
            WHERE resource_properties.resource_id = :resource_id
                AND rpd.name = :name");

        $exists_stmt->execute(
            [
                'resource_id' => $this->id,
                'name'        => $name
            ]
        );

        $exists = $exists_stmt->fetchColumn(0);

        return (bool)$exists;
    }

    /**
     * Retrieves a ResourceProperty object for a property of this resource
     * which has the specified name. If the property has not been set for this
     * resource, but is defined for this resource's category, a new
     * ResourceProperty object will be created, stored and returned.
     *
     * @param string $name The name of the resource property.
     *
     * @return ResourceProperty|null Either a ResourceProperty object for
     *     the resource property matching the specified name or null,
     *     if no resource property with the specified name can be found.
     * @throws InvalidResourceCategoryException If this resource category
     * doesn't match the category of the resource object.
     *
     */
    public function getPropertyObject(string $name)
    {
        if (!$this->propertyExists($name)) {
            if ($name === 'geo_coordinates') {
                return null;
            }
            //A property with the name $name does not exist for this
            //resource object. If it is a defined property
            //we can still try to create it:

            if ($this->category->hasProperty($name)) {
                $property = $this->category->createDefinedResourceProperty(
                    $this,
                    $name
                );

                $property->store();
                return $property;
            } else {
                return null;
            }
        }
        return ResourceProperty::findOneBySql(
            "INNER JOIN resource_property_definitions rpd
                ON resource_properties.property_id = rpd.property_id
            WHERE resource_properties.resource_id = :resource_id
                AND rpd.name = :name",
            [
                'resource_id' => $this->id,
                'name'        => $name
            ]
        );
    }

    /**
     * Returns all info-label properties
     *
     * @return SimpleCollection
     */
    public function getInfolabelProperties()
    {
        return SimpleCollection::createFromArray(
            ResourceProperty::findBySQL('INNER JOIN `resource_property_definitions` USING (`property_id`)
                WHERE `info_label` = 1 AND `state` != "" AND `resource_id` = ?', [$this->id]
            )
        );
    }

    /**
     * Returns the state of the property specified by $name.
     * If the property has not been set for this resource, but is defined
     * for this resource's category, a new ResourceProperty object
     * will be created, stored and its state will be returned.
     *
     * @param string $name The name of the resource property.
     *
     * @return string|null The state of the specified property or null
     *     if the propery can't be found.
     */
    public function getProperty(string $name)
    {
        if (!$this->propertyExists($name)) {
            //A property with the name $name does not exist for this
            //resource object. If it is a defined property
            //we can still try to create it:

            if ($this->category->hasProperty($name)) {
                $property = $this->category->createDefinedResourceProperty(
                    $this,
                    $name,
                    ''
                );

                $property->store();
                return $property->state;
            } else {
                return null;
            }
        }

        $db = DBManager::get();

        $value_stmt = $db->prepare(
            "SELECT resource_properties.state FROM resource_properties
            INNER JOIN resource_property_definitions rpd
                ON resource_properties.property_id = rpd.property_id
            WHERE resource_properties.resource_id = :resource_id
                AND rpd.name = :name");

        $value_stmt->execute(
            [
                'resource_id' => $this->id,
                'name'        => $name
            ]
        );

        $value = $value_stmt->fetchColumn(0);

        if (!$value) {
            return null;
        }

        return $value;
    }

    /**
     * Retrieves an object by the state of a property of this resource,
     * specified by the property's name.
     * This method is useful for properties of type user, institute
     * or fileref. Those properties store IDs of User, Institute
     * or FileRef objects. Therefore the IDs can be resolved directly
     * to get the corresponding User, Institute or FileRef object directly.
     *
     * @param string $name The name of the resource property.
     *
     * @return SimpleORMap|null A SimpleORMap-based object or null,
     *     if no such object can be retrieved from the property's state.
     */
    public function getPropertyRelatedObject(string $name)
    {
        //Get the property state first:
        $property = $this->getPropertyObject($name);

        //Now we return the object which is referenced by the property's state:

        if ($property) {
            switch ($property->definition->type) {
                case 'user':
                    return User::find($property->state);
                case 'institute':
                    return Institute::find($property->state);
                case 'fileref' :
                    return FileRef::find($property->state);
                default:
                    //For all other property types where we cannot create an object
                    //we return the raw state value:
                    return $property->state;
            }
        }
        return null;
    }

    /**
     * Sets a specified property of this resource to the specified state.
     * If the property has not been set for this resource, but is defined
     * for this resource's category, a new ResourceProperty object
     * will be created, stored and its state will be returned.
     *
     * @param string $name The name of the resource property.
     * @param mixed $state The state of the resource property.
     * @param User|null $user The user who wishes to set the property.
     *
     * @return bool True, if the property state could be set, false otherwise.
     */
    public function setProperty(string $name, $state = '', $user = null)
    {
        if (!($user instanceof User)) {
            $user = User::findCurrent();
            if (!$user) {
                //We cannot continue without a user object!
                return false;
            }
        }

        //Get the minimum permission level required for modifying the property:

        if (!$this->userHasPermission($user, 'admin')) {
            throw new AccessDeniedException(
                sprintf(
                    _('Unzureichende Berechtigungen zum Ändern der Ressource %s!'),
                    $this->name
                )
            );
        }
        if (!$this->category->userHasPropertyWritePermissions($name, $user, $this)) {
            throw new AccessDeniedException(
                sprintf(
                    _('Unzureichende Berechtigungen zum Ändern der Eigenschaft %s!'),
                    $name
                )
            );
        }

        if (!$this->propertyExists($name)) {
            //A property with the name $name does not exist for this
            //resource object. If it is a defined property
            //we can still try to create it:

            if ($this->category->hasProperty($name)) {
                $property = $this->category->createDefinedResourceProperty(
                    $this,
                    $name,
                    $state
                );
                return $property->store();
            } else {
                return false;
            }
        }

        $property = $this->getPropertyObject($name);

        if ($property) {
            $property->state = $state;
            if ($property->isDirty()) {
                return $property->store();
            }
            return true;
        }

        return false;
    }

    /**
     * Sets the properties (specified by their names) to the specified values.
     *
     * @param array $properties The properties array in the format "key-value".
     *     The array keys must contain the property name while the
     *     items of the array contain the values.
     *     Example:
     *     ['bar' => 'foo']: Sets the value 'foo' for the property
     *     with the name 'bar'.
     *
     * @param User|null $user The user who wishes to set the properties.
     *     If this is left empty, the current user will be used.
     *
     * @return array If properties cannot be set, their names (as key) and the
     *     error messages (if any) are returned.
     *     The array has the following structure:
     *     [
     *         (property name) => (error message or empty string)
     *     ]
     */
    public function setPropertiesByName(array $properties, User $user)
    {
        $failed_properties = [];

        if (!($user instanceof User)) {
            $user = User::findCurrent();
            if (!$user) {
                //No property can be set.
                foreach ($properties as $name => $state) {
                    $failed_properties[$name] = '';
                }
                return $failed_properties;
            }
        }

        foreach ($properties as $name => $state) {
            try {
                $this->setProperty($name, $state, $user);
            } catch (Exception $e) {
                $this->failed_properties[$name] = $e->getMessage();
            }
        }

        return $failed_properties;
    }

    /**
     * Sets the properties (specified by their IDs) to the specified values.
     *
     * @param array $properties The properties array in the format "key-value".
     *     The array keys must contain the property-ID while the
     *     items of the array contain the values.
     *     Example:
     *     ['1' => 'foo']: Sets the value 'foo' for the property
     *     with the ID '1'.
     *
     * @param User|null $user The user who wishes to set the properties.
     *     If this is left empty, the current user will be used.
     *
     * @return array If properties cannot be set, their ids (as key) and the
     *     error messages (if any) are returned.
     *     The array has the following structure:
     *     [
     *         (property-ID) => (error message or empty string)
     *     ]
     */
    public function setPropertiesById(array $properties, User $user = null)
    {
        $failed_properties = [];

        if (!($user instanceof User)) {
            $user = User::findCurrent();
            if (!$user) {
                //No property can be set.
                foreach ($properties as $id => $state) {
                    $failed_properties[$id] = '';
                }
                return $failed_properties;
            }
        }

        foreach ($properties as $id => $state) {
            $property = ResourcePropertyDefinition::find($id);
            if (!$property) {
                //Invalid property:
                $this->failed_properties[$id] =
                    _('Die Eigenschaft wurde nicht gefunden!');
                continue;
            }
            try {
                $this->setProperty($property->name, $state, $user);
            } catch (Exception $e) {
                $failed_properties[$id] = $e->getMessage();
            }
        }

        return $failed_properties;
    }

    /**
     * Determines if the specified user has sufficient permissions to edit
     * the property specified by its name.
     *
     * @param string $name The name of the resource property.
     * @param user $user The user whose edit permissions shall be checked.
     *
     * @return bool True, if the user has edit permissions for the property,
     *     false otherwise.
     */
    public function isPropertyEditable(string $name, User $user)
    {
        return $this->category->userHasPropertyWritePermissions($name, $user, $this);
    }

    /**
     * Sets the state of a property by its definition_id rather than its name.
     *
     * @param string $property_definition_id The definition-ID of the property.
     * @param string $state The state of the property.
     *
     * @return bool True, if the property state can be stored, false otherwise.
     * @throws ResourcePropertyStateException If the provided state is invalid
     *     for the specified resource property.
     *
     */
    public function setPropertyByDefinitionId($property_definition_id = null, $state = null)
    {
        if (!$property_definition_id and !$state) {
            return false;
        }

        //Get property definition:
        $definition = ResourcePropertyDefinition::find($property_definition_id);
        if (!$definition) {
            return false;
        }

        //Check if the state matches the property definition's rules:
        $definition->validateState($state);

        //Check if the property for this resource already exists.
        //If so, update it. Otherwise create it.

        $property = ResourceProperty::findOneBySql(
            '(property_id = :property_id) AND (resource_id = :resource_id)',
            [
                'property_id' => $definition->id,
                'resource_id' => $this->id
            ]
        );

        if (!$property) {
            $property              = new ResourceProperty();
            $property->property_id = $definition->id;
            $property->resource_id = $this->id;
        }

        $property->state = $state;
        return $property->store();
    }

    /**
     * Sets the property state by specifying an SimpleORMap object.
     * This method is meant for resource properties of type user,
     * institute or fileref.
     *
     * @param string $name The name of the resource property.
     * @param SimpleORMap $object The object for the resource property.
     *
     * @return bool True, if the property has been saved, false otherwise.
     */
    public function setPropertyRelatedObject(string $name, SimpleORMap $object)
    {
        //Get the property state first:
        $property = $this->getPropertyObject($name);

        if (!$property) {
            return false;
        }

        //Now we return the object which is referenced by the property's state:

        switch ($property->definition->type) {
            case 'user':
                if (!($object instanceof User)) {
                    throw new ResourcePropertyException(
                        _("Eine Ressourceneigenschaft vom Typ 'user' benötigt ein Nutzer-Objekt zur Wertzuweisung!")
                    );
                }
            break;
            case 'institute':
                if (!($object instanceof Institute)) {
                    throw new ResourcePropertyException(
                        _("Eine Ressourceneigenschaft vom Typ 'institute' benötigt ein Institut-Objekt zur Wertzuweisung!")
                    );
                }
            break;
            case 'fileref':
                if (!($object instanceof FileRef)) {
                    throw new ResourcePropertyException(
                        _("Eine Ressourceneigenschaft vom Typ 'fileref' benötigt ein FileRef-Objekt zur Wertzuweisung!")
                    );
                }
            break;
            default:
            break;
        }

        //When no exception is thrown above we can set the object's ID
        //as the property's state:
        $property->state = $object->id;

        return $property->store();
    }

    /**
     * Deletes a property for a resource.
     *
     * @param string $name The name of the property to be deleted.
     *
     * @param User $user The user who wishes to delete the property.
     * @return number
     */
    public function deleteProperty(string $name, User $user)
    {
        //Get the user object and the minimum permission level
        //required for modifying the property:

        if (!$this->userHasPermission($user, 'admin')) {
            throw new AccessDeniedException(
                sprintf(
                    _('Unzureichende Berechtigungen zum Ändern der Ressource %s!'),
                    $this->name
                )
            );
        }
        if (!$this->category->userHasPropertyWritePermissions($name, $user)) {
            throw new AccessDeniedException(
                sprintf(
                    _('Unzureichende Berechtigungen zum Löschen der Eigenschaft %s!'),
                    $name
                )
            );
        }

        return ResourceProperty::deleteBySql(
            "INNER JOIN resource_property_definitions rpd
            ON resource_properties.property_id = rpd.property_id
            WHERE
            rpd.name = :name AND resource_properties.resource_id = :resource_id",
            [
                'name'        => $name,
                'resource_id' => $this->id
            ]
        );
    }

    /**
     * Returns the path for the resource's image.
     * If the resource has no image the path for a general
     * resource icon will be returned.
     *
     * Classes derived from the Resource class should only re-implement
     * this method if they have an alternative storage method for
     * resource pictures than the Stud.IP file system.
     *
     * @return string The URL to the resource picture.
     */
    public function getPictureUrl()
    {
        return '';
    }

    /**
     * Returns the default picture for the resource class.
     *
     * Classes derived from Resource should re-implement this method
     * if they want to get a different default picture than the resource icon.
     * The call to getPictureUrl will call the getDefaultPictureUrl method
     * from the derived class.
     *
     * @return string The URL to the picture.
     */
    public function getDefaultPictureUrl()
    {
        return $this->getIcon()->asImagePath();
    }

    /**
     * Returns the Icon for the resource class.
     *
     * Classes derived from Resource should re-implement this method
     * if they want to get a different icon than the resource icon.
     * @param string $role
     * @return Icon The icon for the resource.
     */
    public function getIcon($role = Icon::ROLE_INFO)
    {
        return Icon::create('resources', $role);
    }

    /**
     * Returns all properties in a two-dimensional array with the following
     * property data inside of the second dimension:
     * [
     *     'name' => (the property's name)
     *     'display_name' => (the display name of the property)
     *     'type' => (the property's type)
     *     'state' => (the property's state)
     *     'requestable' => (if the property is requestable or not (true or false))
     * ]
     *
     * @param bool $only_requestable_properties If only requestable properties
     *     shall be returned set this to true. If all properties shall be
     *     returned, set this to false.
     *
     * @return array[] A two-dimensional array containing property data.
     */
    public function getPropertyArray($only_requestable_properties = false)
    {
        $property_array = [];

        if ($this->properties) {
            foreach ($this->properties as $property) {
                if ($only_requestable_properties) {
                    $category_property = ResourceCategoryProperty::findByNameAndCategoryId(
                        $property->name,
                        $this->category_id
                    );

                    if ($category_property) {
                        if ($category_property->requestable) {
                            $property_array[] = [
                                'name'         => $property->name,
                                'display_name' => $property->display_name,
                                'type'         => $property->type,
                                'state'        => $property->state,
                                'requestable'  => $property->isRequestable()
                            ];
                        }
                    }
                } else {
                    $property_array[] = [
                        'name'         => $property->name,
                        'display_name' => $property->display_name,
                        'type'         => $property->type,
                        'state'        => $property->state,
                        'requestable'  => $property->isRequestable()
                    ];
                }
            }
        }
        return $property_array;
    }

    /**
     * Shortcut method for ResourceBooking::countByResourceAndTimeRanges.
     * Determines whether normal resource bookings exist
     * in the specified time range.
     *
     * @param DateTime $begin Time range start timestamp.
     *
     * @param DateTime $end Time range end timestamp.
     *
     * @param array $excluded_booking_ids The IDs of bookings that shall
     *     be excluded from the determination of the "assigned" status.
     *
     * @return bool True, if the resource is assigned in the specified
     *     time range, false otherwise.
     */
    public function isAssigned(
        DateTime $begin,
        DateTime $end,
        $excluded_booking_ids = []
    )
    {
        return ResourceBooking::countByResourceAndTimeRanges(
                $this,
                [
                    [
                        'begin' => $begin->getTimestamp(),
                        'end'   => $end->getTimestamp()
                    ]
                ],
                [0],
                $excluded_booking_ids
            ) > 0;
    }

    /**
     * Shortcut method for ResourceBooking::countByResourceAndTimeRanges.
     * Determines whether resource reservations exist
     * in the specified time range.
     *
     * @param DateTime $begin Time range start timestamp.
     *
     * @param DateTime $end Time range end timestamp.
     *
     * @param array $excluded_reservation_ids The IDs of reservation bookings that shall
     *     be excluded from the determination of the "reserved" status.
     *
     * @return bool True, if the resource is reserved in the specified
     *     time range, false otherwise.
     */
    public function isReserved(
        DateTime $begin,
        DateTime $end,
        $excluded_reservation_ids = []
    )
    {
        //One second is added to the begin timestamp to avoid
        //getting "false" overlaps where another booking ends on exactly
        //the begin timestamp.
        return ResourceBooking::countByResourceAndTimeRanges(
                $this,
                [
                    [
                        'begin' => $begin->getTimestamp(),
                        'end'   => $end->getTimestamp()
                    ]
                ],
                [1, 3],
                $excluded_reservation_ids
            ) > 0;
    }

    /**
     * Shortcut method for ResourceBooking::countByResourceAndTimeRanges.
     * Determines whether resource locks exist
     * in the specified time range.
     *
     * @param DateTime $begin Time range start timestamp.
     *
     * @param DateTime $end Time range end timestamp.
     *
     * @param array $excluded_lock_ids The IDs of lock bookings that shall
     *     be excluded from the determination of the "locked" status.
     *
     * @return bool True, if the resource is locked in the specified
     *     time range, false otherwise.
     */
    public function isLocked(
        DateTime $begin,
        DateTime $end,
        $excluded_lock_ids = []
    )
    {
        //One second is added to the begin timestamp to avoid
        //getting "false" overlaps where another booking ends on exactly
        //the begin timestamp.
        return ResourceBooking::countByResourceAndTimeRanges(
                $this,
                [
                    [
                        'begin' => $begin->getTimestamp(),
                        'end'   => $end->getTimestamp()
                    ]
                ],
                [2],
                $excluded_lock_ids
            ) > 0;
    }

    /**
     * Determines, if the resource is available (not assigned or locked)
     * in a specified time range.
     *
     * @param DateTime $begin Time range start timestamp.
     * @param DateTime $end Time range end timestamp.
     *
     * @param array $excluded_booking_ids The IDs of available bookings that shall
     *     be excluded from the determination of the "available" status.
     *
     * @return bool True, if the resource is available in the specified
     *     time range, false otherwise.
     */
    public function isAvailable(
        DateTime $begin,
        DateTime $end,
        $excluded_booking_ids = []
    )
    {
        return ResourceBooking::countByResourceAndTimeRanges(
                $this,
                [
                    [
                        'begin' => $begin->getTimestamp(),
                        'end'   => $end->getTimestamp()
                    ]
                ],
                [0, 2],
                $excluded_booking_ids
            ) == 0;
    }

    /**
     * Determines, if the resource is available (not assigned or locked)
     * in the time ranges specified by a resource request.
     *
     * @param ResourceRequest $request A resource request object.
     *
     * @return bool True, if the resource is available in the
     *     time ranges of the resource request, false otherwise.
     */
    public function isAvailableForRequest(ResourceRequest $request)
    {
        $time_intervals = $request->getTimeIntervals(true);
        if (!$time_intervals) {
            //Without a single time interval we cannot check
            //if the resource is available.
            return false;
        }
        foreach ($time_intervals as $time_interval) {
            $begin = new DateTime();
            $end   = new DateTime();
            $begin->setTimestamp($time_interval['begin']);
            $end->setTimestamp($time_interval['end']);

            if (!$this->isAvailable($begin, $end)) {
                //The resource is not available in the time interval.
                //We can stop here and return false.
                return false;
            }

            //If code execution reaches this point the resource is
            //available in all time intervals of the resource request:
            return true;
        }
    }

    /**
     * Returns the full (localised) name of the resource.
     *
     * @return string The full name of the resource.
     */
    public function getFullName()
    {
        return sprintf(
            _('Ressource %s'),
            $this->name
        );
    }

    /**
     * Sets the permission for one user for this resource.
     *
     * @param User $user The user whose permission shall be set.
     * @param string $perm The permission level for the specified user.
     *     The levels 'user', 'autor', 'tutor' and 'admin' are allowed.
     *
     * @return bool True, if the permission has been stored successfully,
     *     false otherwise.
     */
    public function setUserPermission(User $user, $perm = 'autor')
    {
        if (!in_array($perm, ['user', 'autor', 'tutor', 'admin'])) {
            return false;
        }

        $perm_object = ResourcePermission::findOneBySql(
            '(user_id = :user_id) AND (resource_id = :resource_id)',
            [
                'user_id'     => $user->id,
                'resource_id' => $this->id
            ]
        );

        if (!$perm_object) {
            $perm_object              = new ResourcePermission();
            $perm_object->user_id     = $user->id;
            $perm_object->resource_id = $this->id;
        }

        $perm_object->perms = $perm;
        $stored = (bool)$perm_object->store();
        if ($stored) {
            if (!isset(self::$permission_cache[$this->id])) {
                self::$permission_cache[$this->id] = [];
            }
            //Update the permission cache.
            self::$permission_cache[$this->id][$user->id] = $perm;
        }
        return $stored;
    }

    /**
     * Deletes the permission a specified user has on this resource.
     *
     * @param User $user The user whose permission shall be deleted.
     *
     * @return bool True
     */
    public function deleteUserPermission(User $user)
    {
        $deleted = ResourcePermission::deleteBySql(
            '(user_id = :user_id) AND (resource_id = :resource_id)',
            [
                'user_id'     => $user->id,
                'resource_id' => $this->id
            ]
        );

        if ($deleted && is_array(self::$permission_cache[$this->id])) {
            //Update the permission cache.
            self::$permission_cache[$this->id][$user->id] = null;
        }

        return true;
    }

    /**
     * Deletes all permissions of all users for this resource.
     *
     * @return bool True
     */
    public function deleteAllPermissions()
    {
        ResourcePermission::deleteBySql(
            'resource_id = :resource_id',
            [
                'resource_id' => $this->id
            ]
        );

        //Update the permission cache:
        self::$permission_cache[$this->id] = [];

        return true;
    }

    /**
     * Retrieves the permission level a specified user
     * has on this resource.
     *
     * Setting the optional $time_range parameter will also enable checks for
     * temporary global permissions.
     *
     * @param User $user The user whose permission shall be retrieved.
     *
     * @param array $time_range (DateTime) This is an optional parameter that can
     *     be used to pass two DateTime objects to this method. The first object
     *     will be treated as the begin timestamp and the second one as the
     *     end timestamp.
     *
     * @param bool $permanent_only Whether to retrieve only permanent permissions
     *     (true) or permanent and temporary permissions (false).
     *     Defaults to false.
     *
     * @return string The permission level, expressed as string.
     *     The level can be 'user', 'autor', 'tutor' or 'admin'.
     */
    public function getUserPermission(User $user, $time_range = [], $permanent_only = false)
    {
        if (ResourceManager::getGlobalResourcePermission($user) === 'admin') {
            return 'admin';
        }


        $perm_string = '';
        $temp_perm   = null;

        $begin = time();
        $end   = $begin;
        //Check for a temporary permission first:
        //check only against current timestamp
        if (!$permanent_only) {
            $temp_perm = ResourceTemporaryPermission::findOneBySql(
                '(resource_id = :resource_id) AND (user_id = :user_id)
                AND (begin <= :begin) AND (end >= :end)',
                [
                    'resource_id' => $this->id,
                    'user_id'     => $user->id,
                    'begin'       => $begin,
                    'end'         => $end
                ]
            );
        }

        if ($temp_perm) {
            $perm_string = $temp_perm->perms;
        } else {
            //No temporary permission exist or has been retrieved.
            //Check for a "normal" permission.
            $cached_perms = self::$permission_cache[$this->id][$user->id] ?? null;
            if ($cached_perms === null) {
                //The permission of the specified user is not in the
                //permission cache. Load it from the database and store
                //it in the permission cache before returning it.
                $perms = ResourcePermission::findOneBySql(
                    '(resource_id = :resource_id) AND (user_id = :user_id)',
                    [
                        'resource_id' => $this->id,
                        'user_id'     => $user->id
                    ]
                );
                if ($perms) {
                    if (!isset(self::$permission_cache[$this->id])) {
                        self::$permission_cache[$this->id] = [];
                    }
                    self::$permission_cache[$this->id][$user->id] = $perms->perms;
                    $perm_string                                  = $perms->perms;
                }
            } else {
                $perm_string = $cached_perms;
            }
        }

        if (!$perm_string) {
            //A user which doesn't have special permissions for this resource
            //can have global resource permissions:
            $global_perm = ResourceManager::getGlobalResourcePermission($user);
            if ($global_perm) {
                //Set the permission cache:
                if (!isset(self::$permission_cache[$this->id])) {
                    self::$permission_cache[$this->id] = [];
                }
                self::$permission_cache[$this->id][$user->id] = $global_perm;
            }
            $perm_string = $global_perm;
        }
        //Now we must check for global resource locks:
        if ($perm_string && $time_range && $this->lockable) {

            if ($time_range[0] instanceof DateTime) {
                $begin = $time_range[0]->getTimestamp();
            } else {
                $begin = $time_range[0];
            }
            if ($time_range[1] instanceof DateTime) {
                $end = $time_range[1]->getTimestamp();
            } else {
                $end = $time_range[1];
            }
            if (GlobalResourceLock::isLocked($begin, $end)) {
                //A permission level exists for the user.
                //The user gets "user" permissions in case
                //a global lock is active.
                $perm_string = 'user';
            }
        }

        //No global resource lock exists. We must return
        //the permission string if it is set:
        if ($perm_string) {
            return $perm_string;
        }

        return '';
    }

    /**
     * Determines if a user has the specified permission.
     *
     * @param ?User $user The user whose permissions shall be checked on this
     *     resource object. May be null.
     * @param string $permission The permission level.
     * @param $time_range @TODO
     *
     * @return bool True, if the specified user has the specified permission,
     *     false otherwise.
     */
    public function userHasPermission(
        ?User $user,
        string $permission = 'user',
        array $time_range = []
    )
    {
        if (!in_array($permission, ['user', 'autor', 'tutor', 'admin']) || $user === null) {
            return false;
        }


        if (ResourceManager::getGlobalResourcePermission($user) === 'admin') {
            return true;
        }

        $perm_level = $this->getUserPermission($user, $time_range);

        if ($permission === 'user') {
            //No check for global resource locks here:
            //If only user permissions are requested we can safely grant them
            //since 'user' users may only perform reading actions but
            //no writing actions.
            if (in_array($perm_level, ['user', 'autor', 'tutor', 'admin'])) {
                return true;
            } else {
                return false;
            }
        } elseif ($permission === 'autor') {
            if (in_array($perm_level, ['autor', 'tutor', 'admin'])) {
                return true;
            } else {
                return false;
            }
        } elseif ($permission === 'tutor') {
            if (in_array($perm_level, ['tutor', 'admin'])) {
                return true;
            } else {
                return false;
            }
        } elseif ($permission === 'admin') {
            if ($perm_level == 'admin') {
                return true;
            } else {
                return false;
            }
        }
        //Code execution should be finished at this point.
        //If this point is reached the user has no permissions for the
        //resource management system at all.
        return false;
    }

    /**
     * Determines whether the user may create a child resource
     * on this resource.
     *
     * @param User $user The user whose permission to create a child
     *     resource shall be checked.
     *
     * @return bool True, if the user may create a child resource
     *     on this resource, false otherwise.
     */
    public function userMayCreateChild(User $user)
    {
        return $this->userHasPermission($user, 'admin');
    }

    /**
     * Checks if the specified user has sufficient permissions to make resource
     * requests, according to the setting RESOURCES_MIN_REQUEST_PERMISSION.
     * This permission check is only relevant for creating requests that are not
     * bound to a course.
     *
     * @param User $user The user whose request permissions shall be checked.
     *
     * @return bool True, if the user has request permissions, false otherwise.
     */
    public function userHasRequestRights(User $user)
    {
        if (!Config::get()->RESOURCES_ALLOW_ROOM_REQUESTS || !$this->booking_plan_request) {
            return false;
        }
        $min_perm = Config::get()->RESOURCES_MIN_REQUEST_PERMISSION;
        if (!in_array($min_perm, ['', 'user', 'autor', 'tutor', 'admin'])) {
            //Invalid permission level!
            return false;
        }
        if (!$min_perm) {
            //No minimum permission set: Every logged-in user
            //can create requests.
            return true;
        }
        return $this->userHasPermission($user, $min_perm);
    }

    /**
     * Determines whether the user may book the resource or not.
     * An optional time range can be set to check  the user's
     * temporary permissions on another date than the current date.
     *
     * @param User $user The user whose booking permissions shall be checked.
     *
     * @param int|string|DateTime $begin The begin timestamp of the
     *     optional time range.
     *
     * @param int|string|DateTime $end The end timestamp of the
     *     optional time range.
     *
     * @return bool True, if the user may book the resource, false otherwise.
     */
    public function userHasBookingRights(
        User $user,
        $begin = null,
        $end = null
    )
    {
        if ($begin && $end) {
            $time_range = [$begin, $end];
        } else {
            $time_range = [];
        }

        //Check the permissions on this resource and the global permissions:
        return $this->userHasPermission($user, 'autor', $time_range);
    }

    /**
     * Determines if the booking plan of the resource is visible for a
     * specified user.
     *
     * @param ?User $user The user whose permission to view the booking plan
     *     shall be determined. May be null.
     *
     * @param DateTime[] $time_range An optional time range for the
     *     permission check.
     * @return bool True, if the user can see the resource booking plan,
     *     false otherwise.
     * @see Resource::getUserPermission
     *
     */
    public function bookingPlanVisibleForUser(?User $user, $time_range = [])
    {
        return $this->userHasPermission($user, 'user', $time_range);
    }

    /**
     * Retrieves a parent resource object that matches the specified
     * class name. The search stops when either a parent resource
     * with the class name is found or when the root resource object
     * is reached.
     *
     * @param string $class_name The class name of the parent.
     *
     * @return Resource|null Either a resource object or null
     *     in case a matching parent resource cannot be found.
     */
    public function findParentByClassName($class_name = 'Resource')
    {
        $resource_ids = [$this->id];
        $resource     = $this->parent;

        while ($resource) {
            //We should check for circular hierarchies first
            //to avoid an endless while loop:
            if (in_array($resource->id, $resource_ids)) {
                //We have a circular hierarchy: this resource is
                //the parent of itself which is an invalid state!
                throw new InvalidResourceException(
                    sprintf(
                        _('Zirkuläre Hierarchie: Die Ressource %1$s ist ein Elternknoten von sich selbst!'),
                        $resource->name
                    )
                );
            }
            if (is_a($resource->class_name, $class_name, true)) {
                //We have found a parent node which has the
                //specified class name: return that parent.
                return $resource;
            }
            //The current parent was not the one we were looking for.
            //Therefore we must go one layer up in the resource
            //hierarchy and continue search:
            $resource_ids[] = $resource->id;
            $resource       = $resource->parent;
        }
        //The search was not successful:
        //We have reached the root resource (whose parent_id field
        //is set to an equivalend of NULL) and we haven't found a
        //resource matching the specified class name.
        return null;
    }

    /**
     * This method searches the hierarchy below this resource
     * to find resources matching the specified class name.
     * Via the optional parameter $depth the search can be limited
     * to a specific amount of layers.
     *
     * @param string $class_name The name of the resource class
     *     where resources shall be found to.
     * @param int $depth The (optional) maximum depth below this resource
     *     which shall be searched.
     * @param bool $convert_objects True, if objects shall be converted to
     *     $class_name (default), false otherwise.
     * @param bool $order_by_name Order the children by name.
     *     Defaults to true.
     *
     * @return Resource[] An array of resource objects or an empty array
     *     if no matching resources can be found.
     */
    public function findChildrenByClassName(
        $class_name = 'Resource',
        $depth = 0,
        $convert_objects = true,
        $order_by_name = true
    )
    {
        $result = [];
        if ($this->children) {
            //this resource has children: iterate over them and
            //check if they match the search criteria.
            foreach ($this->children as $child) {
                if (is_a($child->class_name, $class_name, true)) {
                    if ($convert_objects) {
                        $result[] = $child->getDerivedClassInstance();
                    } else {
                        $result[] = $child;
                    }
                }
                if (($depth > 1) || ($depth == 0)) {
                    //Search the child and lower depth by one when calling this
                    //method on the child.
                    $result = array_merge(
                        $result,
                        $child->findChildrenByClassName(
                            $class_name,
                            (($depth > 1) ? $depth - 1 : 0),
                            $convert_objects
                        )
                    );
                }
            }
            if ($order_by_name) {
                usort(
                    $result,
                    function ($a, $b) {
                        if ($a->name == $b->name) {
                            return 0;
                        } elseif ($a->name < $b->name) {
                            return -1;
                        } else {
                            return 1;
                        }
                    }
                );
            }
        }
        return $result;
    }

    /**
     * Adds a resource as child resource to this resource.
     *
     * @param Resource $resource The child resource.
     *
     * @return bool True on success, false on failure.
     */
    public function addChild(Resource $resource)
    {
        $old_parent    = $resource->parent;
        $old_parent_id = $resource->parent_id;

        $resource->parent    = $this;
        $resource->parent_id = $this->id;

        if (!$resource->checkHierarchy()) {
            //We must revert the parent fields since $resource
            //may be used in other code pieces afterwards.
            $resource->parent    = $old_parent;
            $resource->parent_id = $old_parent_id;
            throw new InvalidArgumentException(
                sprintf(
                    _('Die Ressource %1$s (Typ %2$s) kann nicht unterhalb der Ressource %3$s (Typ %4$s) platziert werden!'),
                    $resource->name,
                    $resource->class_name,
                    $this->name,
                    $this->class_name
                )
            );
        }
        if ($resource->isDirty()) {
            //Only store the resource object if setting the parent_id field
            //did change it:
            return $resource->store();
        }
        //The resource object hasn't changed by setting the parent_id field:
        //We can return true.
        return true;
    }

    /**
     * Get all resource requests for the resource in a given timeframe.
     *
     * @param DateTime $begin Begin of timeframe.
     * @param DateTime $end End of timeframe.
     *
     * @return ResourceRequest[] An array of ResourceRequest objects.
     */
    public function getOpenResourceRequests(DateTime $begin, DateTime $end)
    {
        //We must get all requests that either have a start and end date
        //set or that have a start date, repeate end, repeat interval and
        //repeat quantity set.

        return ResourceRequest::findByResourceAndTimeRanges(
            $this,
            [
                [
                    'begin' => $begin->getTimestamp(),
                    'end'   => $end->getTimestamp()
                ]
            ],
            0
        );
    }

    /**
     * Get all resource bookings for the resource in a given timeframe.
     *
     * @param DateTime $begin Begin of timeframe.
     * @param DateTime $end End of timeframe.
     * @param array $booking_types
     *
     * @return ResourceBooking[] An array of ResourceBooking objects.
     */
    public function getResourceBookings(DateTime $begin, DateTime $end, array $booking_types = [0])
    {
        return ResourceBooking::findByResourceAndTimeRanges(
            $this,
            [
                [
                    'begin' => $begin->getTimestamp(),
                    'end' => $end->getTimestamp()
                ]
            ],
            $booking_types
        );
    }


    /**
     * Get all resource locks for the resource in a given timeframe.
     *
     * @param DateTime $begin Begin of timeframe.
     * @param DateTime $end End of timeframe.
     *
     * @return ResourceBooking[] An array of ResourceBooking objects.
     */
    public function getResourceLocks(DateTime $begin, DateTime $end)
    {
        return ResourceBooking::findByResourceAndTimeRanges(
            $this,
            [
                [
                    $begin->getTimestamp(),
                    $end->getTimestamp()
                ]
            ],
            [2]
        );
    }


    /**
     * Determines if files are attached to this resource.
     * If a folder exists for this resource its files are counted.
     * Depending on whether the folder has files in it or not
     * this method returns true or false.
     *
     * @return bool True, if there are files attached to this resource,
     *     false otherwise.
     */
    public function hasFiles()
    {
        $folder = Folder::findOneBySql(
            'range_id = :range_id',
            [
                'range_id' => $this->id
            ]
        );

        if (!$folder) {
            return false;
        }

        //Since files from resources shall always be stored in the
        //Stud.IP file system we can skip the conversion from Folder
        //to FolderType and count the FileRef-objects for this resource
        //directly in the database. Since resource folders do not
        //have subfolders we will count any file of the resource:
        return FileRef::countBySql(
                'folder_id = :folder_id',
                [
                    'folder_id' => $folder->id
                ]
            ) > 0;
    }

    /**
     * Converts a Resource object to an object of a specialised resource class.
     *
     * @return Resource|other An object of a specialised resource class
     *     or a Resource object, if the resource is a standard resource
     *     with the class_name 'Resource' in its resource category.
     *     If the derived resource class is not available, an instance of
     *     BrokenResource is returned.
     */
    public function getDerivedClassInstance()
    {
        $class_name = $this->class_name;

        if ($class_name == 'Resource') {
            //It is a standard resource which is managed by this class.
            return $this;
        }

        if (is_subclass_of($class_name, 'Resource')) {
            $converted_resource = $class_name::buildExisting(
                $this->toRawArray()
            );
            return $converted_resource;
        } else {
            //$class_name does not contain the name of a subclass
            //of Resource. That's an error!
            $broken_resource = BrokenResource::buildExisting(
                $this->toRawArray()
            );
            return $broken_resource;
        }
    }

    /**
     * Checks if the place in the resource hierarchy (resource tree)
     * is correct for this resource.
     * This method has no function in this class but can be filled
     * with logic in one of the classes derived from Resource.
     *
     * @return bool True, if this resource is correctly placed,
     *     false otherwise.
     * @throws NoResourceClassException
     *     if the class name of this resource is not a derived class
     *     of the Resource class.
     *
     */
    public function checkHierarchy()
    {
        if ($this->class_name == 'Resource') {
            //Objects of the Resource class are always in the right
            //place of the resource hierarchy.
            return true;
        }

        //The object does not use the Resource class name and uses
        //a derived class instead. We must check the hierarchy
        //using the checkHierarchy method of the derived class.

        $converted_resource = $this->getDerivedClassInstance();
        return $converted_resource->checkHierarchy();
    }

    /**
     * Returns the link for an action for this resource.
     * This is the non-static variant of Resource::getLinkForAction.
     *
     * @param string $action The action which shall be executed.
     *     For default Resources the actions 'show', 'add', 'edit' and 'delete'
     *     are defined.
     * @param array $link_parameters Optional parameters for the link.
     * @return string @TODO
     */
    public function getActionLink($action = 'show', $link_parameters = [])
    {
        //We must check the class name and call the appropriate
        //getLinkForAction method for derived classes:

        $class_name = $this->class_name;
        if (is_subclass_of($class_name, 'Resource')) {
            return $class_name::getLinkForAction(
                $action,
                $this->id,
                $link_parameters
            );
        } else {
            return self::getLinkForAction(
                $action,
                $this->id,
                $link_parameters
            );
        }
    }

    /**
     * Returns the URL for an action for this resource.
     * This is the non-static variant of Resource::getURLForAction.
     *
     * @param string $action The action which shall be executed.
     *     For default Resources the actions 'show', 'add', 'edit' and 'delete'
     *     are defined.
     * @param array $url_parameters Optional parameters for the URL.
     * @return string @TODO
     */
    public function getActionURL($action = 'show', $url_parameters = [])
    {
        //We must check the class name and call the appropriate
        //getURLForAction method for derived classes:

        $class_name = $this->class_name;
        if (is_subclass_of($class_name, 'Resource')) {
            return $class_name::getURLForAction(
                $action,
                $this->id,
                $url_parameters
            );
        } else {
            return self::getURLForAction(
                $action,
                $this->id,
                $url_parameters
            );
        }
    }

    public function getItemName($long_format = true)
    {
        if ($long_format) {
            //In some cases the general Resource class may be used
            //when the resource objects are in fact instances
            //of derived classes. To make sure that the correct prefix
            //is always displayed, we retrieve the derived class first
            //before returning the name:
            $derived_class = $this->getDerivedClassInstance();
            return $derived_class->getFullName();
        } else {
            return $this->name;
        }
    }

    public function getItemURL()
    {
        return $this->getActionURL('show');
    }

    public function getItemAvatarURL()
    {
        return Icon::create('resources', Icon::ROLE_INFO)->asImagePath();
    }


    public function getLink() : StudipLink
    {
        return new StudipLink($this->getActionURL(), $this->name, Icon::create('resources'));
    }
}