<?php

namespace Icinga\Module\Director\Web\Form;

use Exception;
use gipfl\IcingaWeb2\Url;
use Icinga\Authentication\Auth;
use Icinga\Module\Director\Auth\Permission;
use Icinga\Module\Director\Data\Db\DbObjectStore;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
use Icinga\Module\Director\Db\Branch\Branch;
use Icinga\Module\Director\Exception\NestingError;
use Icinga\Module\Director\Hook\IcingaObjectFormHook;
use Icinga\Module\Director\IcingaConfig\StateFilterSet;
use Icinga\Module\Director\IcingaConfig\TypeFilterSet;
use Icinga\Module\Director\Objects\IcingaTemplateChoice;
use Icinga\Module\Director\Objects\IcingaCommand;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Module\Director\Resolver\TemplateTree;
use Icinga\Module\Director\Util;
use Icinga\Module\Director\Web\Form\Element\ExtensibleSet;
use Icinga\Module\Director\Web\Form\Validate\NamePattern;
use Zend_Form_Element as ZfElement;
use Zend_Form_Element_Select as ZfSelect;
use Zend_Validate_Callback;

abstract class DirectorObjectForm extends DirectorForm
{
    public const GROUP_ORDER_OBJECT_DEFINITION = 20;
    public const GROUP_ORDER_RELATED_OBJECTS = 25;
    public const GROUP_ORDER_ASSIGN = 30;
    public const GROUP_ORDER_CHECK_EXECUTION = 40;
    public const GROUP_ORDER_CUSTOM_FIELDS = 50;
    public const GROUP_ORDER_CUSTOM_FIELD_CATEGORIES = 60;
    public const GROUP_ORDER_EVENT_FILTERS = 700;
    public const GROUP_ORDER_EXTRA_INFO = 750;
    public const GROUP_ORDER_CLUSTERING = 800;
    public const GROUP_ORDER_BUTTONS = 1000;

    /** @var IcingaObject */
    protected $object;

    /** @var Branch */
    protected $branch;

    protected $objectName;

    protected $className;

    protected $deleteButtonName;

    protected $displayGroups = [];

    protected $resolvedImports;

    protected $listUrl;

    /** @var Auth */
    private $auth;

    private $choiceElements = [];

    protected $preferredObjectType;

    /** @var IcingaObjectFieldLoader */
    protected $fieldLoader;

    private $allowsExperimental;

    private $presetImports;

    private $earlyProperties = array(
        // 'imports',
        'check_command',
        'check_command_id',
        'has_agent',
        'command',
        'command_id',
        'event_command',
        'event_command_id',
    );

    public function setPreferredObjectType($type)
    {
        $this->preferredObjectType = $type;
        return $this;
    }

    public function setAuth(Auth $auth)
    {
        $this->auth = $auth;
        return $this;
    }

    public function getAuth()
    {
        if ($this->auth === null) {
            $this->auth = Auth::getInstance();
        }
        return $this->auth;
    }

    protected function eventuallyAddNameRestriction($restrictionName)
    {
        $restrictions = $this->getAuth()->getRestrictions($restrictionName);
        if (! empty($restrictions)) {
            $this->getElement('object_name')->addValidator(
                new NamePattern($restrictions)
            );
        }

        return $this;
    }

    public function presetImports($imports)
    {
        if (! empty($imports)) {
            if (is_array($imports)) {
                $this->presetImports = $imports;
            } else {
                $this->presetImports = array($imports);
            }
        }

        return $this;
    }

    /**
     * @return DbObject|DbObjectWithSettings|IcingaObject
     */
    protected function object()
    {
        if ($this->object === null) {
            $values = $this->getValues();
            /** @var DbObject|IcingaObject $class */
            $class = $this->getObjectClassname();
            if ($this->preferredObjectType) {
                $values['object_type'] = $this->preferredObjectType;
            }
            if ($this->presetImports) {
                $values['imports'] = $this->presetImports;
            }

            $this->object = $class::create($values, $this->db);
        } else {
            if (! $this->object->hasConnection()) {
                $this->object->setConnection($this->db);
            }
        }

        return $this->object;
    }

    protected function extractChoicesFromPost($post)
    {
        $imports = [];

        foreach ($this->choiceElements as $other) {
            $name = $other->getName();
            if (array_key_exists($name, $post)) {
                $value = $post[$name];
                if (is_string($value)) {
                    $imports[] = $value;
                } elseif (is_array($value)) {
                    foreach ($value as $chosen) {
                        $imports[] = $chosen;
                    }
                }
            }
        }

        return $imports;
    }

    protected function assertResolvedImports()
    {
        if ($this->resolvedImports !== null) {
            return $this->resolvedImports;
        }

        $object = $this->object;

        if (! $object instanceof IcingaObject) {
            return $this->setResolvedImports(false);
        }
        if (! $object->supportsImports()) {
            return $this->setResolvedImports(false);
        }

        if ($this->hasBeenSent()) {
            // prefill special properties, required to resolve fields and similar
            $post = $this->getRequest()->getPost();

            $key = 'imports';
            if ($el = $this->getElement($key)) {
                if (array_key_exists($key, $post)) {
                    $imports = $post[$key];
                    if (! is_array($imports)) {
                        $imports = array($imports);
                    }
                    $imports = array_filter(array_values(array_merge(
                        $imports,
                        $this->extractChoicesFromPost($post)
                    )), 'strlen');

                    /** @var ZfElement $el */
                    $this->populate([$key => $imports]);
                    $el->setValue($imports);
                    if (! $this->tryToSetObjectPropertyFromElement($object, $el, $key)) {
                        return $this->resolvedImports = false;
                    }
                }
            } elseif ($this->presetImports) {
                $imports = array_values(array_merge(
                    $this->presetImports,
                    $this->extractChoicesFromPost($post)
                ));
                if (! $this->eventuallySetImports($imports)) {
                    return $this->resolvedImports = false;
                }
            } else {
                if (! empty($this->choiceElements)) {
                    if (! $this->eventuallySetImports($this->extractChoicesFromPost($post))) {
                        return $this->resolvedImports = false;
                    }
                }
            }

            foreach ($this->earlyProperties as $key) {
                if ($el = $this->getElement($key)) {
                    if (array_key_exists($key, $post)) {
                        $this->populate([$key => $post[$key]]);
                        $this->tryToSetObjectPropertyFromElement($object, $el, $key);
                    }
                }
            }
        }

        try {
            $object->listAncestorIds();
        } catch (NestingError $e) {
            $this->addUniqueErrorMessage($e->getMessage());
            return $this->resolvedImports = false;
        } catch (Exception $e) {
            $this->addException($e, 'imports');
            return $this->resolvedImports = false;
        }

        return $this->setResolvedImports();
    }

    protected function eventuallySetImports($imports)
    {
        try {
            $this->object()->set('imports', $imports);
            return true;
        } catch (Exception $e) {
            $this->addException($e, 'imports');
            return false;
        }
    }

    protected function tryToSetObjectPropertyFromElement(
        IcingaObject $object,
        ZfElement $element,
        $key
    ) {
        $old = null;
        try {
            $old = $object->get($key);
            $object->set($key, $element->getValue());
            $object->resolveUnresolvedRelatedProperties();

            if ($key === 'imports') {
                $object->imports()->getObjects();
            }
            return true;
        } catch (Exception $e) {
            if ($old !== null) {
                $object->set($key, $old);
            }
            $this->addException($e, $key);
            return false;
        }
    }

    public function setResolvedImports($resolved = true)
    {
        return $this->resolvedImports = $resolved;
    }

    public function isObject()
    {
        return $this->getSentOrObjectValue('object_type') === 'object';
    }

    public function isTemplate()
    {
        return $this->getSentOrObjectValue('object_type') === 'template';
    }

    // TODO: move to a subform
    protected function handleRanges(IcingaObject $object, &$values)
    {
        if (! $object->supportsRanges()) {
            return;
        }

        $key = 'ranges';
        $object = $this->object();

        /* Sample:

        array(
            'monday'  => 'eins',
            'tuesday' => '00:00-24:00',
            'sunday'  => 'zwei',
        );

        */
        if (array_key_exists($key, $values)) {
            $object->ranges()->set($values[$key]);
            unset($values[$key]);
        }

        foreach ($object->ranges()->getRanges() as $key => $value) {
            $this->addRange($key, $value);
        }
    }

    protected function addToCheckExecutionDisplayGroup($elements)
    {
        return $this->addElementsToGroup(
            $elements,
            'check_execution',
            self::GROUP_ORDER_CHECK_EXECUTION,
            $this->translate('Check execution')
        );
    }

    public function addElementsToGroup($elements, $group, $order, $legend = null)
    {
        if (! is_array($elements)) {
            $elements = array($elements);
        }

        // These are optional elements, they might exist or not. We still want
        // to see exception for other ones
        $skipLegally = array('check_period_id');

        $skip = array();
        foreach ($elements as $k => $v) {
            if (is_string($v)) {
                $el = $this->getElement($v);
                if (!$el && in_array($v, $skipLegally)) {
                    $skip[] = $k;
                    continue;
                }

                $elements[$k] = $el;
            }
        }

        foreach ($skip as $k) {
            unset($elements[$k]);
        }

        if (! array_key_exists($group, $this->displayGroups)) {
            $this->addDisplayGroup($elements, $group, array(
                'decorators' => array(
                    'FormElements',
                    array('HtmlTag', array('tag' => 'dl')),
                    'Fieldset',
                ),
                'order'  => $order,
                'legend' => $legend ?: $group,
            ));
            $this->displayGroups[$group] = $this->getDisplayGroup($group);
        } else {
            $this->displayGroups[$group]->addElements($elements);
        }

        return $this->displayGroups[$group];
    }

    protected function handleProperties(DbObject $object, &$values)
    {
        if ($this->hasBeenSent()) {
            foreach ($values as $key => $value) {
                try {
                    if ($key === 'imports' && ! empty($this->choiceElements)) {
                        if (! is_array($value)) {
                            $value = [$value];
                        }
                        foreach ($this->choiceElements as $element) {
                            $chosen = $element->getValue();
                            if (is_string($chosen)) {
                                $value[] = $chosen;
                            } elseif (is_array($chosen)) {
                                foreach ($chosen as $choice) {
                                    $value[] = $choice;
                                }
                            }
                        }
                    }
                    $object->set($key, $value);
                    if ($object instanceof IcingaObject) {
                        if ($this->resolvedImports !== false) {
                            $object->imports()->getObjects();
                        }
                    }
                } catch (Exception $e) {
                    $this->addException($e, $key);
                }
            }
        }
    }

    protected function loadInheritedProperties()
    {
        if ($this->assertResolvedImports()) {
            try {
                $this->showInheritedProperties($this->object());
            } catch (Exception $e) {
                $this->addException($e);
            }
        }
    }

    protected function showInheritedProperties(IcingaObject $object)
    {
        $inherited = $object->getInheritedProperties();
        $origins   = $object->getOriginsProperties();

        foreach ($inherited as $k => $v) {
            if ($v !== null && $k !== 'object_name') {
                $el = $this->getElement($k);
                if ($el) {
                    $this->setInheritedValue($el, $inherited->$k, $origins->$k);
                } elseif (substr($k, -3) === '_id') {
                    $k = substr($k, 0, -3);
                    $el = $this->getElement($k);
                    if ($el) {
                        $this->setInheritedValue(
                            $el,
                            $object->getRelatedObjectName($k, $v),
                            $origins->{"{$k}_id"}
                        );
                    }
                }
            }
        }
    }

    protected function prepareFields($object)
    {
        if ($this->assertResolvedImports()) {
            $this->fieldLoader = new IcingaObjectFieldLoader($object);
            $this->fieldLoader->prepareElements($this);
        }

        return $this;
    }

    protected function setCustomVarValues($values)
    {
        if ($this->fieldLoader) {
            $this->fieldLoader->setValues($values, 'var_');
        }

        return $this;
    }

    protected function addFields()
    {
        if ($this->fieldLoader) {
            $this->fieldLoader->addFieldsToForm($this);
            $this->onAddedFields();
        }
    }

    protected function onAddedFields()
    {
    }

    // TODO: remove, used in sets I guess
    protected function fieldLoader($object)
    {
        if ($this->fieldLoader === null) {
            $this->fieldLoader = new IcingaObjectFieldLoader($object);
        }

        return $this->fieldLoader;
    }

    protected function isNew()
    {
        return $this->object === null || ! $this->object->hasBeenLoadedFromDb();
    }

    protected function setButtons()
    {
        if ($this->isNew()) {
            $this->setSubmitLabel(
                $this->translate('Add')
            );
        } else {
            $this->setSubmitLabel(
                $this->translate('Store')
            );
            $this->addDeleteButton();
        }
    }

    /**
     * @param bool $importsFirst
     * @return $this
     */
    protected function groupMainProperties($importsFirst = false)
    {
        if ($importsFirst) {
            $elements = [
                'imports',
                'object_type',
                'object_name',
            ];
        } else {
            $elements = [
                'object_type',
                'object_name',
                'imports',
            ];
        }
        $elements = array_merge($elements, [
            'display_name',
            'host',
            'host_id',
            'address',
            'address6',
            'groups',
            'inherited_groups',
            'applied_groups',
            'users',
            'users_var',
            'user_groups',
            'user_groups_var',
            'apply_to',
            'command_id', // Notification
            'notification_interval',
            'period_id',
            'times_begin',
            'times_end',
            'email',
            'pager',
            'enable_notifications',
            'redundancy_group', //Dependencies
            'disable_checks', //Dependencies
            'disable_notifications',
            'ignore_soft_states',
            'apply_for',
            'create_live',
            'disabled',
        ]);

        // Add template choices to the main section
        /** @var \Zend_Form_Element $el */
        foreach ($this->getElements() as $key => $el) {
            if (substr($el->getName(), 0, 6) === 'choice') {
                $elements[] = $key;
            }
        }

        $this->addDisplayGroup($elements, 'object_definition', array(
            'decorators' => array(
                'FormElements',
                array('HtmlTag', array('tag' => 'dl')),
                'Fieldset',
            ),
            'order' => self::GROUP_ORDER_OBJECT_DEFINITION,
            'legend' => $this->translate('Main properties')
        ));

        return $this;
    }

    protected function setSentValue($name, $value)
    {
        if ($this->hasBeenSent()) {
            $request = $this->getRequest();
            if ($value !== null && $request->isPost() && $request->getPost($name) !== null) {
                $request->setPost($name, $value);
            }
        }

        $this->setElementValue($name, $value);
    }

    public function setElementValue($name, $value = null)
    {
        $el = $this->getElement($name);
        if (! $el) {
            // Not showing an error, as most object properties do not exist. Not
            // yet, because IMO this should be checked.
            // $this->addError(sprintf($this->translate('Form element "%s" does not exist'), $name));
            return;
        }

        if ($value !== null) {
            $el->setValue($value);
        }
    }

    public function setInheritedValue(ZfElement $el, $inherited, $inheritedFrom)
    {
        if ($inherited === null) {
            return;
        }

        $txtInherited = sprintf($this->translate(' (inherited from "%s")'), $inheritedFrom);
        if ($el instanceof ZfSelect) {
            $multi = $el->getMultiOptions();
            if (is_bool($inherited)) {
                $inherited = $inherited ? 'y' : 'n';
            }
            if (is_scalar($inherited) && array_key_exists($inherited, $multi)) {
                $multi[null] = $multi[$inherited] . $txtInherited;
            } else {
                $multi[null] = $this->stringifyInheritedValue($inherited) . $txtInherited;
            }
            $el->setMultiOptions($multi);
        } elseif ($el instanceof ExtensibleSet) {
            $el->setAttrib('inherited', $inherited);
            $el->setAttrib('inheritedFrom', $inheritedFrom);
        } else {
            if (is_string($inherited) || is_int($inherited)) {
                $el->setAttrib('placeholder', $inherited . $txtInherited);
            }
        }

        // We inherited a value, so no need to require the field
        $el->setRequired(false);
    }

    protected function stringifyInheritedValue($value)
    {
        return is_scalar($value) ? $value : substr(json_encode($value), 0, 40);
    }

    public function setListUrl($url)
    {
        $this->listUrl = $url;
        return $this;
    }

    public function onSuccess()
    {
        $object = $this->object();
        if ($object->hasBeenModified()) {
            if (! $object->hasBeenLoadedFromDb()) {
                $this->setHttpResponseCode(201);
            }

            $msg = sprintf(
                $object->hasBeenLoadedFromDb()
                ? $this->translate('The %s has successfully been stored')
                : $this->translate('A new %s has successfully been created'),
                $this->translate($this->getObjectShortClassName())
            );
            $this->getDbObjectStore()->store($object);
        } else {
            if ($this->isApiRequest()) {
                $this->setHttpResponseCode(304);
            }
            $msg = $this->translate('No action taken, object has not been modified');
        }

        $this->setObjectSuccessUrl();
        $this->beforeSuccessfulRedirect();
        $this->redirectOnSuccess($msg);
    }

    protected function setObjectSuccessUrl()
    {
        $object = $this->object();

        if ($object instanceof IcingaObject) {
            $params = $object->getUrlParams();
            $url = Url::fromPath($this->getAction());
            if ($url->hasParam('dbResourceName')) {
                $params['dbResourceName'] = $url->getParam('dbResourceName');
            }
            $this->setSuccessUrl(
                'director/' . strtolower($this->getObjectShortClassName()),
                $params
            );
        } elseif ($object->hasProperty('id')) {
            $this->setSuccessUrl($this->getSuccessUrl()->with('id', $object->getProperty('id')));
        }
    }

    protected function beforeSuccessfulRedirect()
    {
    }

    public function hasElement($name)
    {
        return $this->getElement($name) !== null;
    }

    public function getObject()
    {
        return $this->object;
    }

    public function hasObject()
    {
        return $this->object !== null;
    }

    public function isIcingaObject()
    {
        if ($this->object !== null) {
            return $this->object instanceof IcingaObject;
        }

        /** @var DbObject $class */
        $class = $this->getObjectClassname();
        $instance = $class::create();

        return $instance instanceof IcingaObject;
    }

    public function isMultiObjectForm()
    {
        return false;
    }

    public function setObject(DbObject $object)
    {
        $this->object = $object;
        if ($this->db === null) {
            /** @var Db $connection */
            $connection = $object->getConnection();
            $this->setDb($connection);
        }

        return $this;
    }

    protected function getObjectClassname()
    {
        if ($this->className === null) {
            return 'Icinga\\Module\\Director\\Objects\\'
               . substr(join('', array_slice(explode('\\', get_class($this)), -1)), 0, -4);
        }

        return $this->className;
    }

    protected function getObjectShortClassName()
    {
        if ($this->objectName === null) {
            $className = substr(strrchr(get_class($this), '\\'), 1);
            if (substr($className, 0, 6) === 'Icinga') {
                return substr($className, 6, -4);
            } else {
                return substr($className, 0, -4);
            }
        }

        return $this->objectName;
    }

    protected function removeFromSet(&$set, $key)
    {
        unset($set[$key]);
    }

    protected function moveUpInSet(&$set, $key)
    {
        list($set[$key - 1], $set[$key]) = array($set[$key], $set[$key - 1]);
    }

    protected function moveDownInSet(&$set, $key)
    {
        list($set[$key + 1], $set[$key]) = array($set[$key], $set[$key + 1]);
    }

    protected function beforeSetup()
    {
        if (!$this->hasBeenSent()) {
            return;
        }

        /** @var array $post */
        $post = $this->getRequest()->getPost();
        $values = $post;

        foreach ($post as $key => $value) {
            if (preg_match('/^(.+?)_(\d+)__(MOVE_DOWN|MOVE_UP|REMOVE)$/', $key, $m)) {
                $values[$m[1]] = array_filter($values[$m[1]], 'strlen');
                switch ($m[3]) {
                    case 'MOVE_UP':
                        $this->moveUpInSet($values[$m[1]], $m[2]);
                        break;
                    case 'MOVE_DOWN':
                        $this->moveDownInSet($values[$m[1]], $m[2]);
                        break;
                    case 'REMOVE':
                        $this->removeFromSet($values[$m[1]], $m[2]);
                        break;
                }

                $this->getRequest()->setPost($m[1], $values[$m[1]]);
            }
        }
    }

    protected function onRequest()
    {
        if ($this->object !== null) {
            $this->setDefaultsFromObject($this->object);
        }
        $this->prepareFields($this->object());
        IcingaObjectFormHook::callOnSetup($this);
        if ($this->hasBeenSent()) {
            $this->handlePost();
        }
        try {
            $this->loadInheritedProperties();
            $this->addFields();
            $this->callOnRequestCallables();
        } catch (Exception $e) {
            $this->addUniqueException($e);

            return;
        }

        if ($this->shouldBeDeleted()) {
            $this->deleteObject($this->object());
        }
    }

    protected function handlePost()
    {
        $object = $this->object();

        $post = $this->getRequest()->getPost();
        $this->populate($post);
        $values = $this->getValues();

        if ($object instanceof IcingaObject) {
            $this->setCustomVarValues($post);
        }

        $this->handleProperties($object, $values);

        // TODO: get rid of this
        if ($object instanceof IcingaObject) {
            $this->handleRanges($object, $values);
        }
    }

    protected function setDefaultsFromObject(DbObject $object)
    {
        /** @var ZfElement $element */
        foreach ($this->getElements() as $element) {
            $key = $element->getName();
            if ($object->hasProperty($key)) {
                $value = $object->get($key);
                if ($object instanceof IcingaObject) {
                    if ($object->propertyIsRelatedSet($key)) {
                        if (! count((array) $value)) {
                            continue;
                        }
                    }
                }

                if ($value !== null && $value !== []) {
                    $element->setValue($value);
                }
            }
        }
    }

    protected function deleteObject($object)
    {
        if ($object instanceof IcingaObject && $object->hasProperty('object_name')) {
            $msg = sprintf(
                '%s "%s" has been removed',
                $this->translate($this->getObjectShortClassName()),
                $object->getObjectName()
            );
        } else {
            $msg = sprintf(
                '%s has been removed',
                $this->translate($this->getObjectShortClassName())
            );
        }

        if ($this->listUrl) {
            $url = $this->listUrl;
        } elseif ($object instanceof IcingaObject && $object->hasProperty('object_name')) {
            $url = $object->getOnDeleteUrl();
        } else {
            $url = $this->getSuccessUrl()->without(
                array('field_id', 'argument_id', 'range', 'range_type')
            );
        }

        if ($this->getDbObjectStore()->delete($object)) {
            $this->setSuccessUrl($url);
        }
        $this->redirectOnSuccess($msg);
    }

    /**
     * @return DbObjectStore
     */
    protected function getDbObjectStore()
    {
        $store = new DbObjectStore($this->getDb(), $this->branch);
        return $store;
    }

    protected function addDeleteButton($label = null)
    {
        $object = $this->object;

        if ($label === null) {
            $label = $this->translate('Delete');
        }

        $el = $this->createElement('submit', $label)
            ->setLabel($label)
            ->setDecorators(array('ViewHelper'));
            //->removeDecorator('Label');

        $this->deleteButtonName = $el->getName();

        if ($object instanceof IcingaObject && $object->isTemplate()) {
            if ($cnt = $object->countDirectDescendants()) {
                $el->setAttrib('disabled', 'disabled');
                $el->setAttrib(
                    'title',
                    sprintf(
                        $this->translate('This template is still in use by %d other objects'),
                        $cnt
                    )
                );
            }
        } elseif ($object instanceof IcingaCommand && $object->isInUse()) {
            $el->setAttrib('disabled', 'disabled');
            $el->setAttrib(
                'title',
                sprintf(
                    $this->translate('This Command is still in use by %d other objects'),
                    $object->countDirectUses()
                )
            );
        }

        $this->addElement($el);

        return $this;
    }

    public function hasDeleteButton()
    {
        return $this->deleteButtonName !== null;
    }

    public function shouldBeDeleted()
    {
        if (! $this->hasDeleteButton()) {
            return false;
        }

        $name = $this->deleteButtonName;
        return $this->getSentValue($name) === $this->getElement($name)->getLabel();
    }

    public function abortDeletion()
    {
        if ($this->hasDeleteButton()) {
            $this->setSentValue($this->deleteButtonName, 'ABORTED');
        }
    }

    public function getSentOrResolvedObjectValue($name, $default = null)
    {
        return $this->getSentOrObjectValue($name, $default, true);
    }

    public function getSentOrObjectValue($name, $default = null, $resolved = false)
    {
        // TODO: check whether getSentValue is still needed since element->getValue
        //       is in place (currently for form element default values only)

        if (!$this->hasObject()) {
            if ($this->hasBeenSent()) {
                return $this->getSentValue($name, $default);
            } else {
                if ($name === 'object_type' && $this->preferredObjectType) {
                    return $this->preferredObjectType;
                }
                if ($name === 'imports' && $this->presetImports) {
                    return $this->presetImports;
                }
                if ($this->valueIsEmpty($val = $this->getValue($name))) {
                    return $default;
                } else {
                    return $val;
                }
            }
        }

        if ($this->hasBeenSent()) {
            if (!$this->valueIsEmpty($value = $this->getSentValue($name))) {
                return $value;
            }
        }

        $object = $this->getObject();

        if ($object->hasProperty($name)) {
            if ($resolved && $object->supportsImports()) {
                if ($this->assertResolvedImports()) {
                    $objectProperty = $object->getResolvedProperty($name);
                } else {
                    $objectProperty = $object->$name;
                }
            } else {
                $objectProperty = $object->$name;
            }
        } else {
            $objectProperty = null;
        }

        if ($objectProperty !== null) {
            return $objectProperty;
        }

        if (($el = $this->getElement($name)) && !$this->valueIsEmpty($val = $el->getValue())) {
            return $val;
        }

        return $default;
    }

    public function loadObject($id)
    {
        if ($this->branch && $this->branch->isBranch()) {
            throw new \RuntimeException('Calling loadObject from form in a branch');
        }
        /** @var DbObject $class */
        $class = $this->getObjectClassname();
        if (is_int($id)) {
            $this->object = $class::loadWithAutoIncId($id, $this->db);
            if ($this->object->getKeyName() === 'id') {
                $this->addHidden('id', $id);
            }
        } else {
            $this->object = $class::load($id, $this->db);
        }


        return $this;
    }

    protected function addRange($key, $range)
    {
        $this->addElement('text', 'range_' . $key, array(
            'label' => 'ranges.' . $key,
            'value' => $range->range_value
        ));
    }

    /**
     * @param Db $db
     * @return $this
     */
    public function setDb(Db $db)
    {
        if ($this->object !== null) {
            $this->object->setConnection($db);
        }

        parent::setDb($db);
        return $this;
    }

    public function optionallyAddFromEnum($enum)
    {
        return array(
            null => $this->translate('- click to add more -')
        ) + $enum;
    }

    protected function addObjectTypeElement()
    {
        if (!$this->isNew()) {
            return $this;
        }

        if ($this->preferredObjectType) {
            $this->addHidden('object_type', $this->preferredObjectType);
            return $this;
        }

        $object = $this->object();

        if ($object->supportsImports()) {
            $templates = $this->enumAllowedTemplates();

            if (empty($templates) && $this->getObjectShortClassName() !== 'Command') {
                $types = array('template' => $this->translate('Template'));
            } else {
                $types = array(
                    'object'   => $this->translate('Object'),
                    'template' => $this->translate('Template'),
                );
            }
        } else {
             $types = array('object' => $this->translate('Object'));
        }

        if ($this->object()->supportsApplyRules()) {
            $types['apply'] = $this->translate('Apply rule');
        }

        $this->addElement('select', 'object_type', array(
            'label' => $this->translate('Object type'),
            'description'  => $this->translate(
                'What kind of object this should be. Templates allow full access'
                . ' to any property, they are your building blocks for "real" objects.'
                . ' External objects should usually not be manually created or modified.'
                . ' They allow you to work with objects locally defined on your Icinga nodes,'
                . ' while not rendering and deploying them with the Director. Apply rules allow'
                . ' to assign services, notifications and groups to other objects.'
            ),
            'required'     => true,
            'multiOptions' => $this->optionalEnum($types),
            'class'        => 'autosubmit'
        ));

        return $this;
    }

    protected function hasObjectType()
    {
        if (!$this->object()->hasProperty('object_type')) {
            return false;
        }

        return ! $this->valueIsEmpty($this->getSentOrObjectValue('object_type'));
    }

    protected function addZoneElement($all = false)
    {
        if ($all || $this->isTemplate()) {
            $zones = $this->db->enumZones();
        } else {
            $zones = $this->db->enumNonglobalZones();
        }

        $this->addElement('select', 'zone_id', array(
            'label' => $this->translate('Cluster Zone'),
            'description'  => $this->translate(
                'Icinga cluster zone. Allows to manually override Directors decisions'
                . ' of where to deploy your config to. You should consider not doing so'
                . ' unless you gained deep understanding of how an Icinga Cluster stack'
                . ' works'
            ),
            'multiOptions' => $this->optionalEnum($zones)
        ));

        return $this;
    }

    /**
     * @param $type
     * @return $this
     */
    protected function addChoices($type)
    {
        if ($this->isTemplate()) {
            return $this;
        }

        $connection = $this->getDb();
        $choiceType = 'TemplateChoice' . ucfirst($type);
        $table = "icinga_$type";
        $choices = IcingaObject::loadAllByType($choiceType, $connection);
        $chosenTemplates = $this->getSentOrObjectValue('imports');
        $db = $connection->getDbAdapter();
        if (empty($chosenTemplates)) {
            $importedIds = [];
        } else {
            $importedIds = $db->fetchCol(
                $db->select()->from($table, 'id')
                    ->where('object_name in (?)', (array)$chosenTemplates)
                    ->where('object_type = ?', 'template')
            );
        }

        foreach ($choices as $choice) {
            $required = $choice->get('required_template_id');
            if ($required === null || in_array($required, $importedIds, false)) {
                $this->addChoiceElement($choice);
            }
        }

        return $this;
    }

    protected function addChoiceElement(IcingaTemplateChoice $choice)
    {
        $imports = $this->object()->listImportNames();
        $element = $choice->createFormElement($this, $imports);
        $this->addElement($element);
        $this->choiceElements[$element->getName()] = $element;
        return $this;
    }

    /**
     * @param bool $required
     * @return $this
     * @throws \Zend_Form_Exception
     */
    protected function addImportsElement($required = null)
    {
        if ($this->presetImports) {
            return $this;
        }

        if (in_array($this->getObjectShortClassName(), ['TimePeriod', 'ScheduledDowntime'])) {
            $required = false;
        } else {
            $required = $required !== null ? $required : !$this->isTemplate();
        }
        $enum = $this->enumAllowedTemplates();
        if (empty($enum)) {
            if ($required) {
                if ($this->hasBeenSent()) {
                    $this->addError($this->translate('No template has been chosen'));
                } else {
                    if ($this->hasPermission(Permission::ADMIN)) {
                        $html = $this->translate('Please define a related template first');
                    } else {
                        $html = $this->translate('No related template has been provided yet');
                    }
                    $this->addHtml('<p class="warning">' . $html . '</p>');
                }
            }
            return $this;
        }

        $db = $this->getDb()->getDbAdapter();
        $object = $this->object;
        if ($object->supportsChoices()) {
            $choiceNames = $db->fetchCol(
                $db->select()->from(
                    $this->object()->getTableName(),
                    'object_name'
                )->where('template_choice_id IS NOT NULL')
            );
        } else {
            $choiceNames = [];
        }

        $type = $object->getShortTableName();
        $this->addElement('extensibleSet', 'imports', array(
            'label'        => $this->translate('Imports'),
            'description'  => $this->translate(
                'Importable templates, add as many as you want. Please note that order'
                . ' matters when importing properties from multiple templates: last one'
                . ' wins'
            ),
            'required'     => $required,
            'spellcheck'   => 'false',
            'hideOptions'  => $choiceNames,
            'suggest'      => "{$type}templates",
            // 'multiOptions' => $this->optionallyAddFromEnum($enum),
            'sorted'       => true,
            'value'        => $this->presetImports,
            'class'        => 'autosubmit',
            'validators'   => [
                new Zend_Validate_Callback(function ($value) {
                    $objectInstance = $this->object->getShortTableName();
                    // Avoid cyclic imports for hosts and commands
                    if ($objectInstance !== 'host' && $objectInstance !== 'command') {
                        return true;
                    }

                    $templateTree = new TemplateTree($objectInstance, $this->getDb());
                    $importsElement = $this->getElement('imports');
                    $objectName = $this->object->getObjectName();
                    if (in_array($objectName, $value, true)) {
                        $importsElement->addErrorMessage(
                            $this->translate('You can not import the same object into itself, please remove it first')
                        );

                        return false;
                    }

                    if ($this->object->isTemplate()) {
                        $descendents = [];
                        if (! $this->isNew()) {
                            $descendents = $templateTree->getDescendantsFor($this->object);
                        }

                        $loopedImports = array_intersect($descendents, $value);
                        if (! empty($loopedImports)) {
                            $loopedImport = array_slice($loopedImports, 0, 1);
                            array_push($loopedImport, ...$descendents);
                            $importsElement->addErrorMessage(
                                sprintf(
                                    $this->translate('Loop detected for imports: %s'),
                                    implode('->', $loopedImports)
                                )
                            );

                            return false;
                        }
                    }

                    return true;
                })
            ]
        ));

        return $this;
    }

    protected function addDisabledElement()
    {
        if ($this->isTemplate()) {
            return $this;
        }

        $this->addBoolean(
            'disabled',
            array(
                'label'       => $this->translate('Disabled'),
                'description' => $this->translate('Disabled objects will not be deployed')
            ),
            'n'
        );

        return $this;
    }

    /**
     * @return $this
     * @throws \Zend_Form_Exception
     */
    protected function addGroupDisplayNameElement()
    {
        $this->addElement('text', 'display_name', array(
            'label' => $this->translate('Display Name'),
            'description' => $this->translate(
                'An alternative display name for this group. If you wonder how this'
                . ' could be helpful just leave it blank'
            )
        ));

        return $this;
    }

    /**
     * @param bool $force
     *
     * @return $this
     * @throws \Zend_Form_Exception
     */
    protected function addCheckCommandElements($force = false)
    {
        if (! $force && ! $this->isTemplate()) {
            return $this;
        }

        $this->addElement('text', 'check_command', array(
            'label' => $this->translate('Check command'),
            'description'  => $this->translate('Check command definition'),
            // 'multiOptions' => $this->optionalEnum($this->db->enumCheckcommands()),
            'class'        => 'autosubmit director-suggest', // This influences fields
            'data-suggestion-context' => 'checkcommandnames',
            'value' => $this->getObject()->get('check_command')
        ));
        $this->getDisplayGroup('object_definition')
            // ->addElement($this->getElement('check_command_id'))
            ->addElement($this->getElement('check_command'));

        $eventCommands = $this->db->enumEventcommands();

        if (! empty($eventCommands)) {
            $this->addElement('select', 'event_command_id', array(
                'label' => $this->translate('Event command'),
                'description'  => $this->translate('Event command definition'),
                'multiOptions' => $this->optionalEnum($eventCommands),
                'class'        => 'autosubmit',
            ));
            $this->addToCheckExecutionDisplayGroup('event_command_id');
        }

        return $this;
    }

    protected function addCheckExecutionElements($force = false)
    {
        if (! $force && ! $this->isTemplate()) {
            return $this;
        }

        $this->addElement(
            'text',
            'check_interval',
            array(
                'label' => $this->translate('Check interval'),
                'description' => $this->translate('Your regular check interval')
            )
        );

        $this->addElement(
            'text',
            'retry_interval',
            array(
                'label' => $this->translate('Retry interval'),
                'description' => $this->translate(
                    'Retry interval, will be applied after a state change unless the next hard state is reached'
                )
            )
        );

        $this->addElement(
            'text',
            'max_check_attempts',
            array(
                'label' => $this->translate('Max check attempts'),
                'description' => $this->translate(
                    'Defines after how many check attempts a new hard state is reached'
                )
            )
        );

        $this->addElement(
            'text',
            'check_timeout',
            array(
                'label' => $this->translate('Check timeout'),
                'description' => $this->translate(
                    "Check command timeout in seconds. Overrides the CheckCommand's timeout attribute"
                )
            )
        );

        $periods = $this->db->enumTimeperiods();

        if (!empty($periods)) {
            $this->addElement(
                'select',
                'check_period_id',
                array(
                    'label' => $this->translate('Check period'),
                    'description' => $this->translate(
                        'The name of a time period which determines when this'
                        . ' object should be monitored. Not limited by default.'
                    ),
                    'multiOptions' => $this->optionalEnum($periods),
                )
            );
        }

        $this->optionalBoolean(
            'enable_active_checks',
            $this->translate('Execute active checks'),
            $this->translate('Whether to actively check this object')
        );

        $this->optionalBoolean(
            'enable_passive_checks',
            $this->translate('Accept passive checks'),
            $this->translate('Whether to accept passive check results for this object')
        );

        $this->optionalBoolean(
            'enable_notifications',
            $this->translate('Send notifications'),
            $this->translate('Whether to send notifications for this object')
        );

        $this->optionalBoolean(
            'enable_event_handler',
            $this->translate('Enable event handler'),
            $this->translate('Whether to enable event handlers this object')
        );

        $this->optionalBoolean(
            'enable_perfdata',
            $this->translate('Process performance data'),
            $this->translate('Whether to process performance data provided by this object')
        );

        $this->optionalBoolean(
            'enable_flapping',
            $this->translate('Enable flap detection'),
            $this->translate('Whether flap detection is enabled on this object')
        );

        $this->addElement(
            'text',
            'flapping_threshold_high',
            array(
                'label' => $this->translate('Flapping threshold (high)'),
                'description' => $this->translate(
                    'Flapping upper bound in percent for a service to be considered flapping'
                )
            )
        );

        $this->addElement(
            'text',
            'flapping_threshold_low',
            array(
                'label' => $this->translate('Flapping threshold (low)'),
                'description' => $this->translate(
                    'Flapping lower bound in percent for a service to be considered not flapping'
                )
            )
        );

        $this->optionalBoolean(
            'volatile',
            $this->translate('Volatile'),
            $this->translate('Whether this check is volatile.')
        );

        $elements = array(
            'check_interval',
            'retry_interval',
            'max_check_attempts',
            'check_timeout',
            'check_period_id',
            'enable_active_checks',
            'enable_passive_checks',
            'enable_notifications',
            'enable_event_handler',
            'enable_perfdata',
            'enable_flapping',
            'flapping_threshold_high',
            'flapping_threshold_low',
            'volatile'
        );
        $this->addToCheckExecutionDisplayGroup($elements);

        return $this;
    }

    protected function enumAllowedTemplates()
    {
        $object = $this->object();
        $tpl = $this->db->enumIcingaTemplates($object->getShortTableName());
        if (empty($tpl)) {
            return [];
        }

        /** @var int|string $id */
        $id = $object->get('id');

        if (array_key_exists($id, $tpl)) {
            unset($tpl[$id]);
        }

        return array_combine($tpl, $tpl);
    }

    protected function addExtraInfoElements()
    {
        $this->addElement('textarea', 'notes', array(
            'label'   => $this->translate('Notes'),
            'description' => $this->translate(
                'Additional notes for this object'
            ),
            'rows'    => 2,
            'columns' => 60,
        ));

        $this->addElement('text', 'notes_url', array(
            'label'   => $this->translate('Notes URL'),
            'description' => $this->translate(
                'An URL pointing to additional notes for this object'
            ),
        ));

        $this->addElement('text', 'action_url', array(
            'label'   => $this->translate('Action URL'),
            'description' => $this->translate(
                'An URL leading to additional actions for this object. Often used'
                . ' with Icinga Classic, rarely with Icinga Web 2 as it provides'
                . ' far better possibilities to integrate addons'
            ),
        ));

        $this->addElement('text', 'icon_image', array(
            'label'   => $this->translate('Icon image'),
            'description' => $this->translate(
                'An URL pointing to an icon for this object. Try "tux.png" for icons'
                . ' relative to public/img/icons or "cloud" (no extension) for items'
                . ' from the Icinga icon font'
            ),
        ));

        $this->addElement('text', 'icon_image_alt', array(
            'label'   => $this->translate('Icon image alt'),
            'description' => $this->translate(
                'Alternative text to be shown in case above icon is missing'
            ),
        ));

        $elements = array(
            'notes',
            'notes_url',
            'action_url',
            'icon_image',
            'icon_image_alt',
        );

        $this->addDisplayGroup($elements, 'extrainfo', array(
            'decorators' => array(
                'FormElements',
                array('HtmlTag', array('tag' => 'dl')),
                'Fieldset',
            ),
            'order'  => self::GROUP_ORDER_EXTRA_INFO,
            'legend' => $this->translate('Additional properties')
        ));

        return $this;
    }

    /**
     * Add an assign_filter form element
     *
     * Forms should use this helper method for objects using the typical
     * assign_filter column
     *
     * @param array $properties Form element properties
     *
     * @return $this
     * @throws \Zend_Form_Exception
     */
    protected function addAssignFilter($properties)
    {
        if (!$this->object || !$this->object->supportsAssignments()) {
            return $this;
        }

        $this->addFilterElement('assign_filter', $properties);
        $el = $this->getElement('assign_filter');

        $this->addDisplayGroup(array($el), 'assign', array(
            'decorators' => array(
                'FormElements',
                array('HtmlTag', array('tag' => 'dl')),
                'Fieldset',
            ),
            'order'  => self::GROUP_ORDER_ASSIGN,
            'legend' => $this->translate('Assign where')
        ));

        return $this;
    }

    /**
     * Add a dataFilter element with fitting decorators
     *
     * TODO: Evaluate whether parts or all of this could be moved to the element
     * class.
     *
     * @param string $name Element name
     * @param array $properties Form element properties
     *
     * @return $this
     * @throws \Zend_Form_Exception
     */
    protected function addFilterElement($name, $properties)
    {
        $this->addElement('dataFilter', $name, $properties);
        $el = $this->getElement($name);

        $ddClass = 'full-width';
        if (array_key_exists('required', $properties) && $properties['required']) {
            $ddClass .= ' required';
        }

        $el->clearDecorators()
            ->addDecorator('ViewHelper')
            ->addDecorator('Errors')
            ->addDecorator('Description', array('tag' => 'p', 'class' => 'description'))
            ->addDecorator('HtmlTag', array(
                'tag'   => 'dd',
                'class' => $ddClass,
            ));

        return $this;
    }

    protected function addEventFilterElements($elements = array('states','types'))
    {
        if (in_array('states', $elements)) {
            $this->addElement('extensibleSet', 'states', array(
                'label' => $this->translate('States'),
                'multiOptions' => $this->optionallyAddFromEnum($this->enumStates()),
                'description'  => $this->translate(
                    'The host/service states you want to get notifications for'
                ),
            ));
        }

        if (in_array('types', $elements)) {
            $this->addElement('extensibleSet', 'types', array(
                'label' => $this->translate('Transition types'),
                'multiOptions' => $this->optionallyAddFromEnum($this->enumTypes()),
                'description'  => $this->translate(
                    'The state transition types you want to get notifications for'
                ),
            ));
        }

        $this->addDisplayGroup($elements, 'event_filters', array(
            'decorators' => array(
                'FormElements',
                array('HtmlTag', array('tag' => 'dl')),
                'Fieldset',
            ),
            'order' => self::GROUP_ORDER_EVENT_FILTERS,
            'legend' => $this->translate('State and transition type filters')
        ));

        return $this;
    }

    /**
     * @param  string $permission
     * @return bool
     */
    public function hasPermission($permission)
    {
        return Util::hasPermission($permission);
    }

    public function setBranch(Branch $branch)
    {
        $this->branch = $branch;

        return $this;
    }

    protected function allowsExperimental()
    {
        // NO, it is NOT a good idea to use this. You'll break your monitoring
        // and nobody will help you.
        if ($this->allowsExperimental === null) {
            $this->allowsExperimental = $this->db->settings()->get(
                'experimental_features'
            ) === 'allow';
        }

        return $this->allowsExperimental;
    }

    protected function enumStates()
    {
        $set = new StateFilterSet();
        return $set->enumAllowedValues();
    }

    protected function enumTypes()
    {
        $set = new TypeFilterSet();
        return $set->enumAllowedValues();
    }
}
