<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
 * This file is part of Icinga 2 Web.
 *
 * Icinga 2 Web - Head for multiple monitoring backends.
 * Copyright (C) 2013 Icinga Development Team
 *
 * 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.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * @copyright 2013 Icinga Development Team <info@icinga.org>
 * @license   http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
 * @author    Icinga Development Team <info@icinga.org>
 */
// {{{ICINGA_LICENSE_HEADER}}}

namespace Icinga\Form\Config;

use \Zend_Config;
use \Icinga\Application\Config as IcingaConfig;
use \Icinga\Application\Icinga;
use \Icinga\Application\Logger;
use \Icinga\Application\DbAdapterFactory;
use \Icinga\Web\Form;
use \Icinga\Web\Form\Element\Note;
use \Icinga\Web\Form\Decorator\ConditionalHidden;

/**
 * Form for modifying the authentication provider and order.
 *
 * This is a composite form from one or more forms under the Authentication folder
 */
class AuthenticationForm extends Form
{
    /**
     * The configuration to use for populating this form
     *
     * @var IcingaConfig
     */
    private $config = null;

    /**
     * The resources to use instead of the factory provided ones (use for testing)
     *
     * @var null
     */
    private $resources = null;

    /**
     * An array containing all provider subforms currently displayed
     *
     * @var array
     */
    private $backendForms = array();


    /**
     * Set an alternative array of resources that should be used instead of the DBFactory resource set
     * (used for testing)
     *
     * @param array $resources The resources to use for populating the db selection field
     */
    public function setResources(array $resources)
    {
        $this->resources = $resources;
    }

    /**
     * Set the configuration to be used for this form
     *
     * @param IcingaConfig $cfg
     */
    public function setConfiguration($cfg)
    {
        $this->config = $cfg;
    }

    /**
     *  Add a hint to remove the backend identified by $name
     *
     *  The button will have the name "backend_$name_remove"
     *
     *  @param  string $name The backend to add this button for
     *
     *  @return string The id of the added button
     */
    private function addRemoveHint($name)
    {
        $this->addElement(
            'checkbox',
            'backend_' . $name . '_remove',
            array(
                'name'      => 'backend_' . $name . '_remove',
                'label'     => 'Remove this authentication provider',
                'value'     => $name,
                'checked'   => $this->isMarkedForDeletion($name)
            )
        );
        $this->enableAutoSubmit(array('backend_' . $name . '_remove'));
        return 'backend_' . $name . '_remove';
    }

    /**
     *  Add the form for the provider identified by $name, with the configuration $backend
     *
     *  Supported backends are backends with a form found under \Icinga\Form\Config\Authentication.
     *  The backend name ist the (uppercase first) prefix with 'BackendForm' as the suffix.
     *
     *  Originally it was intended to add the provider as a subform. As this didn't really work with
     *  the Zend validation logic (maybe our own validation logic breaks it), we now create the form, but add
     *  all elements to this form explicitly.
     *
     *  @param string       $name       The name of the backend to add
     *  @param Zend_Config  $backend    The configuration of the backend
     */
    private function addProviderForm($name, $backend)
    {
        $type = ucfirst(strtolower($backend->get('backend')));
        $formClass = '\Icinga\Form\Config\Authentication\\' . $type . 'BackendForm';
        if (!class_exists($formClass)) {
            Logger::error('Unsupported backend found in authentication configuration: ' . $backend->get('backend'));
            return;
        }

        $form = new $formClass();
        $form->setBackendName($name);
        $form->setBackend($backend);

        if ($this->resources) {
            $form->setResources($this->resources);
        }

        // It would be nice to directly set the form via
        // this->setForm, but Zend doesn't handle form validation
        // properly if doing so.
        $form->create();
        foreach ($form->getElements() as $elName => $element) {
            if ($elName === 'backend_' . $this->filterName($name) . '_name') {
                continue;
            }
            $this->addElement($element, $elName);
        }
        $this->backendForms[] = $form;
    }

    /**
     * Add the buttons for modifying authentication priorities
     *
     * @param   string  $name   The name of the backend to add the buttons for
     * @param   array   $order  The current order which will be used to determine the changed order
     *
     * @return  array An array containing the newly added form element ids as strings
     */
    public function addPriorityButtons($name, $order = array())
    {
        $formEls = array();
        $priorities = array(
            'up'    =>  join(',', self::moveElementUp($name, $order)),
            'down'  =>  join(',', self::moveElementDown($name, $order))
        );
        if ($priorities['up'] != join(',', $order)) {
            $this->addElement(
                'button',
                'priority' . $name . '_up',
                array(
                    'name'  => 'priority',
                    'label' => 'Move up in authentication order',
                    'value' =>  $priorities['up'],
                    'type'  => 'submit'
                )
            );
            $formEls[] = 'priority' . $name . '_up';
        }
        if ($priorities['down'] != join(',', $order)) {
            $this->addElement(
                'button',
                'priority' . $name . '_down',
                array(
                    'name'  => 'priority',
                    'label' => 'Move down in authentication order',
                    'value' =>  $priorities['down'],
                    'type'  => 'submit'
                )
            );
            $formEls[] = 'priority' . $name . '_down';
        }

        return $formEls;
    }

    /**
     * Overwrite for Zend_Form::populate in order to preserve the modified priority of the backends
     *
     * @param   array $values The values to populate the form with
     *
     * @return  self
     *
     * @see     Zend_Form::populate()
     */
    public function populate(array $values)
    {
        $last_priority = $this->getValue('current_priority');
        parent::populate($values);
        $this->getElement('current_priority')->setValue($last_priority);
        return $this;
    }

    /**
     * Return an array containing all authentication providers in the order they should be used
     *
     * @return array    An array containing the identifiers (section names) of the authentication backend in
     *                  the order they should be persisted
     */
    private function getAuthenticationOrder()
    {
        $request = $this->getRequest();
        $order = $request->getParam(
            'priority',
            $request->getParam('current_priority', null)
        );

        if ($order === null) {
            $order = array_keys($this->config->toArray());
        } else {
            $order = explode(',', $order);
        }

        return $order;
    }

    /**
     * Return true if the backend should be deleted when the changes are persisted
     *
     * @param   string $backendName The name of the backend to check for being in a 'delete' state
     *
     * @return  bool Whether this backend will be deleted on save
     */
    private function isMarkedForDeletion($backendName)
    {
        return intval($this->getRequest()->getParam('backend_' . $backendName . '_remove', 0)) === 1;
    }

    /**
     * Add persistent values to the form in hidden fields
     *
     * Currently this adds the 'current_priority' field to persist priority modifications. This prevents changes in the
     * authentication order to be lost as soon as other changes are submitted (like marking a backend for deletion)
     */
    private function addPersistentState()
    {
        $this->addElement(
            'hidden',
            'current_priority',
            array(
                'name'  =>  'current_priority',
                'value' =>  join(',', $this->getAuthenticationOrder())
            )
        );
    }

    /**
     * Create the authentication provider configuration form
     *
     * @see IcingaForm::create()
     */
    public function create()
    {
        $order = $this->getAuthenticationOrder();

        foreach ($order as $name) {
            $this->addElement(
                new Note(
                    array(
                        'escape' => false,
                        'name'  => 'title_backend_' . $name,
                        'value' => '<h4>Backend ' . $name . '</h4>'
                    )
                )
            );
            $this->addRemoveHint($this->filterName($name));
            $backend = $this->config->get($name, null);
            if ($backend === null) {
                continue;
            }
            if (!$this->isMarkedForDeletion($this->filterName($name))) {
                $this->addProviderForm($name, $backend);
                $this->addPriorityButtons($name, $order);
            }
        }

        $this->addPersistentState();
        $this->enableConditionalDecorator();
        $this->setSubmitLabel('{{SAVE_ICON}} Save Changes');
    }

    /**
     * Return the configuration state defined by this form
     *
     * @return array
     */
    public function getConfig()
    {
        $result = array();
        foreach ($this->backendForms as $name) {

            $name->populate($this->getRequest()->getParams());
            $result += $name->getConfig();

        }
        return $result;
    }

    /**
     *  Enable the "ConditionalHidden" Decorator for all elements in this form
     *
     *  @see ConditionalHidden
     */
    private function enableConditionalDecorator()
    {
        foreach ($this->getElements() as $element) {
            $element->addDecorator(new ConditionalHidden());
        }
    }

    /**
     * Static helper for moving an element in an array one slot up, if possible
     *
     * Example:
     *
     * <pre>
     * $array = array('first', 'second', 'third');
     * moveElementUp('third', $array); // returns ['first', 'third', 'second']
     * </pre>
     *
     * @param   string    $key              The key to bubble up one slot
     * @param   array     $array            The array to work with
     *
     * @return  array                       The modified array
     */
    private static function moveElementUp($key, array $array)
    {
        $swap = null;
        for ($i=0; $i<count($array)-1; $i++) {
            if ($array[$i+1] !== $key) {
                continue;
            }
            $swap = $array[$i];
            $array[$i] = $array[$i+1];
            $array[$i+1] = $swap;
            return $array;
        }
        return $array;
    }

    /**
     * Static helper for moving an element in an array one slot up, if possible
     *
     * Example:
     *
     * <pre>
     * $array = array('first', 'second', 'third');
     * moveElementDown('first', $array); // returns ['second', 'first', 'third']
     * </pre>
     *
     * @param   string    $key              The key to bubble up one slot
     * @param   array     $array            The array to work with
     *
     * @return  array                       The modified array
     */
    private static function moveElementDown($key, array $array)
    {
        $swap = null;
        for ($i=0; $i<count($array)-1; $i++) {
            if ($array[$i] !== $key) {
                continue;
            }
            $swap = $array[$i+1];
            $array[$i+1] = $array[$i];
            $array[$i] = $swap;
            return $array;
        }
        return $array;
    }
}
