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

class FieldEditingUI
{
    # constants to define the operators supported by the editing UI
    const OP_NOP = 0;
    const OP_SET = 1;
    const OP_CLEAR = 2;
    const OP_CLEARALL = 3;
    const OP_APPEND = 4;
    const OP_PREPEND = 5;
    const OP_REPLACE = 6;
    const OP_FIND_REPLACE = 7;

    /**
    * Create a UI for specifing edits to metadata fields.
    * @param string $EditFormName Name to use for the HTML elements.
    *    The form cannot contain any input elements whose names are
    *    EditFormName.
    * @param int $SchemaId Schema Id (OPTIONAL, default Resource schema).
    */
    public function __construct(
            $EditFormName,
            $SchemaId=MetadataSchema::SCHEMAID_DEFAULT)
    {
        $this->EditFormName = $EditFormName;
        $this->Schema = new MetadataSchema($SchemaId);

        $this->Fields = array();

        $this->AllowedFieldTypes =
            MetadataSchema::MDFTYPE_TEXT |
            MetadataSchema::MDFTYPE_PARAGRAPH |
            MetadataSchema::MDFTYPE_NUMBER |
            MetadataSchema::MDFTYPE_DATE |
            MetadataSchema::MDFTYPE_TIMESTAMP |
            MetadataSchema::MDFTYPE_FLAG |
            MetadataSchema::MDFTYPE_TREE |
            MetadataSchema::MDFTYPE_CONTROLLEDNAME |
            MetadataSchema::MDFTYPE_OPTION |
            MetadataSchema::MDFTYPE_URL |
            MetadataSchema::MDFTYPE_REFERENCE ;
    }

    /**
    * Add a field to the list of editable fields.
    * @param int|string $FieldNameOrId Field name or id
    * @param string $CurrentValue Initial value to display
    * @param mixed $CurrentOperator Initial operator (one of the OP_XX class constants)
    * @param bool $AllowRemoval TRUE if this field should be removable
    *   (OPTIONAL, default FALSE)
    */
    public function AddField(
            $FieldNameOrId,
            $CurrentValue = NULL,
            $CurrentOperator = NULL,
            $AllowRemoval = FALSE)
    {
        # if a field name was passed in, convert it to a field id
        if (!is_numeric($FieldNameOrId))
        {
            $FieldNameOrId = $this->Schema->GetFieldByName($FieldNameOrId)->Id();
        }

        $this->Fields[]= array(
            "Type" => "Regular",
            "FieldId" => $FieldNameOrId,
            "CurrentValue" => $CurrentValue,
            "CurrentOperator" => $CurrentOperator,
            "AllowRemoval" => $AllowRemoval );
    }

    /**
    * Add a selectable field to the list of editable fields.
    * @param array $FieldTypesOrIds Either an array of FieldIds, or a
    *   bitmask of MDFTYPE_ constants specifying allowed fields
    *   (OPTIONAL, defaults to all fields in the schema supported by the
    *   editing UI)
    * @param int|null $CurrentFieldId FieldId giving the field selected by default
    *   (OPTIONAL, default NULL)
    * @param string $CurrentValue Initial value to display
    * @param mixed $CurrentOperator Initial operator to display (one of the
    *   OP_XX class constants)
    * @param bool $AllowRemoval TRUE if this field should be removable
    *   (OPTIONAL, default TRUE)
    */
    public function AddSelectableField(
            $FieldTypesOrIds = NULL,
            $CurrentFieldId = NULL,
            $CurrentValue = NULL,
            $CurrentOperator = NULL,
            $AllowRemoval = TRUE)
    {
        $Options = $this->TypesOrIdsToFieldList($FieldTypesOrIds);

        if (count($Options)>0)
        {
            $this->Fields[]= array(
                "Type" => "Selectable",
                "SelectOptions" => $Options,
                "CurrentValue" => $CurrentValue,
                "CurrentOperator" => $CurrentOperator,
                "AllowRemoval" => $AllowRemoval );
        }
    }

    /**
    * Add a button to create more fields above the button.
    * @param string $Label Label to display on the button (OPTIONAL, default
    *   "Add field")
    * @param mixed $FieldTypesOrIds Either an array of FieldIds, or a
    *   bitmask of MDFTYPE_ constants specifying allowed fields
    *   (OPTIONAL, defaults to all fields in the schema supported by the
    *    editing UI)
    */
    public function AddFieldButton($Label = "Add field", $FieldTypesOrIds = NULL)
    {
        $Options = $this->TypesOrIdsToFieldList($FieldTypesOrIds);

        if (count($Options)>0)
        {
            $this->Fields[]= array(
                "Type" => "AddButton",
                "SelectOptions" => $Options,
                "Label" => $Label,
                "CurrentOperator" => NULL,
                "CurrentValue" => NULL,
                "AllowRemoval" => TRUE );
        }
    }

    /**
    * Display editing form elements enclosed in a <table>.  Note that
    * it still must be wrapped in a <form> that has a submit button.
    * @param string $TableId HTML identifier to use (OPTIONAL, default
    *   NULL)
    * @param string $TableStyle CSS class to attach for this table
    *   (OPTIONAL, default NULL)
    */
    public function DisplayAsTable($TableId = NULL, $TableStyle = NULL)
    {
        print('<table id="'.defaulthtmlentities($TableId).'" '
              .'class="'.defaulthtmlentities($TableStyle).'">');
        $this->DisplayAsRows();
        print('</table>');
    }

    /**
    * Display the table rows for the editing form, without the
    * surrounding <table> tags.
    */
    public function DisplayAsRows()
    {
        # make sure the necessary javascript is required
        $GLOBALS["AF"]->REquireUIFile("jquery-ui.js");
        $GLOBALS["AF"]->RequireUIFile("CW-QuickSearch.js");
        $GLOBALS["AF"]->RequireUIFile("FieldEditingUI.js");

        # get a list of the fields examined in this chunk of UI, to
        # use when constructing the value selector
        $FieldsExamined = array();
        foreach ($this->Fields as $FieldRow)
        {
            if ($FieldRow["Type"] == "Regular")
            {
                $FieldsExamined[]= $FieldRow["FieldId"];
            }
            else
            {
                $FieldsExamined = array_merge(
                    $FieldsExamined,
                    $FieldRow["SelectOptions"]);
            }
        }
        $FieldsExamined = array_unique($FieldsExamined);

        # iterate over each field adding edit rows for all of them

        print('<tr class="cw-feui-empty '.$this->EditFormName.'" '.
              'data-formname="'.$this->EditFormName.'"><td colspan="4">'.
              '<i>No fields selected for editing</i></td></tr>');

        # note that all of the fields we create for these rows will be named
        # $this->EditFormName.'[]' , combining them all into an array of results per
        #   http://php.net/manual/en/faq.html.php#faq.html.arrays
        foreach ($this->Fields as $FieldRow)
        {
            $CurOp = $FieldRow["CurrentOperator"];
            $CurVal = $FieldRow["CurrentValue"];
            $AllowRemoval = $FieldRow["AllowRemoval"];

            print('<tr class="field_row '.$this->EditFormName
                  .($FieldRow["Type"]=="AddButton" ? ' template_row':'').'">');

            print("<td>");
            if ($FieldRow["AllowRemoval"])
            {
                print("<span class=\"cw-button cw-button-elegant cw-button-constrained "
                      ."cw-feui-delete\">X</span>");
            }
            print("</td>");

            if ($FieldRow["Type"] == "Regular")
            {
                $Field = new MetadataField( $FieldRow["FieldId"] );

                # for fields that cannot be selected, we already know
                # the type and can print field-specific elements
                # (operators, etc) rather than relying on js to clean
                # them up for us.
                $TypeName = defaulthtmlentities(
                    str_replace(' ', '', strtolower($Field->TypeAsName())));
                if ($Field->Type() == MetadataSchema::MDFTYPE_OPTION &&
                    $Field->AllowMultiple() )
                {
                    $TypeName = "mult".$TypeName;
                }

                # encode the field for this row in a form value
                print('<td>'.$Field->Name()
                      .'<input type="hidden"'
                      .' name="'.$this->EditFormName.'[]"'
                      .' class="field-subject field-static field-type-'.$TypeName.'"'
                      .' value="S_'.$Field->Id().'"></td>'."\n");

                print('<td><select name="'.$this->EditFormName.'[]">'."\n");
                if (!$AllowRemoval)
                {
                    $this->PrintOp(self::OP_NOP, $CurOp);
                }
                # determine operators, make a select widget
                switch ($Field->Type())
                {
                    case MetadataSchema::MDFTYPE_URL:
                    case MetadataSchema::MDFTYPE_TEXT:
                    case MetadataSchema::MDFTYPE_PARAGRAPH:
                        $this->PrintOp(self::OP_APPEND, $CurOp);
                        $this->PrintOp(self::OP_PREPEND, $CurOp);
                        $this->PrintOp(self::OP_REPLACE, $CurOp);
                        $this->PrintOp(self::OP_FIND_REPLACE, $CurOp);
                        break;

                    case MetadataSchema::MDFTYPE_FLAG:
                        $this->PrintOp(self::OP_SET, $CurOp);
                        break;

                    case MetadataSchema::MDFTYPE_TIMESTAMP:
                    case MetadataSchema::MDFTYPE_DATE:
                    case MetadataSchema::MDFTYPE_NUMBER:
                        $this->PrintOp(self::OP_SET, $CurOp);
                        if ($Field->Optional())
                        {
                            $this->PrintOp(self::OP_CLEARALL, $CurOp);
                        }
                        break;

                    case MetadataSchema::MDFTYPE_OPTION:
                    case MetadataSchema::MDFTYPE_TREE:
                    case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
                    case MetadataSchema::MDFTYPE_REFERENCE:
                        $this->PrintOp(self::OP_SET, $CurOp);
                        $this->PrintOp(self::OP_CLEAR, $CurOp);

                        if ($Field->Optional() &&
                             ($Field->Type() != MetadataSchema::MDFTYPE_OPTION ||
                              $Field->AllowMultiple() ))
                        {
                            $this->PrintOp(self::OP_CLEARALL, $CurOp);
                        }
                        break;

                    default:
                        throw new Exception("Unsupported field type");
                }
                print('</select></td>');

                print('<td>');
                switch ($Field->Type())
                {
                    case MetadataSchema::MDFTYPE_URL:
                    case MetadataSchema::MDFTYPE_TEXT:
                    case MetadataSchema::MDFTYPE_PARAGRAPH:
                    case MetadataSchema::MDFTYPE_NUMBER:
                    case MetadataSchema::MDFTYPE_DATE:
                    case MetadataSchema::MDFTYPE_TIMESTAMP:
                        if ($CurOp == self::OP_FIND_REPLACE)
                        {
                            print('<input type="text" '
                                  .'name="'.$this->EditFormName.'[]" '
                                  .'value="'.defaulthtmlentities($CurVal[0]).'">');
                            print('<input type="text" class="field-value-repl" '
                                  .'name="'.$this->EditFormName.'[]" '
                                  .'value="'.defaulthtmlentities($CurVal[1]).'">');
                        }
                        else
                        {
                            print('<input type="text" '
                                  .'name="'.$this->EditFormName.'[]" '
                                  .'value="'.defaulthtmlentities($CurVal).'">');
                        }
                        break;

                    case MetadataSchema::MDFTYPE_TREE:
                    case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
                    case MetadataSchema::MDFTYPE_REFERENCE:
                        print('<div class="field-value-qs cw-quicksearch '
                              .'cw-quicksearch-fieldid-'.$Field->Id().'" '
                              .'data-fieldid="'.$Field->Id().'">'
                              .'<input class="cw-quicksearch-display '
                              .'cw-resourceeditor-metadatafield F_'
                              .$this->EditFormName.'"'
                              .'value="'.defaulthtmlentities($CurVal).'" />'
                              .'<input name="'.$this->EditFormName.'[]"'
                              .'class="cw-quicksearch-value" type="hidden"'
                              .'value="'.defaulthtmlentitites($CurVal).'" />'
                              .'<div style="display: none;"'
                              .'class="cw-quicksearch-menu">'
                              .'<div class="cw-quicksearch-message ui-front"></div>'
                              .'</div></div>');
                        break;

                    case MetadataSchema::MDFTYPE_FLAG:
                    case MetadataSchema::MDFTYPE_OPTION:
                        print('<select name="'.$this->EditFormName.'[]">');
                        foreach ($Field->GetPossibleValues() as $Id => $Val)
                        {
                            print('<option value="'.$Id.'" '
                                  .'class="field-id-'.$Field->Id().'"'
                                  .( ($CurVal == $Id) ? ' selected' : '')
                                  .'>'.defaulthtmlentities($Val)
                                  .'</option>'."\n");
                        }
                        print('</select>'."\n");
                        break;
                }
                print ('</td>');
            }
            else
            {
                # for selectable fields, we need to generate all the
                # html elements that we might need and then depend on
                # javascript to display only those that are relevant

                # each field will have five elements

                # 1. a field selector
                print('<td><select name="'.$this->EditFormName.'[]" '
                    .'class="field-subject">');
                foreach ($FieldRow["SelectOptions"] as $FieldId)
                {
                    $Field = new MetadataField($FieldId);
                    $TypeName = defaulthtmlentities(
                        str_replace(' ', '', strtolower($Field->TypeAsName())));

                    if ($Field->Type() == MetadataSchema::MDFTYPE_OPTION &&
                        $Field->AllowMultiple() )
                    {
                        $TypeName = "mult".$TypeName;
                    }

                    if (!$Field->Optional())
                    {
                        $TypeName .= " required";
                    }

                    print('<option class="field-type-'.$TypeName.'" '
                          .'data-maxnumsearchresults="'.$Field->NumAjaxResults().'" '
                          .'value="'.$Field->Id().'">'
                          .defaulthtmlentities($Field->Name()).'</option>');
                }
                print('</select></td>');

                # 2.  an operator selector
                $TextTypes = array("url","text","paragraph");

                print('<td><select name="'.$this->EditFormName.'[]" '
                    .'class="field-operator">');

                # for fields that cannot be removed, allow a 'do nothing' option
                if (!$AllowRemoval)
                {
                    $this->PrintOp(self::OP_NOP, $CurOp,
                      array("flag", "tree", "option",
                      "multoption", "controlledname",
                      "reference", "timstamp", "date",
                      "number") );
                }

                # display all avaialble operators, annotated such that
                # js can switch between them
                $this->PrintOp(self::OP_SET, $CurOp,
                               array("flag","tree", "option", "multoption",
                                     "controlledname",
                                     "reference",
                                     "timestamp", "date", "number"));
                $this->PrintOp(self::OP_CLEAR, $CurOp,
                                array("tree", "option",
                                      "multoption", "controlledname",
                                      "reference"));
                $this->PrintOp(self::OP_CLEARALL, $CurOp,
                               array("tree","controlledname","multoption",
                                     "timestamp","date","number"));

                $this->PrintOp(self::OP_APPEND, $CurOp, $TextTypes );
                $this->PrintOp(self::OP_PREPEND, $CurOp, $TextTypes );
                $this->PrintOp(self::OP_REPLACE, $CurOp, $TextTypes );
                $this->PrintOp(self::OP_FIND_REPLACE, $CurOp, $TextTypes );
                print('</select></td><td>');

                # 3. a value selector (for option and flag values)
                print('<select name="'.$this->EditFormName.'[]" '
                    .'class="field-value-select">');
                foreach ($FieldsExamined as $FieldId)
                {
                    $Field = new MetadataField($FieldId);
                    if ($Field->Type() == MetadataSchema::MDFTYPE_FLAG ||
                        $Field->Type() == MetadataSchema::MDFTYPE_OPTION)
                    {
                        foreach ($Field->GetPossibleValues() as $Id => $Val)
                        {
                            print('<option value="'.$Id.'" '
                                  .'class="field-id-'.$Field->Id().'">'
                                  .defaulthtmlentities($Val)
                                  .'</option>'."\n");
                        }
                    }
                }
                print('</select>');

                # 4. two text entries (free-form text and possible replacement)
                print('<input type="text" class="field-value-edit" '
                     .'name="'.$this->EditFormName.'[]" '
                     .'value="'.defaulthtmlentities($CurVal).'">'
                     .'<input type="text" class="field-value-repl" '
                     .'name="'.$this->EditFormName.'[]" '
                     .'value="'.defaulthtmlentities($CurVal).'">');

                # 5. an ajax search box
                # CW-QuickSearch.js wants a textarea
                print('<div class="field-value-qs cw-quicksearch '
                    . 'cw-quicksearch-template">'
                    .'<input class="cw-quicksearch-display '
                    .'cw-resourceeditor-metadatafield F_'.$this->EditFormName.'" '
                    .'value="'.defaulthtmlentities($CurVal).'" />'
                    .'<input name="'.$this->EditFormName.'[]" '
                    .'class="cw-quicksearch-value" type="hidden" '
                    .'value="'.defaulthtmlentities($CurVal).'" />'
                    .'<div style="display: none;" '
                    .'class="cw-quicksearch-menu">'
                    .'<div class="cw-quicksearch-message ui-front"></div>'
                    .'</div></div>');

                if ($FieldRow["Type"] == "AddButton")
                {
                    print('</tr><tr class="button_row"><td colspan="4">'
                    .'<span class="cw-button cw-button-elegant cw-feui-add">'
                    .defaulthtmlentities($FieldRow["Label"]).'</span></td></tr>');
                }
            }
            print('</tr>');
        }
    }

    /**
    * Extract values from a dynamics field edit/modification form.
    * @return array of arrays like
    *      array("FieldId" => $FieldId, "Op" => $Operator, "Val" => $Value,) ...)
    *  extracted from the $_POST data for $EditFormName.
    */
    public function GetValuesFromFormData()
    {
        $Results = array();

        if (!isset($_POST[$this->EditFormName]))
        {
            return $Results;
        }

        # extract the array of data associated with our EditFormName
        $FormData = $_POST[$this->EditFormName];

        while (count($FormData))
        {
            # first element of each row is a field id
            $FieldId = array_shift($FormData);
            $Op = array_shift($FormData);

            # when the row was static, it'll have a 'S_' prefix
            # to make it non-numeric
            if (!is_numeric($FieldId))
            {
                # remove the S_ prefix to get the real field id
                $FieldId = substr($FieldId, 2);

                # grab the value(s) for this field
                $Val = array_shift($FormData);
                if ($Op == self::OP_FIND_REPLACE)
                {
                    $Val2 = array_shift($FormData);
                }
            }
            else
            {
                # for selectable fields, we'll have all possible
                # elements and will need to grab the correct ones for
                # the currently selected field
                $SelectVal = array_shift($FormData);
                $TextVal   = array_shift($FormData);
                $TextVal2  = array_shift($FormData);
                $SearchVal = array_shift($FormData);

                $Field = new MetadataField($FieldId);

                switch ($Field->Type())
                {
                    case MetadataSchema::MDFTYPE_PARAGRAPH:
                    case MetadataSchema::MDFTYPE_URL:
                    case MetadataSchema::MDFTYPE_TEXT:
                    case MetadataSchema::MDFTYPE_NUMBER:
                    case MetadataSchema::MDFTYPE_DATE:
                    case MetadataSchema::MDFTYPE_TIMESTAMP:
                        $Val = $TextVal;
                        break;

                    case MetadataSchema::MDFTYPE_TREE:
                    case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
                    case MetadataSchema::MDFTYPE_REFERENCE:
                        $Val = $SearchVal;
                        break;

                    case MetadataSchema::MDFTYPE_FLAG:
                    case MetadataSchema::MDFTYPE_OPTION:
                        $Val = $SelectVal;
                        break;

                    default:
                        throw new Exception("Unsupported field type");
                }
            }

            $ResRow = array(
                "FieldId" => $FieldId, "Op" => $Op, "Val" => $Val );

            if ($Op == self::OP_FIND_REPLACE)
            {
                $ResRow["Val2"] = $TextVal2;
            }

            $Results[]= $ResRow;
        }

        return $Results;
    }

    /**
    * Load a configured set of fields.
    * @param array $Data Fields to load in the format from GetValuesFromFormData()
    * @see GetValuesFromFormData()
    */
    public function LoadConfiguration($Data)
    {
        foreach ($Data as $Row)
        {
            $this->AddField(
                $Row["FieldId"],
                ($Row["Op"] == self::OP_FIND_REPLACE) ?
                array( $Row["Val"], $Row["Val2"] ) :
                $Row["Val"],
                $Row["Op"],
                TRUE);
        }
    }

    /**
    * Apply the changes extracted from an editing form to a specified resource.
    * @param Resource $Resource Resource to modify.
    * @param CWUser $User User for permissions checks, or NULL when
    *     checks should be skipped.
    * @param array $ChangesToApply Changes in the format from
    *     GetValuesFromFormData().
    * @return TRUE when resource was changed, FALSE otherwise.
    */
    public static function ApplyChangesToResource($Resource, $User, $ChangesToApply)
    {
        $Changed = FALSE;

        foreach ($ChangesToApply as $Change)
        {
            $Field = new MetadataField($Change["FieldId"]);

            if ( ($User === NULL ||
                  $Resource->UserCanEditField($User, $Field) ) &&
                 $Field->Editable() )
            {
                $OldVal = $Resource->Get( $Field );

                switch ($Change["Op"])
                {
                    case self::OP_NOP:
                        break;

                    case self::OP_SET:
                        self::ModifyFieldValue($User, $Resource, $Field, $Change["Val"]);
                        break;

                    case self::OP_REPLACE:
                        if ( strlen($Change["Val"]) > 0 ||
                             $Field->Optional() )
                        {
                            self::ModifyFieldValue($User, $Resource,
                                                   $Field, $Change["Val"]);
                        }
                        break;

                    case self::OP_FIND_REPLACE:
                        self::ModifyFieldValue(
                            $User,
                            $Resource,
                            $Field,
                            str_replace($Change["Val"], $Change["Val2"], $OldVal));
                        break;

                    case self::OP_CLEAR:
                        if (isset($OldVal[$Change["Val"]]) &&
                            ($Field->Optional() || count($OldVal)>1) )
                        {
                            $Resource->Clear($Field, $Change["Val"]);
                        }
                        break;

                    case self::OP_CLEARALL:
                        if ($Field->Optional())
                        {
                            $Resource->Clear($Field);
                        }
                        break;

                    case self::OP_APPEND:
                        $Sep = $Field->Type() == MetadataSchema::MDFTYPE_PARAGRAPH ?
                            "\n" : " " ;
                        self::ModifyFieldValue($User, $Resource, $Field,
                                               $OldVal.$Sep.$Change["Val"]);
                        break;

                    case self::OP_PREPEND:
                        $Sep = $Field->Type() == MetadataSchema::MDFTYPE_PARAGRAPH ?
                            "\n" : " " ;
                        self::ModifyFieldValue($User, $Resource, $Field,
                                               $Change["Val"].$Sep.$OldVal);
                        break;
                }

                $NewVal = $Resource->Get( $Field );
                if ($NewVal != $OldVal)
                {
                    $Changed = TRUE;
                }
            }
        }

        # if there were any changes
        if ($Changed)
        {
            # update timestamp fields
            $Schema = new MetadataSchema( $Resource->SchemaId() );
            $TimestampFields = $Schema->GetFields(
                MetadataSchema::MDFTYPE_TIMESTAMP);
            foreach ($TimestampFields as $Field)
            {
                if ($Field->UpdateMethod() ==
                    MetadataField::UPDATEMETHOD_ONRECORDCHANGE)
                {
                    $Resource->Set($Field, "now");
                }
            }

            # if we're in the default schema and have a user, then
            #  update Last Modified By Id
            if ($Schema->Id() == MetadataSchema::SCHEMAID_DEFAULT &&
                $User !== NULL)
            {
                $Resource->Set("Last Modified By Id", $User);
            }

            # if we were working on a real resource
            if (!$Resource->IsTempResource())
            {
                # signal resource modification event
                $GLOBALS["AF"]->SignalEvent(
                    "EVENT_RESOURCE_MODIFY", array("Resource" => $Resource));

                # queue updates to search, recommender, and resource counts
                $SearchEngine = new SPTSearchEngine();
                $SearchEngine->QueueUpdateForItem(
                    $Resource->Id(),
                    $GLOBALS["SysConfig"]->SearchEngineUpdatePriority());

                $Recommender = new SPTRecommender();
                $Recommender->QueueUpdateForItem(
                    $Resource->Id(),
                    $GLOBALS["SysConfig"]->RecommenderEngineUpdatePriority());
            }
        }

        return $Changed;
    }

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

    private $EditFormName;
    private $SchemaId;
    private $Fields;

    # mapping of operator constants to their friendly names
    private $OpNames = array(
        self::OP_NOP => "Do nothing",
        self::OP_SET => "Set",
        self::OP_CLEAR => "Clear One Value",
        self::OP_CLEARALL => "Clear All Values",
        self::OP_APPEND => "Append",
        self::OP_PREPEND => "Prepend",
        self::OP_REPLACE => "Replace With",
        self::OP_FIND_REPLACE => "Find/Replace" );

    # to store a bitmask
    private $AllowedFieldTypes;

    /**
    * Print an <option> for an operator, with CSS classes needed by
    * javascript interface helper.
    * @param string $Value Initial value to display.
    * @param string $Selected Value that should be selected.
    * @param array $TypeNames Names of the CWIS field types for which
    *   this operator is appropriate, converted to lowercase.
    */
    private function PrintOp($Value, $Selected=NULL, $TypeNames=array())
    {
        $Classes = array();
        foreach ($TypeNames as $Name)
        {
            $Classes[]= "field-type-".$Name;
        }

        print('<option value="'.$Value.'" '
              .($Selected==$Value ? 'selected' : '')
              .'class="'.implode(' ', $Classes).'"'
              .'>'.$this->OpNames[$Value]
              .'</option>'."\n");
    }

    /**
    * Convert FieldTypesOrIds into a list of fields.
    * @param null|array $FieldTypesOrIds NULL, array of FieldIds, or
    *     Bitmask of MDFType values
    * @return array of FieldIds
    */
    private function TypesOrIdsToFieldList($FieldTypesOrIds)
    {
        if ($FieldTypesOrIds === NULL)
        {
            $FieldTypesOrIds = $this->AllowedFieldTypes;
        }

        $FieldList = array();

        if (is_array($FieldTypesOrIds))
        {
            # if we were given a list of fields, add all the editable ones
            foreach ($FieldTypesOrIds as $FieldId)
            {
                $Field = $this->Schema->GetField($FieldId);
                if ($Field instanceof MetadataField && $Field->Editable())
                {
                    $FieldList[]= $FieldId;
                }
            }
        }
        else
        {
            # otherwise, iterate over all supported fields and add the editable ones
            foreach ($this->Schema->GetFields($FieldTypesOrIds) as $Field)
            {
                if ($Field->Editable())
                {
                    $FieldList[]= $Field->Id();
                }
            }
        }

        return $FieldList;
    }

    /**
    * Modify fields, signaling events and performing keyword
    *   substitutions as necessary.
    * @param User $User User performing the modifications
    * @param Resource $Resource Resource to modify
    * @param MetadataField $Field Field to modify
    * @param mixed $NewValue Value to set
    */
    private static function ModifyFieldValue($User, $Resource, $Field, $NewValue)
    {
        # process substitutions for fields where they apply
        $ShouldDoSubs = array(
            MetadataSchema::MDFTYPE_TEXT,
            MetadataSchema::MDFTYPE_PARAGRAPH );

        if (in_array($Field->Type(), $ShouldDoSubs))
        {
            $Substitutions = array(
                "X-USERNAME-X"  => ($User!==NULL) ? $User->Get("UserName") : "",
                "X-USEREMAIL-X" => ($User!==NULL) ? $User->Get("EMail") : "",
                "X-DATE-X"      => date("M j Y"),
                "X-TIME-X"      => date("g:ia T") );

            $NewValue = str_replace(
                array_keys($Substitutions),
                array_values($Substitutions),
                $NewValue);
        }

        # process edit hooks for fields where they apply
        $ShouldCallHooks = array(
            MetadataSchema::MDFTYPE_TEXT,
            MetadataSchema::MDFTYPE_NUMBER,
            MetadataSchema::MDFTYPE_DATE,
            MetadataSchema::MDFTYPE_TIMESTAMP,
            MetadataSchema::MDFTYPE_PARAGRAPH,
            MetadataSchema::MDFTYPE_FLAG,
            MetadataSchema::MDFTYPE_URL);

        if (in_array($Field->Type(), $ShouldCallHooks))
        {
            $SignalResult = $GLOBALS["AF"]->SignalEvent(
                "EVENT_POST_FIELD_EDIT_FILTER", array(
                    "Field" => $Field,
                    "Resource" => $Resource,
                    "Value" => $NewValue));
            $NewValue = $SignalResult["Value"];
        }

        # update the desired field with the new value
        $Resource->Set($Field, $NewValue);
    }
}
