<?php

/*
 * Copyright 2005 - 2025 Centreon (https://www.centreon.com/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * For more information : contact@centreon.com
 *
 */

/**
 * Basic error codes
 *
 * @var int
 * @since      1.5.0
 */
define('HTML_QUICKFORM_ADVMULTISELECT_ERROR_INVALID_INPUT', 1);

/**
 * Element for HTML_QuickForm that emulate a multi-select.
 *
 * The HTML_QuickForm_advmultiselect package adds an element to the
 * HTML_QuickForm package that is two select boxes next to each other
 * emulating a multi-select.
 *
 * @category  HTML
 * @package   HTML_QuickForm_advmultiselect
 * @author    Laurent Laville <pear@laurent-laville.org>
 * @copyright 2005-2009 Laurent Laville
 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD
 * @version   Release: 1.5.1
 * @see      http://pear.php.net/package/HTML_QuickForm_advmultiselect
 * @since     Class available since Release 0.4.0
 */
class HTML_QuickForm_advmultiselect extends HTML_QuickForm_select
{
    /**
     * Prefix function name in javascript move selections
     *
     * @var string
     * @since      0.4.0
     */
    public $_jsPrefix;

    /**
     * Postfix function name in javascript move selections
     *
     * @var string
     * @since      0.4.0
     */
    public $_jsPostfix;

    /**
     * Associative array of the multi select container attributes
     *
     * @var array
     * @since      0.4.0
     */
    public $_tableAttributes;

    /**
     * Associative array of the add button attributes
     *
     * @var array
     * @since      0.4.0
     */
    public $_addButtonAttributes;

    /**
     * Associative array of the remove button attributes
     *
     * @var array
     * @since      0.4.0
     */
    public $_removeButtonAttributes;

    /**
     * Associative array of the select all button attributes
     *
     * @var array
     * @since      1.1.0
     */
    public $_allButtonAttributes;

    /**
     * Associative array of the select none button attributes
     *
     * @var array
     * @since      1.1.0
     */
    public $_noneButtonAttributes;

    /**
     * Associative array of the toggle selection button attributes
     *
     * @var array
     * @since      1.1.0
     */
    public $_toggleButtonAttributes;

    /**
     * Associative array of the move up button attributes
     *
     * @var array
     * @since      0.5.0
     */
    public $_upButtonAttributes;

    /**
     * Associative array of the move up button attributes
     *
     * @var array
     * @since      0.5.0
     */
    public $_downButtonAttributes;

    /**
     * Associative array of the move top button attributes
     *
     * @var array
     * @since      1.5.0
     */
    public $_topButtonAttributes;

    /**
     * Associative array of the move bottom button attributes
     *
     * @var array
     * @since      1.5.0
     */
    public $_bottomButtonAttributes;

    /**
     * Defines if both list (unselected, selected) will have their elements be
     * arranged from lowest to highest (or reverse)
     * depending on comparaison function.
     *
     * SORT_ASC  is used to sort in ascending order
     * SORT_DESC is used to sort in descending order
     *
     * @var string ('none' == false, 'asc' == SORT_ASC, 'desc' == SORT_DESC)
     * @since  0.5.0
     */
    public $_sort;

    /**
     * Associative array of the unselected item box attributes
     *
     * @var array
     * @since      0.4.0
     */
    public $_attributesUnselected;

    /**
     * Associative array of the selected item box attributes
     *
     * @var array
     * @since      0.4.0
     */
    public $_attributesSelected;

    /**
     * Associative array of the internal hidden box attributes
     *
     * @var array
     * @since      0.4.0
     */
    public $_attributesHidden;

    /**
     * Default Element template string
     *
     * @var string
     * @since      0.4.0
     */
    public $_elementTemplate;

    /**
     * Default Element stylesheet string
     *
     * @var string
     * @since      0.4.0
     */
    public $_elementCSS = '
#qfams_{id} {
  font: 13.3px sans-serif;
  background-color: #fff;
  overflow: auto;
  height: 14.3em;
  width: 12em;
  border-left:   1px solid #404040;
  border-top:    1px solid #404040;
  border-bottom: 1px solid #d4d0c8;
  border-right:  1px solid #d4d0c8;
}
#qfams_{id} label {
  padding-right: 3px;
  display: block;
}
';

    /**
     * Class constructor
     *
     * Class constructors :
     * Zend Engine 1 uses HTML_QuickForm_advmultiselect, while
     * Zend Engine 2 uses __construct
     *
     * @param string $elementName Dual Select name attribute
     * @param mixed $elementLabel Label(s) for the select boxes
     * @param mixed $options Data to be used to populate options
     * @param mixed $attributes Either a typical HTML attribute string or
     *                          an associative array
     * @param int $sort Either SORT_ASC for auto ascending arrange,
     *                  SORT_DESC for auto descending arrange, or
     *                  NULL for no sort (append at end: default)
     *
     * @return void
     * @since      version 0.4.0 (2005-06-25)
     */
    public function __construct(
        $elementName = null,
        $elementLabel = null,
        $options = null,
        $attributes = null,
        $sort = null,
    ) {

        $opts    = $options;
        $options = null;  // prevent to use the default select element load options
        parent::__construct($elementName, $elementLabel, $options, $attributes);

        // allow to load options at once and take care of fancy attributes
        $this->load($opts);

        // add multiple selection attribute by default if missing
        $this->updateAttributes(['multiple' => 'multiple']);

        if (is_null($this->getAttribute('size'))) {
            // default size is ten item on each select box (left and right)
            $this->updateAttributes(['size' => 10]);
        }
        if (is_null($this->getAttribute('style'))) {
            // default width of each select box
            $this->updateAttributes(['style' => 'width:100px;']);
        }
        $this->_tableAttributes = $this->getAttribute('class');
        if (is_null($this->_tableAttributes)) {
            // default table layout
            $attr = ['border' => '0', 'cellpadding' => '10', 'cellspacing' => '0'];
        } else {
            $attr = ['class' => $this->_tableAttributes];
            $this->_removeAttr('class', $this->_attributes);
        }
        $this->_tableAttributes = $this->_getAttrString($attr);

        // set default add button attributes
        $this->setButtonAttributes('add');
        // set default remove button attributes
        $this->setButtonAttributes('remove');
        // set default selectall button attributes
        $this->setButtonAttributes('all');
        // set default selectnone button attributes
        $this->setButtonAttributes('none');
        // set default toggle selection button attributes
        $this->setButtonAttributes('toggle');
        // set default move up button attributes
        $this->setButtonAttributes('moveup');
        // set default move up button attributes
        $this->setButtonAttributes('movedown');
        // set default move top button attributes
        $this->setButtonAttributes('movetop');
        // set default move bottom button attributes
        $this->setButtonAttributes('movebottom');
        // defines javascript functions names
        $this->_jsPrefix  = 'QFAMS.';
        $this->_jsPostfix = 'moveSelection';

        // set select boxes sort order (none by default)
        if (! isset($sort)) {
            $sort = false;
        }
        if ($sort === SORT_ASC) {
            $this->_sort = 'asc';
        } elseif ($sort === SORT_DESC) {
            $this->_sort = 'desc';
        } else {
            $this->_sort = 'none';
        }

        // set the default advmultiselect element template (with javascript embedded)
        $this->setElementTemplate();
    }

    /**
     * Sets the button attributes
     *
     * In <b>custom example 1</b>, the <i>add</i> and <i>remove</i> buttons
     * have look set by the css class <i>inputCommand</i>.
     *
     * In <b>custom example 2</b>, the basic text <i>add</i> and <i>remove</i>
     * buttons are now replaced by images.
     *
     * In <b>custom example 5</b>, we have ability to sort the selection list
     * (on right side) by :
     * <pre>
     *  - <b>user-end</b>: with <i>Up</i> and <i>Down</i> buttons
     *  - <b>programming</b>: with the QF element constructor $sort option
     * </pre>
     *
     * @param string $button Button identifier, either 'add', 'remove',
     *                       'all', 'none', 'toggle',
     *                       'movetop', 'movebottom'
     *                       'moveup' or 'movedown'
     * @param mixed $attributes (optional) Either a typical HTML attribute string
     *                          or an associative array
     *
     * @throws PEAR_Error $button argument
     *                    is not a string, or not in range
     *                    (add, remove, all, none, toggle,
     *                    movetop, movebottom, moveup, movedown)
     * @return void
     * @since      version 0.4.0 (2005-06-25)
     *
     * @example    examples/qfams_custom_5.php
     *             Custom example 5: source code
     * @see       http://www.laurent-laville.org/img/qfams/screenshot/custom5.png
     *             Custom example 5: screenshot
     *
     * @example    examples/qfams_custom_2.php
     *             Custom example 2: source code
     * @see       http://www.laurent-laville.org/img/qfams/screenshot/custom2.png
     *             Custom example 2: screenshot
     *
     * @example    examples/qfams_custom_1.php
     *             Custom example 1: source code
     * @see       http://www.laurent-laville.org/img/qfams/screenshot/custom1.png
     *             Custom example 1: screenshot
     */
    public function setButtonAttributes($button, $attributes = null)
    {
        if (! is_string($button)) {
            return PEAR::throwError(
                'Argument 1 of HTML_QuickForm_advmultiselect::'
                       . 'setButtonAttributes is not a string',
                HTML_QUICKFORM_ADVMULTISELECT_ERROR_INVALID_INPUT,
                ['level' => 'exception']
            );
        }

        switch ($button) {
            case 'add':
                if (is_null($attributes)) {
                    $this->_addButtonAttributes
                    = ['name'  => 'add', 'value' => ' >> ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_addButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'remove':
                if (is_null($attributes)) {
                    $this->_removeButtonAttributes
                    = ['name'  => 'remove', 'value' => ' << ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_removeButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'all':
                if (is_null($attributes)) {
                    $this->_allButtonAttributes
                    = ['name'  => 'all', 'value' => ' Select All ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_allButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'none':
                if (is_null($attributes)) {
                    $this->_noneButtonAttributes
                    = ['name'  => 'none', 'value' => ' Select None ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_noneButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'toggle':
                if (is_null($attributes)) {
                    $this->_toggleButtonAttributes
                    = ['name'  => 'toggle', 'value' => ' Toggle Selection ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_toggleButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'moveup':
                if (is_null($attributes)) {
                    $this->_upButtonAttributes
                    = ['name'  => 'up', 'value' => ' Up ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_upButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'movedown':
                if (is_null($attributes)) {
                    $this->_downButtonAttributes
                    = ['name'  => 'down', 'value' => ' Down ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_downButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'movetop':
                if (is_null($attributes)) {
                    $this->_topButtonAttributes
                    = ['name'  => 'top', 'value' => ' Top ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_topButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            case 'movebottom':
                if (is_null($attributes)) {
                    $this->_bottomButtonAttributes
                    = ['name'  => 'bottom', 'value' => ' Bottom ', 'type'  => 'button'];
                } else {
                    $this->_updateAttrArray(
                        $this->_bottomButtonAttributes,
                        $this->_parseAttributes($attributes)
                    );
                }
                break;
            default:
                return PEAR::throwError(
                    'Argument 1 of HTML_QuickForm_advmultiselect::'
                           . 'setButtonAttributes has unexpected value',
                    HTML_QUICKFORM_ADVMULTISELECT_ERROR_INVALID_INPUT,
                    ['level' => 'error']
                );
        }
    }

    /**
     * Sets element template
     *
     * @param string $html (optional) The HTML surrounding select boxes and buttons
     * @param string $js (optional) if we need to include qfams javascript handler
     *
     * @return string
     * @since      version 0.4.0 (2005-06-25)
     */
    public function setElementTemplate($html = null, $js = true)
    {
        $oldTemplate = $this->_elementTemplate;

        if (isset($html) && is_string($html)) {
            $this->_elementTemplate = $html;
        } else {
            $this->_elementTemplate = '
{javascript}
<table{class}>
<!-- BEGIN label_2 --><tr><th>{label_2}</th><!-- END label_2 -->
<!-- BEGIN label_3 --><th>&nbsp;</th><th>{label_3}</th></tr><!-- END label_3 -->
<tr>
  <td valign="top">{unselected}</td>
  <td align="center">{add}{remove}</td>
  <td valign="top">{selected}</td>
</tr>
</table>
';
        }
        if ($js == false) {
            $this->_elementTemplate = str_replace(
                '{javascript}',
                '',
                $this->_elementTemplate
            );
        }

        return $oldTemplate;
    }

    /**
     * Gets default element stylesheet for a single multi-select shape render
     *
     * In <b>custom example 4</b>, the template defined allows
     * a single multi-select checkboxes shape. Useful when javascript is disabled
     * (or when browser is not js compliant). In our example, no need to add
     * javascript code, but css is mandatory.
     *
     * @param bool $raw (optional) html output with style tags or just raw data
     *
     * @return string
     * @since      version 0.4.0 (2005-06-25)
     *
     * @example    qfams_custom_4.php
     *             Custom example 4: source code
     * @see       http://www.laurent-laville.org/img/qfams/screenshot/custom4.png
     *             Custom example 4: screenshot
     */
    public function getElementCss($raw = true)
    {
        $id  = $this->getAttribute('name');
        $css = str_replace('{id}', $id, $this->_elementCSS);

        if ($raw !== true) {
            $css = '<style type="text/css">' . PHP_EOL
                 . '<!--' . $css . ' -->' . PHP_EOL
                 . '</style>';
        }

        return $css;
    }

    /**
     * Returns the HTML generated for the advanced mutliple select component
     *
     * @return string
     * @since      version 0.4.0 (2005-06-25)
     */
    public function toHtml()
    {
        if ($this->_flagFrozen) {
            return $this->getFrozenHtml();
        }

        $tabs    = $this->_getTabs();
        $tab     = $this->_getTab();

        $selectId       = $this->getName();
        $selectName     = $this->getName() . '[]';
        $selectNameFrom = $this->getName() . '-f[]';
        $selectNameTo   = $this->getName() . '-t[]';
        $selected_count = 0;

        // placeholder {unselected} existence determines if we will render
        if (! str_contains($this->_elementTemplate, '{unselected}')) {
            // ... a single multi-select with checkboxes
            $this->_jsPostfix = 'editSelection';

            $id = $this->getAttribute('name');

            $strHtmlSelected  = $tab . '<div id="qfams_' . $id . '">' . PHP_EOL;
            $unselected_count = count($this->_options);

            $checkbox_id_suffix = 0;

            foreach ($this->_options as $option) {
                $_labelAttributes
                    = ['style', 'class', 'onmouseover', 'onmouseout'];
                $labelAttributes = [];
                foreach ($_labelAttributes as $attr) {
                    if (isset($option['attr'][$attr])) {
                        $labelAttributes[$attr] = $option['attr'][$attr];
                        unset($option['attr'][$attr]);
                    }
                }

                if (is_array($this->_values)
                    && in_array((string) $option['attr']['value'], $this->_values)) {
                    // The items is *selected*
                    $checked = ' checked="checked"';
                    $selected_count++;
                } else {
                    // The item is *unselected* so we want to put it
                    $checked = '';
                }
                $checkbox_id_suffix++;
                $strHtmlSelected .= $tab
                                 . '<label'
                                 . $this->_getAttrString($labelAttributes) . '>'
                                 . '<input type="checkbox"'
                                 . ' id="' . $selectId . $checkbox_id_suffix . '"'
                                 . ' name="' . $selectName . '"'
                                 . $checked
                                 . $this->_getAttrString($option['attr'])
                                 . ' />' . $option['text'] . '</label>'
                                 . PHP_EOL;
            }
            $strHtmlSelected .= $tab . '</div>' . PHP_EOL;

            $strHtmlHidden     = '';
            $strHtmlUnselected = '';
            $strHtmlAdd        = '';
            $strHtmlRemove     = '';

            // build the select all button with all its attributes
            $jsName     = $this->_jsPrefix . $this->_jsPostfix;
            $attributes = ['onclick' => $jsName
                                             . "('" . $selectId . "', 1);"];
            $this->_allButtonAttributes
                        = array_merge($this->_allButtonAttributes, $attributes);
            $attrStrAll = $this->_getAttrString($this->_allButtonAttributes);
            $strHtmlAll = "<input{$attrStrAll} />" . PHP_EOL;

            // build the select none button with all its attributes
            $attributes = ['onclick' => $jsName
                                             . "('" . $selectId . "', 0);"];
            $this->_noneButtonAttributes
                         = array_merge($this->_noneButtonAttributes, $attributes);
            $attrStrNone = $this->_getAttrString($this->_noneButtonAttributes);
            $strHtmlNone = "<input{$attrStrNone} />" . PHP_EOL;

            // build the toggle selection button with all its attributes
            $attributes = ['onclick' => $jsName
                                             . "('" . $selectId . "', 2);"];
            $this->_toggleButtonAttributes
                           = array_merge(
                               $this->_toggleButtonAttributes,
                               $attributes
                           );
            $attrStrToggle = $this->_getAttrString($this->_toggleButtonAttributes);
            $strHtmlToggle = "<input{$attrStrToggle} />" . PHP_EOL;

            $strHtmlMoveUp      = '';
            $strHtmlMoveDown    = '';
            $strHtmlMoveTop     = '';
            $strHtmlMoveBottom  = '';

            // default selection counters
            $strHtmlSelectedCount = $selected_count . '/' . $unselected_count;
        } else {
            // ... or a dual multi-select
            $this->_jsPostfix = 'moveSelection';
            $jsName           = $this->_jsPrefix . $this->_jsPostfix;

            // set name of Select From Box
            $this->_attributesUnselected
                = ['id' => $selectId . '-f', 'name' => $selectNameFrom, 'ondblclick' => $jsName
                    . "('{$selectId}', "
                    . "this.form.elements['" . $selectNameFrom . "'], "
                    . "this.form.elements['" . $selectNameTo . "'], "
                    . "this.form.elements['" . $selectName . "'], "
                    . "'add', '{$this->_sort}')"];
            $this->_attributesUnselected
                = array_merge($this->_attributes, $this->_attributesUnselected);
            $attrUnselected = $this->_getAttrString($this->_attributesUnselected);

            // set name of Select To Box
            $this->_attributesSelected
                = ['id' => $selectId . '-t', 'name' => $selectNameTo, 'ondblclick' => $jsName
                    . "('{$selectId}', "
                    . "this.form.elements['" . $selectNameFrom . "'], "
                    . "this.form.elements['" . $selectNameTo . "'], "
                    . "this.form.elements['" . $selectName . "'], "
                    . "'remove', '{$this->_sort}')"];
            $this->_attributesSelected
                = array_merge($this->_attributes, $this->_attributesSelected);
            $attrSelected = $this->_getAttrString($this->_attributesSelected);

            // set name of Select hidden Box
            $this->_attributesHidden
                = ['name' => $selectName, 'style' => 'overflow: hidden; visibility: hidden; '
                           . 'width: 1px; height: 0;'];
            $this->_attributesHidden
                = array_merge($this->_attributes, $this->_attributesHidden);
            $attrHidden = $this->_getAttrString($this->_attributesHidden);

            // prepare option tables to be displayed as in POST order
            $append = is_array($this->_values) ? count($this->_values) : 0;
            $arrHtmlSelected = $append > 0 ? array_fill(0, $append, ' ') : [];

            $options           = count($this->_options);
            $arrHtmlUnselected = [];
            if ($options > 0) {
                $arrHtmlHidden = array_fill(0, $options, ' ');

                foreach ($this->_options as $option) {
                    if (is_array($this->_values)
                        && in_array(
                            (string) $option['attr']['value'],
                            $this->_values
                        )) {
                        // Get the post order
                        $key = array_search(
                            $option['attr']['value'],
                            $this->_values
                        );

                        /** The items is *selected* so we want to put it
                         * in the 'selected' multi-select */
                        $arrHtmlSelected[$key] = $option;
                        /** Add it to the 'hidden' multi-select
                         * and set it as 'selected' */
                        if (isset($option['attr']['disabled'])) {
                            unset($option['attr']['disabled']);
                        }
                        $option['attr']['selected'] = 'selected';
                        $arrHtmlHidden[$key]        = $option;
                    } else {
                        /** The item is *unselected* so we want to put it
                         * in the 'unselected' multi-select */
                        $arrHtmlUnselected[] = $option;
                        // Add it to the hidden multi-select as 'unselected'
                        $arrHtmlHidden[$append] = $option;
                        $append++;
                    }
                }
            } else {
                $arrHtmlHidden = [];
            }

            // The 'unselected' multi-select which appears on the left
            $unselected_count = count($arrHtmlUnselected);

            if ($unselected_count == 0) {
                $this->_attributesUnselected['disabled'] = 'disabled';
                $this->_attributesUnselected
                    = array_merge($this->_attributes, $this->_attributesUnselected);
                $attrUnselected = $this->_getAttrString($this->_attributesUnselected);
            }
            $strHtmlUnselected = "<select{$attrUnselected}>" . PHP_EOL;
            if ($unselected_count > 0) {
                foreach ($arrHtmlUnselected as $data) {
                    $strHtmlUnselected
                        .= $tabs . $tab
                        . '<option' . $this->_getAttrString($data['attr']) . '>'
                        . $data['text'] . '</option>' . PHP_EOL;
                }
            } else {
                $strHtmlUnselected .= '<option value="">&nbsp;</option>';
            }
            $strHtmlUnselected .= '</select>';

            // The 'selected' multi-select which appears on the right
            $selected_count = count($arrHtmlSelected);

            if ($selected_count == 0) {
                $this->_attributesSelected['disabled'] = 'disabled';
                $this->_attributesSelected
                    = array_merge($this->_attributes, $this->_attributesSelected);
                $attrSelected = $this->_getAttrString($this->_attributesSelected);
            }
            $strHtmlSelected = "<select{$attrSelected}>" . PHP_EOL;
            if ($selected_count > 0) {
                foreach ($arrHtmlSelected as $data) {
                    if (! isset($data['attr']) || ! isset($data['text'])) {
                        continue;
                    }

                    $strHtmlSelected .= $tabs . $tab
                        . '<option' . $this->_getAttrString($data['attr']) . '>'
                        . $data['text'] . '</option>' . PHP_EOL;
                }
            } else {
                $strHtmlSelected .= '<option value="">&nbsp;</option>';
            }
            $strHtmlSelected .= '</select>';

            // The 'hidden' multi-select
            $strHtmlHidden = "<select{$attrHidden}>" . PHP_EOL;
            if ($arrHtmlHidden !== []) {
                foreach ($arrHtmlHidden as $data) {
                    if (! isset($data['attr']) || ! isset($data['text'])) {
                        continue;
                    }

                    $strHtmlHidden .= $tabs . $tab
                        . '<option' . $this->_getAttrString($data['attr']) . '>'
                        . $data['text'] . '</option>' . PHP_EOL;
                }
            }
            $strHtmlHidden .= '</select>';

            // build the remove button with all its attributes
            $attributes
                = ['onclick' => $jsName
                            . "('{$selectId}', "
                            . "this.form.elements['" . $selectNameFrom . "'], "
                            . "this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "'], "
                            . "'remove', '{$this->_sort}'); return false;"];
            $this->_removeButtonAttributes
                = array_merge($this->_removeButtonAttributes, $attributes);
            $attrStrRemove = $this->_getAttrString($this->_removeButtonAttributes);
            $strHtmlRemove = "<input{$attrStrRemove} />" . PHP_EOL;

            // build the add button with all its attributes
            $attributes
                = ['onclick' => $jsName
                            . "('{$selectId}', "
                            . "this.form.elements['" . $selectNameFrom . "'], "
                            . "this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "'], "
                            . "'add', '{$this->_sort}'); return false;"];
            $this->_addButtonAttributes
                = array_merge($this->_addButtonAttributes, $attributes);
            $attrStrAdd = $this->_getAttrString($this->_addButtonAttributes);
            $strHtmlAdd = "<input{$attrStrAdd} />" . PHP_EOL;

            // build the select all button with all its attributes
            $attributes
                = ['onclick' => $jsName
                            . "('{$selectId}', "
                            . "this.form.elements['" . $selectNameFrom . "'], "
                            . "this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "'], "
                            . "'all', '{$this->_sort}'); return false;"];
            $this->_allButtonAttributes
                = array_merge($this->_allButtonAttributes, $attributes);
            $attrStrAll = $this->_getAttrString($this->_allButtonAttributes);
            $strHtmlAll = "<input{$attrStrAll} />" . PHP_EOL;

            // build the select none button with all its attributes
            $attributes
                = ['onclick' => $jsName
                            . "('{$selectId}', "
                            . "this.form.elements['" . $selectNameFrom . "'], "
                            . "this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "'], "
                            . "'none', '{$this->_sort}'); return false;"];
            $this->_noneButtonAttributes
                = array_merge($this->_noneButtonAttributes, $attributes);
            $attrStrNone = $this->_getAttrString($this->_noneButtonAttributes);
            $strHtmlNone = "<input{$attrStrNone} />" . PHP_EOL;

            // build the toggle button with all its attributes
            $attributes
                = ['onclick' => $jsName
                            . "('{$selectId}', "
                            . "this.form.elements['" . $selectNameFrom . "'], "
                            . "this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "'], "
                            . "'toggle', '{$this->_sort}'); return false;"];
            $this->_toggleButtonAttributes
                = array_merge($this->_toggleButtonAttributes, $attributes);
            $attrStrToggle = $this->_getAttrString($this->_toggleButtonAttributes);
            $strHtmlToggle = "<input{$attrStrToggle} />" . PHP_EOL;

            // build the move up button with all its attributes
            $attributes
                = ['onclick' => "{$this->_jsPrefix}moveUp"
                            . "(this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "']); "
                            . 'return false;'];
            $this->_upButtonAttributes
                = array_merge($this->_upButtonAttributes, $attributes);
            $attrStrUp     = $this->_getAttrString($this->_upButtonAttributes);
            $strHtmlMoveUp = "<input{$attrStrUp} />" . PHP_EOL;

            // build the move down button with all its attributes
            $attributes
                = ['onclick' => "{$this->_jsPrefix}moveDown"
                            . "(this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "']); "
                            . 'return false;'];
            $this->_downButtonAttributes
                = array_merge($this->_downButtonAttributes, $attributes);
            $attrStrDown     = $this->_getAttrString($this->_downButtonAttributes);
            $strHtmlMoveDown = "<input{$attrStrDown} />" . PHP_EOL;

            // build the move top button with all its attributes
            $attributes
                = ['onclick' => "{$this->_jsPrefix}moveTop"
                            . "(this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "']); "
                            . 'return false;'];
            $this->_topButtonAttributes
                = array_merge($this->_topButtonAttributes, $attributes);
            $attrStrTop     = $this->_getAttrString($this->_topButtonAttributes);
            $strHtmlMoveTop = "<input{$attrStrTop} />" . PHP_EOL;

            // build the move bottom button with all its attributes
            $attributes
                = ['onclick' => "{$this->_jsPrefix}moveBottom"
                            . "(this.form.elements['" . $selectNameTo . "'], "
                            . "this.form.elements['" . $selectName . "']); "
                            . 'return false;'];
            $this->_bottomButtonAttributes
                = array_merge($this->_bottomButtonAttributes, $attributes);
            $attrStrBottom     = $this->_getAttrString($this->_bottomButtonAttributes);
            $strHtmlMoveBottom = "<input{$attrStrBottom} />" . PHP_EOL;

            // default selection counters
            $strHtmlSelectedCount = $selected_count;
        }
        $strHtmlUnselectedCount = $unselected_count;

        $strHtmlSelectedCountId   = $selectId . '_selected';
        $strHtmlUnselectedCountId = $selectId . '_unselected';

        // render all part of the multi select component with the template
        $strHtml = $this->_elementTemplate;

        // Prepare multiple labels
        $labels = $this->getLabel();
        if (is_array($labels)) {
            array_shift($labels);
        }
        // render extra labels, if any
        if (is_array($labels)) {
            foreach ($labels as $key => $text) {
                $key     = is_int($key) ? $key + 2 : $key;
                $strHtml = str_replace("{label_{$key}}", $text, $strHtml);
                $strHtml = str_replace("<!-- BEGIN label_{$key} -->", '', $strHtml);
                $strHtml = str_replace("<!-- END label_{$key} -->", '', $strHtml);
            }
        }
        // clean up useless label tags
        if (strpos($strHtml, '{label_')) {
            $strHtml = preg_replace('/\s*<!-- BEGIN label_(\S+) -->'
                           . '.*<!-- END label_\1 -->\s*/i', '', $strHtml);
        }

        $placeHolders = ['{stylesheet}', '{javascript}', '{class}', '{unselected_count_id}', '{selected_count_id}', '{unselected_count}', '{selected_count}', '{unselected}', '{selected}', '{add}', '{remove}', '{all}', '{none}', '{toggle}', '{moveup}', '{movedown}', '{movetop}', '{movebottom}'];
        $htmlElements = [$this->getElementCss(false), $this->getElementJs(false), $this->_tableAttributes, $strHtmlUnselectedCountId, $strHtmlSelectedCountId, $strHtmlUnselectedCount, $strHtmlSelectedCount, $strHtmlUnselected, $strHtmlSelected . $strHtmlHidden, $strHtmlAdd, $strHtmlRemove, $strHtmlAll, $strHtmlNone, $strHtmlToggle, $strHtmlMoveUp, $strHtmlMoveDown, $strHtmlMoveTop, $strHtmlMoveBottom];

        $strHtml = str_replace($placeHolders, $htmlElements, $strHtml);

        $comment = $this->getComment();

        if (! empty($comment)) {
            $strHtml = $tabs . '<!-- ' . $comment . ' //-->' . PHP_EOL . $strHtml;
        }

        return $strHtml;
    }

    /**
     * Returns the javascript code generated to handle this element
     *
     * @param bool $raw (optional) html output with script tags or just raw data
     * @param bool $min (optional) uses javascript compressed version
     *
     * @return string
     * @since      version 0.4.0 (2005-06-25)
     */
    public function getElementJs($raw = true, $min = false)
    {
        $js = __DIR__ . DIRECTORY_SEPARATOR;

        if ($min) {
            $js .= 'qfamsHandler-min.js';
        } else {
            $js .= 'qfamsHandler.js';
        }

        $js = file_exists($js) ? file_get_contents($js) : '';

        if ($raw !== true) {
            $js = '<script type="text/javascript">'
                . PHP_EOL . '//<![CDATA['
                . PHP_EOL . $js
                . PHP_EOL . '//]]>'
                . PHP_EOL . '</script>'
                . PHP_EOL;
        }

        return $js;
    }

    /**
     * Loads options from different types of data sources
     *
     * This method overloaded parent method of select element, to allow
     * loading options with fancy attributes.
     *
     * @param mixed &$options Options source currently supports assoc array or DB_result
     * @param mixed $param1 (optional) See function detail
     * @param mixed $param2 (optional) See function detail
     * @param mixed $param3 (optional) See function detail
     * @param mixed $param4 (optional) See function detail
     *
     * @since      version 1.5.0 (2009-02-15)
     * @throws PEAR_Error
     * @return PEAR_Error|null on error and TRUE on success
     * @see        loadArray()
     */
    public function load(
        &$options,
        $param1 = null,
        $param2 = null,
        $param3 = null,
        $param4 = null,
    ) {

        if (is_array($options)) {
            $ret = $this->loadArray($options, $param1);
        } else {
            $ret = parent::load($options, $param1, $param2, $param3, $param4);
        }

        return $ret;
    }

    /**
     * Loads the options from an associative array
     *
     * This method overloaded parent method of select element, to allow to load
     * array of options with fancy attributes.
     *
     * @param array $arr Associative array of options
     * @param mixed $values (optional) Array or comma delimited string of selected values
     *
     * @since      version 1.5.0 (2009-02-15)
     * @throws PEAR_Error
     * @return PEAR_Error on error and TRUE on success
     * @see        load()
     */
    public function loadArray(array $arr, $values = null)
    {
        if (! is_array($arr)) {
            return PEAR::throwError(
                'Argument 1 of HTML_QuickForm_advmultiselect::'
                       . 'loadArray is not a valid array',
                HTML_QUICKFORM_ADVMULTISELECT_ERROR_INVALID_INPUT,
                ['level' => 'exception']
            );
        }
        if (isset($values)) {
            $this->setSelected($values);
        }
        if (is_array($arr)) {
            foreach ($arr as $key => $val) {
                if (is_array($val)) {
                    $this->addOption($val[0], $key, $val[1]);
                } else {
                    $this->addOption($val, $key);
                }
            }
        }

        return true;
    }

    /**
     * Sets which items should be persistant
     *
     * Sets which items should have the disabled attribute
     * to keep it persistant
     *
     * @param mixed $optionValues Options (key-values) that should be persistant
     * @param bool $persistant (optional) TRUE if persistant, FALSE otherwise
     *
     * @since      version 1.5.0 (2009-02-15)
     * @throws PEAR_Error
     * @return PEAR_Error on error and TRUE on success
     */
    public function setPersistantOptions($optionValues, $persistant = true)
    {
        if (! is_bool($persistant)) {
            return PEAR::throwError(
                'Argument 2 of HTML_QuickForm_advmultiselect::'
                       . 'setPersistantOptions is not a boolean',
                HTML_QUICKFORM_ADVMULTISELECT_ERROR_INVALID_INPUT,
                ['level' => 'exception']
            );
        }
        if (is_string($optionValues)) {
            $optionValues = [$optionValues];
        }
        if (! is_array($optionValues)) {
            return PEAR::throwError(
                'Argument 1 of HTML_QuickForm_advmultiselect::'
                       . 'setPersistantOptions is not a valid array',
                HTML_QUICKFORM_ADVMULTISELECT_ERROR_INVALID_INPUT,
                ['level' => 'exception']
            );
        }

        foreach ($this->_options as $k => $v) {
            if (in_array($v['attr']['value'], $optionValues)) {
                if ($persistant) {
                    $this->_options[$k]['attr']['disabled'] = 'disabled';
                } else {
                    unset($this->_options[$k]['attr']['disabled']);
                }
            }
        }

        return true;
    }

    /**
     * Returns list of persistant options
     *
     * Returns list of persistant options (key-values) that could not
     * be selected or unselected.
     *
     * @since      version 1.5.0 (2009-02-15)
     * @return array
     */
    public function getPersistantOptions()
    {
        $options = [];

        foreach ($this->_options as $k => $v) {
            if (isset($v['attr']['disabled'])) {
                $options[] = $this->_options[$k]['attr']['value'];
            }
        }

        return $options;
    }
}

if (class_exists('HTML_QuickForm')) {
    (new HTML_QuickForm())->registerElementType(
        'advmultiselect',
        'HTML/QuickForm/advmultiselect.php',
        'HTML_QuickForm_advmultiselect'
    );
}
