<?php

/**
 * MultiModelForm.php
 *
 * Handling of multiple records and models in a form
 *
 * Uses the jQuery plugin RelCopy
 * @link http://www.andresvidal.com/labs/relcopy.html
 *
 * @author Joe Blocher <yii@myticket.at>
 * @copyright 2011 myticket it-solutions gmbh
 * @license New BSD License
 * @category User Interface
 * @version 3.0
 */
class MultiModelForm extends CWidget
{
    const CLASSPREFIX = 'mmf_'; //prefix for tag classes

    /**
     * The model to handle
     *
     * @var CModel $model
     */
    public $model;

    /**
     * Configuration of the form provided by the models method getMultiModelForm()
     *
     * This configuration array defines generation CForm
     * Can be a config array or a config file that returns the configuration
     *
     * @link http://www.yiiframework.com/doc/guide/1.1/en/form.builder
     * @var mixed $elements
     */
    public $formConfig = array();

    /**
     * Array of models loaded from db.
     * Created for example by $model->findAll();
     *
     * @var CModel $data
     */
    public $data;

    /**
     * The controller returns all validated items (array of model)
     * if a validation error occurs.
     * The form will then be rendered with error output.
     * $data will be ignored in this case.
     * @see method run()
     *
     * @var array $validatedItems
     */
    public $validatedItems;

    /**
     * Set to true if the error summary should be rendered for the model of this form
     *
     * @var boolean $showErrorSummary
     */
    public $showErrorSummary = false;


    /**
     * The text of the copy/clone link
     *
     * @var string $addItemText
     */
    public $addItemText = 'Add item';

    /**
     * Show 'Add item' link and empty item in errormode
     *
     * @var boolean $allowAddOnError
     */
    public $showAddItemOnError = true;

    /**
     * The text for the remove link
     * Can be an image tag too.
     * Leave empty to disable removing.
     *
     * @var string $removeText
     */
    public $removeText = 'Remove';

    /**
     * The confirmation text before remove an item
     * Set to null/empty to disable confirmation
     *
     * @var string $removeText
     */
    public $removeConfirm = 'Delete this item?';

    /**
     * The htmlOptions for the remove link
     *
     * @var array $removeHtmlOptions
     */
    public $removeHtmlOptions = array();

    /**
     * Show elements as table
     * If set to true, $fieldsetWrapper, $rowWrapper and $removeLinkWrapper will be ignored
     *
     * @var boolean
     */
    public $tableView = false;

    /**
     * The htmlOptions for the table tag
     *
     * @var array $tableHtmlOptions
     */
    public $tableHtmlOptions = array();

    /**
     * Items are rendered as <tfoot><tr><td>Item1</td><td>Item2</td> ...</tr></tfoot>
     *
     * @var string $tableFootCells
     */
    public $tableFootCells = array();


    /**
     * Set this attribute to enable manual sorting by drag/drop of the multiple items
     * Uses the CJuiSortable widget
     *
     * @var string the name of the attribute
     */
    public $sortAttribute;


    /**
     * The options property of the zii.widgets.jui.CJuiSortable
     *
     * @link http://www.yiiframework.com/doc/api/1.1/CJuiWidget#options-detail
     * @link http://jqueryui.com/demos/sortable/
     *
     * @var array
     */
    public $sortOptions = array(
        'placeholder' => 'ui-state-highlight',
        'opacity' => 0.8,
        'cursor' => 'move'
    );

    /**
     * The wrapper for each fieldset
     *
     * @var array $fieldsetWrapper
     */
    public $fieldsetWrapper = array(
        'tag' => 'div',
        'htmlOptions' => array('class' => 'view'), //'fieldset' is unknown in the default css context of form.css
    );

    /**
     * The wrapper for a row
     *
     * @var array $rowWrapper
     */
    public $rowWrapper = array(
        'tag' => 'div',
        'htmlOptions' => array('class' => 'row'),
    );

    /**
     * The wrapper for the removeLink
     *
     * @var array $fieldsetWrapper
     */
    public $removeLinkWrapper = array(
        'tag' => 'span',
        'htmlOptions' => array(),
    );

    /**
     * The javascript code jsBeforeClone,jsAfterClone ...
     * This allows to handle widgets on cloning.
     * Important: 'this' is the current handled jQuery object
     * For CJuiDatePicker and extension 'datetimepicker' see prepared php-code below: afterNewIdDatePicker,afterNewIdDateTimePicker
     *
     * Usage if you have CJuiDatePicker to clone (assume your form elements are defined in the array $formConfig):
     * 'jsAfterNewId' => MultiModelForm::afterNewIdDateTimePicker($formConfig['elements']['mydatefield']),
     *
     */
    public $jsBeforeClone; // 'jsBeforeClone' => "alert(this.attr('class'));";
    public $jsAfterClone; // 'jsAfterClone' => "alert(this.attr('class'));";
    public $jsBeforeNewId; // 'jsBeforeNewId' => "alert(this.attr('id'));";
    public $jsAfterNewId; // 'jsAfterNewId' => "alert(this.attr('id'));";

    /**
     * Available options for the jQuery plugin RelCopy
     *
     * string excludeSelector - A jQuery selector used to exclude an element and its children
     * integer limit - The number of allowed copies. Default: 0 is unlimited
     * string append - Additional HTML to attach at the end of each copy.
     * string copyClass - A class to attach to each copy
     * boolean clearInputs - Option to clear each copies text input fields or textarea
     *
     * @link http://www.andresvidal.com/labs/relcopy.html
     *
     * @var array $options
     */
    public $options = array();

    /**
     * The assets url
     *
     * @var string $_assets
     */
    private $_assets;

    /**
     * Support for CJuiDatePicker
     * Set 'jsAfterNewId'=MultiModelForm::afterNewIdDateTimePicker($myFormConfig['elements']['mydate'])
     * if you use at least one datepicker.
     *
     * The options will be assigned from the config array of the element
     *
     * @param array $element
     * @return string
     */
    public static function afterNewIdDatePicker($element)
    {
        $options = isset($element['options']) ? $element['options'] : array();
        $jsOptions = CJavaScript::encode($options);

        $language = isset($element['language']) ? $element['language'] : '';
        if (!empty($language))
            $language = "jQuery.datepicker.regional['$language'],";

        return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker'); this.datepicker(jQuery.extend({showMonthAfterYear:false}, $language {$jsOptions}));};";
    }

    /**
     * Support for extension datetimepicker
     * @link http://www.yiiframework.com/extension/datetimepicker/
     *
     * @param array $element
     * @return string
     */
    public static function afterNewIdDateTimePicker($element)
    {
        $options = isset($element['options']) ? $element['options'] : array();
        $jsOptions = CJavaScript::encode($options);

        $language = isset($element['language']) ? $element['language'] : '';
        if (!empty($language))
            $language = "jQuery.datepicker.regional['$language'],";

        return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker').datetimepicker(jQuery.extend($language {$jsOptions}));};";
    }

    /**
     * Support for CJuiAutoComplete: not working - needs review
     *
     * @param array $element
     * @return string
     */

    public static function afterNewIdAutoComplete($element)
    {
        $options = isset($element['options']) ? $element['options'] : array();
        if (isset($element['sourceUrl']))
            $options['source'] = CHtml::normalizeUrl($element['sourceUrl']);
        else
            $options['source'] = $element['source'];

        $jsOptions = CJavaScript::encode($options);

        //return "this.autocomplete($jsOptions);"; //works for non-autocomplete elements
        //return "if(this.hasClass('ui-autocomplete-input')) this.autocomplete($jsOptions);";
        //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete($jsOptions);";
        //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete('destroy').autocomplete($jsOptions);";
        //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);";
        //return "if(this.hasClass('ui-autocomplete-input')) this.unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);";
    }

    /**
     * This static function should be used in the controllers update action
     * The models will be validated before saving
     *
     * If a record is not valid, the invalid model will be set to $model
     * to display error summary
     *
     * @param mixed $model CActiveRecord or other CModel
     * @param array $validatedItems returns the array of validated records
     * @param array $deleteItems
     * @param array $masterValues attributes to assign before saving
     * @param array $formData (default = $_POST)
     * @return boolean
     */
    public static function save($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null)
    {
        //validate if empty: means no validation has been done
        $doValidate = empty($validatedItems) && empty($deleteItems);

        if (!isset($formData))
            $formData = $_POST;

        $sortAttribute = !empty($formData[self::CLASSPREFIX . 'sortAttribute']) ? $formData[self::CLASSPREFIX . 'sortAttribute'] : null;
        $sortIndex = 0;

        if ($doValidate)
        {
            //validate and assign $masterValues
            if (!self::validate($model, $validatedItems, $deleteItems, $masterValues, $formData))
                return false;
        }

        if (!empty($validatedItems))
            foreach ($validatedItems as $item)
            {
                if (!$doValidate) //assign $masterValues
                {
                    if (!empty($masterValues))
                        $item->setAttributes($masterValues, false);
                }

                //if sortable, assign the sortAttribute
                if (!empty($sortAttribute))
                {
                    $sortIndex++;
                    $item->$sortAttribute = $sortIndex;
                }

                if (!$item->save())
                    return false;
            }

        //$deleteItems = array of primary keys to delete
        if (!empty($deleteItems))
            foreach ($deleteItems as $pk)
                if (!empty($pk))
                {
                    //array doesn't work with activerecord?
                    if (count($pk == 1))
                    {
                        $vals = array_values($pk);
                        $pk = $vals[0];
                    }

                    $model->deleteByPk($pk);
                }

        return true;
    }

    /**
     * Validates submitted formdata
     * If a record is not valid, the invalid model will be set to $model
     * to display error summary
     *
     * @param mixed $model
     * @param array $validatedItems returns the array of validated records
     * @param array $deleteItems returns the array of model for deleting
     * @param array $masterValues attributes to assign before saving
     * @param array $formData (default = $_POST)
     * @return boolean
     */
    public static function validate($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null)
    {
        $widget = new MultiModelForm;
        $widget->model = $model;

        $widget->checkModel();

        if (!$widget->initItems($validatedItems, $deleteItems, $masterValues, $formData))
            return false; //at least one item is not valid
        else
            return true;
    }

    /**
     * Converts the submitted formdata into an array of model
     *
     * @param array $formData the postdata $_POST submitted by the form
     * @param array $validatedItems the items which were validated
     * @param array $deleteItems the items to delete
     * @param array $masterValues assign additional masterdata before save
     * @return array array of model
     */
    public function initItems(&$validatedItems, &$deleteItems, $masterValues = array(), $formData = null)
    {
        if (!isset($formData))
            $formData = $_POST;

        $result = true;
        $newItems = array();

        $validatedItems = array(); //bugfix: 1.0.2
        $deleteItems = array();

        $modelClass = get_class($this->model);

        if (!isset($formData) || empty($formData[$modelClass]))
            return true;

        //----------- NEW (on validation error) -----------

        if (isset($formData[$modelClass]['n__']))
        {
            foreach ($formData[$modelClass]['n__'] as $idx => $attributes)
            {
                $model = new $modelClass;
                $model->attributes = $attributes;

                if (!empty($masterValues))
                    $model->setAttributes($masterValues, false); //assign mastervalues

                // validate
                if (!$model->validate())
                    $result = false;

                $validatedItems[] = $model;
            }

            unset($formData[$modelClass]['n__']);
        }

        //----------- UPDATE -----------

        $allExistingPk = isset($formData[$modelClass]['pk__']) ? $formData[$modelClass]['pk__'] : null; //bugfix: 1.0.1

        if (isset($formData[$modelClass]['u__']))
        {
            foreach ($formData[$modelClass]['u__'] as $idx => $attributes)
            {
                $model = new $modelClass('update');

                //should work for CModel, mongodb models... too
                if (method_exists($model, 'setIsNewRecord'))
                    $model->setIsNewRecord(false);

                $model->attributes = $attributes;

                if (!empty($masterValues))
                    $model->setAttributes($masterValues, false); //assign mastervalues

                //ensure to assign primary keys (when pk is unsafe or not defined in rules)
                if (is_array($allExistingPk))
                {
                    $primaryKeys = $allExistingPk[$idx];
                    $model->setAttributes($primaryKeys, false);
                }

                // validate
                if (!$model->validate())
                    $result = false;

                $validatedItems[] = $model;

                // remove from $allExistingPk
                if (is_array($allExistingPk))
                    unset($allExistingPk[$idx]);
            }

            unset($formData[$modelClass]['u__']);
        }

        //----------- DELETE -----------

        // add remaining primarykeys to $deleteItems (reindex)
        if (is_array($allExistingPk))
            foreach ($allExistingPk as $idx => $delPks)
                $deleteItems[] = $delPks;

        // remove handled formdata pk__
        unset($formData[$modelClass]['pk__']);

        //----------- Check for cloned elements by jQuery -----------
        if(!empty($formData[$modelClass])) //has cloned elements
        {
            // use the first item as reference
            $refAttribute = key($formData[$modelClass]);
            $refArray = array_shift($formData[$modelClass]);

            if (!empty($refArray))
                foreach ($refArray as $idx => $value)
                {
                    // check continue if all values are empty
                    if (empty($value))
                    {
                        $allEmpty = true;
                        foreach ($formData[$modelClass] as $attrKey => $values)
                        {
                            if (is_array($values[$idx])) //bugfix v2.1.1 have to check empty array items too
                            {
                                $isEmpty = true;
                                foreach ($formData[$modelClass][$attrKey] as $item)
                                {
                                    if (!empty($item[$idx]))
                                    {
                                        $isEmpty = false;
                                        break;
                                    }
                                }
                            }
                            else
                                $isEmpty = empty($values[$idx]);

                            $allEmpty = $isEmpty && $allEmpty;
                            if (!$allEmpty)
                                break;
                        }

                        if ($allEmpty)
                            continue;
                    }

                    $model = new $modelClass;
                    $model->$refAttribute = $value;

                    foreach ($formData[$modelClass] as $attrKey => $values)
                    {
                        //v2.2 support for checkboxlist / radiolist
                        if (is_array($values[$idx]))
                        {
                            $arrayAttribute = array();
                            foreach ($formData[$modelClass][$attrKey] as $item)
                            {
                                if (!empty($item[$idx]))
                                    $arrayAttribute[] = $item[$idx];
                            }

                            $model->$attrKey = $arrayAttribute;
                        }
                        else
                            $model->$attrKey = $values[$idx];
                    }

                    //assign mastervalues without checking rules for new records
                    $model->setAttributes($masterValues, false);

                    // validate
                    if (!$model->validate())
                        $result = false;

                    $validatedItems[] = $model;
                }
        }

        return $result;
    }

    /**
     * Get the primary key as array (key => value)
     *
     * @param CModel $model
     * @return array
     */
    public function getPrimaryKey($model)
    {
        $result = array();

        if ($model instanceof CActiveRecord)
        {
            $pkValue = $model->primaryKey;
            if (!empty($pkValue))
            {
                $pkName = $model->primaryKey();
                if (empty($pkName))
                    $pkName = $model->tableSchema->primaryKey;

                $result = array($pkName => $pkValue);
            }
        }
        else // when working with EMongoDocument
            if (method_exists($model, 'primaryKey'))
            {
                $pkName = $model->primaryKey();
                $pkValue = $model->$pkName;
                if (empty($pkValue))
                    $result = array($pkName => $pkValue);
            }

        return $result;
    }


    /**
     * Get the copyClass
     *
     * @return string
     */
    public function getCopyClass()
    {
        if (isset($this->options['copyClass']))
            return $this->options['copyClass'];
        else
        {
            $selector = $this->id . '_copy';
            $this->options['copyClass'] = $selector;
            return $selector;
        }
    }

    /**
     * The link for removing a fieldset
     *
     * @return string
     */
    public function getRemoveLink()
    {
        if (empty($this->removeText))
            return '';

        $onClick = 'jQuery(this).parent().parent().remove(); return false;';

        if (!empty($this->removeConfirm))
            $onClick = "if(confirm('{$this->removeConfirm}')) " . $onClick;

        $htmlOptions = array_merge($this->removeHtmlOptions, array('onclick' => $onClick));
        $link = CHtml::link($this->removeText, '#', $htmlOptions);

        return CHtml::tag($this->removeLinkWrapper['tag'],
            $this->removeLinkWrapper['htmlOptions'], $link);
    }

    /**
     * Check if rows has to be sortable
     * Works only if not is as tableView because the submitted $_POST data are not in the correct sorted order
     * Sorting in tableView needs more investigation/workaround ...
     *
     * @return bool
     */
    public function isSortable()
    {
        return !empty($this->sortAttribute) && !$this->tableView;
    }

    /**
     * Initialize the widget: register scripts
     */
    public function init()
    {
        if ($this->tableView)
        {
            $this->fieldsetWrapper = array('tag' => 'tr', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'row'));
            $this->rowWrapper = array('tag' => 'td', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'cell'));
            $this->removeLinkWrapper = $this->rowWrapper;
        }

        $this->checkModel();
        $this->registerClientScript();
        parent::init();
    }


    /**
     * Check the model instance on init / after create
     * Add all model attributes as hidden and visible=false if they are not part of the formConfig
     * Need this because on update all attributes have to be submitted, no 'loadModel' is called
     */
    protected function checkModel()
    {
        if (is_string($this->model))
            $this->model = new $this->model;

        if (isset($this->model) && isset($this->formConfig))
        {
            // add undefined attributes in the form config as hidden fields and attribute visible = false
            if (isset($this->formConfig) && !empty($this->formConfig['elements']))
                foreach ($this->model->attributes as $attribute => $value)
                {
                    if (!array_key_exists($attribute, $this->formConfig['elements']))
                        $this->formConfig['elements'][$attribute] = array('type' => 'hidden', 'visible' => false);
                }
        }
    }


    /**
     * @return array the javascript options
     */
    protected function getClientOptions()
    {
        if (empty($this->options))
            $this->options = array();

        if (!empty($this->removeText))
        {
            $append = $this->getRemoveLink();
            $this->options['append'] = empty($this->options['append']) ? $append : $append . ' ' . $this->options['append'];
        }

        if (!empty($this->jsBeforeClone))
            $this->options['beforeClone'] = $this->jsBeforeClone;

        if (!empty($this->jsAfterClone))
            $this->options['afterClone'] = $this->jsAfterClone;

        if (!empty($this->jsBeforeNewId))
            $this->options['beforeNewId'] = $this->jsBeforeNewId;

        if (!empty($this->jsAfterNewId))
            $this->options['afterNewId'] = $this->jsAfterNewId;

        return CJavaScript::encode($this->options);

    }

    /**
     * The id selector for jQuery.sortable
     *
     * @return string
     */
    protected function getSortSelectorId()
    {
        return self::CLASSPREFIX . 'sortable';
    }

    /**
     * Registers the relcopy javascript file.
     */
    public function registerClientScript()
    {
        $cs = Yii::app()->getClientScript();

        $this->_assets = Yii::app()->assetManager->publish(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets');

        $cs->registerCoreScript('jquery');
        $cs->registerScriptFile($this->_assets . '/js/jquery.relcopy.yii.1.0.js');

        $options = $this->getClientOptions();
        $cs->registerScript(__CLASS__ . '#' . $this->id, "jQuery('#{$this->id}').relCopy($options);");

        //add the script for jQuery.sortable
        if ($this->isSortable())
        {
            $cs->registerCoreScript('jquery.ui');
            $cssFile = $cs->getCoreScriptUrl() . '/jui/css/base/jquery-ui.css';
            $cs->registerCssFile($cssFile);

            $options = CJavaScript::encode($this->sortOptions);
            Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id . 'Sortable', "jQuery('#{$this->getSortSelectorId()}').sortable({$options});");
        }
    }

    /**
     * Render the top of the table: AddLink, Table header
     */
    public function renderTableBegin($renderAddLink)
    {
        $form = new MultiModelRenderForm($this->formConfig, $this->model);
        $form->parentWidget = $this;

        //add link as div
        if ($renderAddLink)
        {
            $addLink = $form->getAddLink();
            echo CHtml::tag('div', array('class' => self::CLASSPREFIX . 'addlink'), $addLink);
        }

        $tableHtmlOptions = array_merge(array('class' => self::CLASSPREFIX . 'table'), $this->tableHtmlOptions);

        //table
        echo CHtml::tag('table', $tableHtmlOptions, false, false);

        //thead
        $form->renderTableHeader();

        //tfoot
        if (!empty($this->tableFootCells))
        {
            $cells = '';
            foreach ($this->tableFootCells as $cell)
            {
                $cells .= CHtml::tag('td', array('class' => self::CLASSPREFIX . 'cell'), $cell);
            }

            $cells = CHtml::tag('tr', array('class' => self::CLASSPREFIX . 'row'), $cells);
            echo CHtml::tag('tfoot', array(), $cells);
        }

        //tbody
        $tbodyOptions = $this->isSortable() ? array('id' => $this->getSortSelectorId()) : array();
        echo CHtml::tag('tbody', $tbodyOptions, false, false);
    }


    /**
     * Renders the active form if a model and formConfig is set
     * $this->data is array of model
     */
    public function run()
    {
        if (empty($this->model) || empty($this->formConfig))
            return;

        //form is displayed again with some invalid models
        $isErrorMode = !empty($this->validatedItems);
        $showAddLink = !$isErrorMode || ($isErrorMode && $this->showAddItemOnError);

        $this->formConfig['activeForm'] = array('class' => 'MultiModelEmbeddedForm');

        $idx = 0;
        $errorPk = null;


        if ($isErrorMode)
        {
            if ($this->showErrorSummary)
                echo CHtml::errorSummary($this->validatedItems);

            $data = $this->validatedItems;
        }
        else
            $data = $this->data; //from the db


        if ($this->tableView)
            $this->renderTableBegin(false); //RODAX $showAddLink);

        if ($this->isSortable())
        {
            //render the name of the sortAttribute as hidden input
            //used in MultiModelForm::save
            echo CHtml::hiddenField(self::CLASSPREFIX . 'sortAttribute', $this->sortAttribute);

            if (!$this->tableView)
                echo CHtml::openTag('div', array('id' => $this->getSortSelectorId()));
        }

        // existing records
        if (is_array($data) && !empty($data))
        {
            foreach ($data as $model)
            {
                $form = new MultiModelRenderForm($this->formConfig, $model);
                $form->index = $idx;
                $form->parentWidget = $this;

                $form->primaryKey = $this->getPrimaryKey($model);

                if (!$this->tableView)
                {
                    if ($showAddLink && $idx == 0) // no existing data rendered
                        echo $form->renderAddLink();
                }

                // render pk outside of removeable tag, for checking records to delete
                // see method initItems()
                echo $form->renderHiddenPk('[pk__]');
                echo $form->render();

                $idx++;
            }
        }

        //if form is displayed first time or in errormode and want to show 'Add item' (and a 'CopyTemplate')
        if($showAddLink)
        {
            // add an empty fieldset as CopyTemplate
            $form = new MultiModelRenderForm($this->formConfig, $this->model);
            $form->index = $idx;
            $form->parentWidget = $this;
            $form->isCopyTemplate = true;

            if (!$this->tableView)
            {
                if ($idx == 0) // no existing data rendered
                    echo $form->renderAddLink();
            }

            echo $form->render();
        }

        if ($this->tableView)
        {
            echo CHtml::closeTag('tbody');
            echo CHtml::closeTag('table');
        }
        elseif ($this->isSortable())
            echo CHtml::closeTag('div');
    }
}

/**
 * The CForm to render the input form
 */
class MultiModelRenderForm extends CForm
{
    public $parentWidget;
    public $index;
    public $isCopyTemplate;
    public $primaryKey;


    /**
     * Wraps a content with row wrapper
     *
     * @param string $content
     * @return string
     */
    protected function getWrappedRow($content)
    {
        return CHtml::tag($this->parentWidget->rowWrapper['tag'],
            $this->parentWidget->rowWrapper['htmlOptions'], $content);
    }

    /**
     * Wraps a content with fieldset wrapper
     *
     * @param string $content
     * @return string
     */
    protected function getWrappedFieldset($content)
    {
        return CHtml::tag($this->parentWidget->fieldsetWrapper['tag'],
            $this->parentWidget->fieldsetWrapper['htmlOptions'], $content);
    }

    /**
     * Returns the generated label from Yii form builder
     * Needs to be replaced by the real attributeLabel
     * @see method  renderFormElements()
     *
     * @param string $prefix
     * @param string $attributeName
     * @return string
     */
    protected function getAutoCreatedLabel($prefix, $attributeName)
    {
        return ($this->model->generateAttributeLabel('[' . $prefix . '][' . $this->index . ']' . $attributeName));
    }

    /**
     * Renders the table head
     *
     * @return string
     */
    public function renderTableHeader()
    {
        $cells = '';

        foreach ($this->getElements() as $element)
            if ($element->visible)
            {
                $text = empty($element->label) ? '&nbsp;' : $element->label;
                $options = array();

                if ($element->getRequired())
                {
                    $options = array('class' => CHtml::$requiredCss);
                    $text .= CHtml::$afterRequiredLabel;
                }

                $cells .= CHtml::tag('th', $options, $text);
            }

        //add an empty column instead of remove link
        $cells .= CHtml::tag('th', array(), '&nbsp');

        $row = $this->getWrappedFieldset($cells);
        echo CHtml::tag('thead', array(), $cells);
    }


    /**
     * Check if elem is a array type
     *
     * @param string $type
     * @return boolean
     */
    protected function isElementArrayType($type)
    {
        switch ($type)
        {
            case 'checkboxlist':
            case 'radiolist':
                return true;
            default:
                return false;
        } // switch
    }

    /**
     * Renders a single form element
     * Remove the '[]' from the label
     *
     * @return string
     */
    protected function renderFormElements()
    {
        $output = '';

        $elements = $this->getElements();

        foreach ($elements as $element)
        {
            if (isset($element->name))
            {
                $elemName = $element->name;
                $elemLabel = $element->renderLabel(); //get the correct/nice label before changing name
                $element->label = ''; //no label on $element->render()

                if ($this->isCopyTemplate) // new fieldset
                {
                    if ($element->visible)
                    {
                        //v.2.2 support for checkboxlist radiolist
                        //Array types have to be rendered as array in the CopyTemplate
                        $element->name = $this->isElementArrayType($element->type) ? $elemName . '[][]' : $elemName . '[]';

                        $elemOutput = ($element->type == 'hidden' || //bugfix: v2.1.1
                            $this->parentWidget->tableView) ? '' : $elemLabel;

                        $elemOutput .= $element->render();
                        //bugfix: v2.1 - don't render hidden inputs in table cell
                        $output .= $element->type == 'hidden' ? $elemOutput : $this->getWrappedRow($elemOutput);
                    }
                }
                elseif (!empty($this->primaryKey))
                { // existing fieldsets update

                    $prefix = 'u__';
                    $element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName;

                    if ($element->type == 'hidden')
                        $output .= $element->render();
                    else
                    {
                        $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel;                        
                        $elemOutput .= $element->render();
                        $output .= $this->getWrappedRow($elemOutput);
                    }
                }
                else
                { //in validation error mode: the new added items before
                    if ($element->visible)
                    {
                        $prefix = 'n__';
                        $element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName;

                        if ($element->type == 'hidden')
                            $output .= $element->render();
                        else
                        {
                            $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel;
                            $elemOutput .= $element->render();
                            $output .= $this->getWrappedRow($elemOutput);
                        }
                    }
                }
            }
        }

        if (!$this->isCopyTemplate)
            $output .= $this->parentWidget->getRemoveLink();

        return $output;
    }

    /**
     * Renders the primary key value as hidden field
     * Need determine which records to delete
     *
     * @param string $classSuffix
     * @return string
     */
    public function renderHiddenPk($classSuffix = '[pk__]')
    {
        foreach ($this->primaryKey as $key => $value)
        {
            $modelClass = get_class($this->parentWidget->model);
            $name = $modelClass . $classSuffix . '[' . $this->index . ']' . '[' . $key . ']';
            return CHtml::hiddenField($name, $value);
        }
    }

    /**
     * Get the add item link
     *
     * @return string
     */
    public function getAddLink()
    {
        return CHtml::tag('a',
            array('id' => $this->parentWidget->id,
                'href' => '#',
                'rel' => '.' . $this->parentWidget->getCopyClass()
            ),
            $this->parentWidget->addItemText
        );
    }

    /**
     * Renders the link 'Add' for cloning the DOM element
     *
     * @return string
     */
    public function renderAddLink()
    {
        return $this->getWrappedRow($this->getAddLink());
    }

    /**
     * Renders the CForm
     * Each fieldset is wrapped with the fieldsetWrapper
     *
     * @return string
     */
    public function render()
    {
        $elemOutput = $this->renderBegin();
        $elemOutput .= $this->renderFormElements();
        $elemOutput .= $this->renderEnd();
        // wrap $elemOutput
        $wrapperClass = $this->parentWidget->fieldsetWrapper['htmlOptions']['class'];

        if ($this->isCopyTemplate)
        {
            $class = empty($wrapperClass)
                ? $this->parentWidget->getCopyClass()
                : $wrapperClass . ' ' . $this->parentWidget->getCopyClass();
        }
        else
            $class = $wrapperClass;

        $this->parentWidget->fieldsetWrapper['htmlOptions']['class'] = $class;
        return $this->getWrappedFieldset($elemOutput);
    }
}

/**
 * MultiModelEmbeddedForm
 *
 * A CActiveForm with no output of the form begin and close tag
 * In Yii 1.1.6 the form end/close is the only output of the methods init() and run()
 * Needs review in upcoming releases
 *
 */
class MultiModelEmbeddedForm extends CActiveForm
{
    /**
     * Initializes the widget.
     * Don't render the open tag
     */
    public function init()
    {
        ob_start();
        parent::init();
        ob_get_clean();
    }

    /**
     * Runs the widget.
     * Don't render the close tag
     */
    public function run()
    {
        ob_start();
        parent::run();
        ob_get_clean();
    }
}