<?PHP
#
#   FILE:  ConfigSettingsUI.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2014 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

/**
* Class supplying a standard user interface for viewing and setting
* configuration parameters.
*/
class ConfigSettingsUI {

    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
    * Class constructor.
    * @param array $CfgParams Array of arrays of configuration setting
    *       parameters.
    * @param array $CfgValues Associative array of current values for
    *       configuration settings, with setting names for the index.
    * @param string $UniqueKey Unique string to include in form field names
    *       to distinguish them from other fields in the form.  (OPTIONAL)
    */
    function __construct($CfgParams, $CfgValues, $UniqueKey = NULL)
    {
        $this->CfgParams = $CfgParams;
        $this->CfgValues = $CfgValues;
        $this->UniqueKey = $UniqueKey;
    }

    /**
    * Display HTML table with settings parameters.
    * @param string $TableId CSS ID for table element.  (OPTIONAL)
    * @param string $TableStyle CSS styles for table element.  (OPTIONAL)
    */
    function DisplaySettingsTable($TableId = NULL, $TableStyle = NULL)
    {
        # display nothing if there are no settings
        if (!count($this->CfgParams)) {  return;  }

        # check whether table should be split into sections
        $TableIsSectioned = FALSE;
        foreach ($this->CfgParams as $Name => $Params)
        {
            if ($Params["Type"] == "Heading") {  $TableIsSectioned = TRUE;  }
        }

        # begin table
        ?><table class="cw-table cw-table-fullsize cw-table-sideheaders cw-table-padded cw-table-striped <?PHP
        if ($TableIsSectioned) {  print(" cw-table-sectioned");  }
        ?> cw-content-sysconfigtable"<?PHP
        if ($TableId) {  print(" id=\"".$TableId."\"");  }
        if ($TableStyle) {  print(" style=\"".$TableStyle."\"");  }
        ?>>
        <tbody><?PHP

        # for each setting
        foreach ($this->CfgParams as $Name => $Params)
        {
            # if setting is actually a section heading
            if ($Params["Type"] == "Heading")
            {
                # split table section and display heading
                if (isset($HeadingAlreadyDisplayed)) {  print("</tbody><tbody>");  }
                ?><tr><th colspan="3" scope="rowspan"><?PHP
                print($Params["Label"]);
                $HeadingAlreadyDisplayed = TRUE;
                ?></th></tr><?PHP
            }
            else
            {
                $FieldName = "F_".preg_replace("/[^a-zA-Z0-9]/", "", $Name);
                $IsTallRow = !isset($Params["Units"])
                        && !in_array($Params["Type"],
                                array("Flag", "Text", "Number"));
                ?>
                <tr valign="top"<?PHP
                        if ($IsTallRow) print' class="cw-content-tallrow"';  ?>>
                    <th style="padding-top: 9px;">
                        <label for="<?PHP  print $FieldName;
                                ?>"><?PHP  print $Params["Label"];  ?></label>
                    </th>
                    <td <?PHP  if (!isset($Params["Help"])) {
                                    print "colspan=\"2\"";  }  ?>><?PHP
                            $this->DisplaySetting($Name,
                                    $this->CfgValues[$Name], $Params);  ?></td>
                    <?PHP  if (isset($Params["Help"])) {  ?>
                    <td style="width: 50%; vertical-align: top; padding-top: 9px;"><?PHP
                            print $Params["Help"];  ?></td>
                    <?PHP  }  ?>
                </tr>
                <?PHP
            }
        }

        # end table
        ?></tbody>
        </table><?PHP
    }

    /**
    * Retrieve values set by form.
    * @return array Array of configuration settings, with setting names
    *       for the index, and new setting values for the values.
    */
    function GetNewSettingsFromForm()
    {
        # for each configuration setting
        $NewSettings = array();
        foreach ($this->CfgParams as $Name => $Params)
        {
            # determine form field name (matches mechanism in HTML)
            $FieldName = $this->GetFormFieldName($Name);

            # assume the plugin value will not change
            $DidValueChange = FALSE;
            $OldValue = $this->CfgValues[$Name];
            $NewSettings[$Name] = $OldValue;

            # retrieve value based on configuration parameter type
            switch ($Params["Type"])
            {
                case "Flag":
                    # if radio buttons were used
                    if (array_key_exists("OnLabel", $Params)
                            && array_key_exists("OffLabel", $Params))
                    {
                        if (isset($_POST[$FieldName]))
                        {
                            $NewValue = ($_POST[$FieldName] == "1") ? TRUE : FALSE;

                            # flag that the values changed if they did
                            $DidValueChange = $this->DidValueChange(
                                    $OldValue, $NewValue);

                            $NewSettings[$Name] = $NewValue;
                        }
                    }
                    # else checkbox was used
                    else
                    {
                        $NewValue = isset($_POST[$FieldName]) ? TRUE : FALSE;

                        # flag that the values changed if they did
                        $DidValueChange = $this->DidValueChange($OldValue, $NewValue);

                        $NewSettings[$Name] = $NewValue;
                    }
                    break;

                case "Option":
                    $NewValue = GetArrayValue($_POST, $FieldName, array());

                    # flag that the values changed if they did
                    $DidValueChange = $this->DidValueChange($OldValue, $NewValue);

                    $NewSettings[$Name] = $NewValue;
                    break;

                case "Privileges":
                case "MetadataField":
                    $NewValue = GetArrayValue($_POST, $FieldName, array());
                    if ($NewValue == "-1") {  $NewValue = array();  }

                    # flag that the values changed if they did
                    $DidValueChange = $this->DidValueChange($OldValue, $NewValue);

                    $NewSettings[$Name] = $NewValue;
                    break;

                default:
                    if (isset($_POST[$FieldName]))
                    {
                        $NewValue = $_POST[$FieldName];

                        # flag that the values changed if they did
                        $DidValueChange = $this->DidValueChange($OldValue, $NewValue);

                        $NewSettings[$Name] = $NewValue;
                    }
                    break;
            }

            # if value changed and there is an event to signal for changes
            if ($DidValueChange && $this->SettingChangeEventName)
            {
                # add parameters for this event to existing parameters
                $EventParams = array();
                foreach ($EventParams as $ParamName => $ParamValue)
                {
                    switch ($ParamName)
                    {
                        case "SettingName":
                            $EventParams[$ParamName] = $Name;
                            break;

                        case "OldValue":
                            $EventParams[$ParamName] = $OldValue;
                            break;

                        case "NewValue":
                            $EventParams[$ParamName] = $NewValue;
                            break;

                        default:
                            $EventParams[$ParamName] = $ParamValue;
                            break;
                    }
                }
                $EventParams = array_merge($EventParams,
                        $this->SettingChangeEventParams);

                # signal event
                $GLOBALS["AF"]->SignalEvent(
                        $this->SettingChangeEventName, $EventParams);
            }
        }

        # return updated setting values to caller
        return $NewSettings;
    }

    /**
    * Set event to signal when retrieving values from form when settings
    * have changed.  If the supplied event parameters include parameter
    * names (indexes) of "SettingName", "OldValue", or "NewValue", the
    * parameter value will be replaced with an appropriate value before
    * the event is signaled.
    * @param string $EventName Name of event to signal.
    * @param array $EventParams Array of event parameters, with CamelCase
    *       parameter names for index.  (OPTIONAL)
    * @see ConfigSettingsUI::GetNewsettingsFromForm()
    */
    function SetEventToSignalOnChange($EventName, $EventParams = array())
    {
        $this->SettingChangeEventName = $EventName;
        $this->SettingChangeEventParams = $EventParams;
    }


    # ---- PRIVATE INTERFACE -------------------------------------------------

    private $CfgParams;
    private $CfgValues;
    private $SettingChangeEventName = NULL;
    private $SettingChangeEventParams = array();

    /**
    * Display HTML form field for specified setting.
    * @param string $Name Setting name.
    * @param mixed $Value Current value for setting.
    * @param array $Params Setting parameters.
    */
    private function DisplaySetting($Name, $Value, $Params)
    {
        $FieldName = $this->GetFormFieldName($Name);

        switch ($Params["Type"])
        {
            case "Text":
            case "Number":
                $Size = isset($Params["Size"]) ? $Params["Size"]
                        : (isset($Params["MaxVal"])
                            ? (strlen(intval($Params["MaxVal"]) + 1)) : 40);
                $MaxLen = isset($Params["MaxLength"]) ? $Params["MaxLength"]
                        : (isset($Params["MaxVal"])
                            ? (strlen(intval($Params["MaxVal"]) + 3)) : 80);
                print('<input type="text" size="'.$Size.'" maxlength="'
                        .$MaxLen.'" id="'.$FieldName.'" name="'.$FieldName.'" value="'
                        .htmlspecialchars($Value).'" />');
                break;

            case "Paragraph":
                $Rows = isset($Params["Rows"]) ? $Params["Rows"] : 4;
                $Columns = isset($Params["Columns"]) ? $Params["Columns"] : 40;
                print('<textarea rows="'.$Rows.'" cols="'.$Columns
                        .'" id="'.$FieldName.'" name="'.$FieldName.'">'
                        .htmlspecialchars($Value)
                        .'</textarea>');
                break;

            case "Flag":
                if (array_key_exists("OnLabel", $Params)
                        && array_key_exists("OffLabel", $Params))
                {
                    print('<input type="radio" id="'.$FieldName.'On" name="'
                            .$FieldName.'" value="1"'
                            .($Value ? ' checked' : '')
                            .' /> <label for="'.$FieldName.'On">'.$Params["OnLabel"]
                            ."</label>\n");
                    print('<input type="radio" id="'.$FieldName.'Off" name="'
                            .$FieldName.'" value="0"'
                            .($Value ? '' : ' checked')
                            .' /> <label for="'.$FieldName.'Off">'.$Params["OffLabel"]
                            ."</label>\n");
                }
                else
                {
                    print('<input type="checkbox" id="'.$FieldName.'" name="'
                            .$FieldName.'" '
                            .($Value ? ' checked' : '')
                            ." />\n");
                }
                break;

            case "Option":
                $AllowMultiple =
                    isset($Params["AllowMultiple"]) && $Params["AllowMultiple"];
                $FieldName = $AllowMultiple ? $FieldName."[]" : $FieldName;

                PrintOptionList(
                    $FieldName,
                    $Params["Options"],
                    $Value,
                    "", isset($Params["Rows"]) ? $Params["Rows"] : 1, 1,
                    $AllowMultiple,
                    NULL,
                    "auto");
                break;

            case "MetadataField":
                $AllowMultiple =
                    isset($Params["AllowMultiple"]) && $Params["AllowMultiple"];
                $FieldName = $AllowMultiple ? $FieldName."[]" : $FieldName;
                $FieldTypes = GetArrayValue($Params, "FieldTypes");
                $SchemaId = GetArrayValue($Params, "SchemaId", MetadataSchema::SCHEMAID_DEFAULT);

                $Schema = new MetadataSchema($SchemaId);
                print $Schema->GetFieldsAsOptionList(
                        $FieldName, $FieldTypes,
                        $Value,
                        !$AllowMultiple, NULL,
                        $AllowMultiple);
                break;

            case "Privileges":
                $AllowMultiple =
                    isset($Params["AllowMultiple"]) && $Params["AllowMultiple"];
                $PrivFactory = new PrivilegeFactory();
                print $PrivFactory->GetItemsAsOptionList(
                        $FieldName, $Value, NULL,
                        ($AllowMultiple ? 15 : 1));
                break;
        }

        if (isset($Params["Units"]))
        {
            ?>&nbsp;<span><?PHP
            print $Params["Units"];
            ?></span><?PHP
        }
    }

    /**
    * Determine if an old setting value is different from a new one.
    * @param mixed $OldValue Old setting value.
    * @param mixed $NewValue New setting value.
    * @return Returns TRUE if the values are different and FALSE otherwise.
    */
    private function DidValueChange($OldValue, $NewValue)
    {
        # didn't change if they are identical
        if ($OldValue === $NewValue)
        {
            return FALSE;
        }

        # need special cases from this point because PHP returns some odd results
        # when performing loose equality comparisons:
        # http://php.net/manual/en/types.comparisons.php#types.comparisions-loose

        # consider NULL and an empty string to be the same. this is in case a setting
        # is currently set to NULL and receives an empty value from the form.
        # $_POST values are always strings
        if (is_null($OldValue) && is_string($NewValue) && !strlen($NewValue)
            || is_null($NewValue) && is_string($OldValue) && !strlen($OldValue))
        {
            return FALSE;
        }

        # if they both appear to be numbers and are equal
        if (is_numeric($OldValue) && is_numeric($NewValue) && $OldValue == $NewValue)
        {
            return FALSE;
        }

        # true-like values
        if ($OldValue === TRUE && ($NewValue === 1 || $NewValue === "1")
            || $NewValue === TRUE && ($OldValue === 1 || $OldValue === "1"))
        {
            return FALSE;
        }

        # false-like values
        if ($OldValue === FALSE && ($NewValue === 0 || $NewValue === "0")
            || $NewValue === FALSE && ($OldValue === 0 || $OldValue === "0"))
        {
            return FALSE;
        }

        # arrays
        if (is_array($OldValue) && is_array($NewValue))
        {
            # they certainly changed if the counts are different
            if (count($OldValue) != count($NewValue))
            {
                return TRUE;
            }

            # the algorithm for associative arrays is slightly different from
            # sequential ones. the values for associative arrays must match the keys
            if (count(array_filter(array_keys($OldValue), "is_string")))
            {
                foreach ($OldValue as $Key => $Value)
                {
                    # it changed if the keys don't match
                    if (!array_key_exists($Key, $NewValue))
                    {
                        return TRUE;
                    }

                    # the arrays changed if a value changed
                    if ($this->DidValueChange($Value, $NewValue[$Key]))
                    {
                        return TRUE;
                    }
                }
            }

            # sequential values don't have to have the same keys, just the same
            # values
            else
            {
                # sort them so all the values match up if they're equal
                sort($OldValue);
                sort($NewValue);

                foreach ($OldValue as $Key => $Value)
                {
                    # the arrays changed if a value changed
                    if ($this->DidValueChange($Value, $NewValue[$Key]))
                    {
                        return TRUE;
                    }
                }
            }

            # the arrays are equal
            return FALSE;
        }

        # they changed
        return TRUE;
    }

    /**
    * Get HTML form field name for specified setting.
    * @param string $SettingName Setting name.
    * @return string Form field name.
    */
    private function GetFormFieldName($SettingName)
    {
        return "F_"
                .($this->UniqueKey ? $this->UniqueKey."_" : "")
                .preg_replace("/[^a-zA-Z0-9]/", "", $SettingName);
    }
}

