/**
 * ExtJS Wizard Component
 * Copyright (C) 2008 Hans-Peter Oeri <hp@oeri.ch>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * ----------------------------------------------------------------------
 *
 * @author Hans-Peter Oeri <hp@oeri.ch>
 * @url https://saintcyr.oeri.ch/trac/ext-ux/
 *
 * Prior work by
 * @author Thorsten Suckow-Homberg <ts@siteartwork.de>
 * @url http://www.siteartwork.de/wizardcomponent
 */

Ext.namespace('Ext.ux');

/**
 * @class Ext.ux.WizardPanel
 * @extends Ext.Panel
 *
 * A basic wizard widget; basically a panel that guides the user through a series
 * of sub-panels or "cards". The wizard contains the cards, the control buttons
 * and an information panel (showing the current step).
 *
 * @constructor
 * @param {Object} config The config object
 */
Ext.ux.WizardPanel = Ext.extend( Ext.Panel, {

    /**
	 * @cfg {Number} height The height of the dialog. Defaults to "400".
	 */
    height: 400,

    /**
	 * @cfg {Number} width The width of the dialog. Defaults to "540".
	 */
    width: 540,

    /**
     * @cfg {Boolean} broadFinish Finish the wizard whenever an action would
     * call a card "out of range"; if set to false, only an explicit call to
     * {@link Ext.ux.WizardPanel.doFinish} finishes the wizard. Defaults to true.
     */
    broadFinish: true,

    /**
     * @cfg {Object} wizButtons Named array of {@link Ext.Action} configs that
     * are included in the wizard. The array index will be set as the 'wizCommand'
     * property. Certain properties may be overriden per wizard card, see
     * {@link wizStaticProperties}.
     */
    wizButtons: {
		 cancel: {
			 text: 'Cancel'
		}
		,back: {
			 text: '<<< Previous'
		}
		,forward: {
			 text: 'Next >>>'
		}
		,finish: {
			 text: 'Finish'
		}
	},

	/**
	 * @cfg {Array} wizButtonsLayout Arrangement of buttons within {@link wizButtonsTarget},
	 * by listing the button names of {@link wizButtons}. If the target is a
	 * toolbar, certain special values might be included.
	 */
	wizButtonsLayout: [
		 'cancel'
		,'back'
		,'->'
		,'forward'
		,'finish'
	],


	/**
	 * @cfg {String} wizButtonsTarget Panel element to add wizard buttons to.
	 * Might be one of the toolbars or 'buttons'.
	 */
	wizButtonsTarget: 'bbar',

	/**
	 * @cfg {Array} wizValidityButtons Array of buttons whose disabled state
	 * depends on the validity of the current card's form. This property might
	 * also be set per card.
	 */
	wizValidityButtons: [ 'forward' ],

	/**
	 * @cfg {Object} wizStaticProperties Named array of property => method that
	 * may be overridden per card. Include a {@link wizButtons} object in the
	 * card and include it in {@link defButtonProperties}.
	 */
	wizStaticProperties: {
		hidden: 'setHidden',
		disabled: 'setDisabled'
	},

	/**
	 * @cfg {Array} cards An array of the cards to show in order. Might be configured
	 * with index or id. Changes with {@link setCards}.
	 */
	cards: null,

	/**
	 * @cfg {Object} infoPanel config object or {@link Ext.BoxComponent} to use
	 * as information panel. Should attach itself to the wizard's change event.
	 */
	infoPanel: { xtype: 'ux.WizardPanel.Info' },

	/**
	 * @cfg {Object} cardsPanel The card panel that holds the various wizard cards.
	 */
	cardsPanel: {
		border: false
	},

	/**
	 * @cfg {Number} activeIndex The card to display/displayed.
	 */
	activeIndex: 0,

    /**
     * Inits this component with the specified config-properties and automatically
     * creates its components.
     */
	initComponent: function() {
		// create button/action objects
		for (var butind in this.wizButtons) {
			Ext.applyIf( this.wizButtons[butind], {
				wizCommand: butind
				,handler: this.buttonHandler
				,scope: this
			} );

			// replace config with the respective Action object
			var a = new Ext.Action( this.wizButtons[butind] );
			this.wizButtons[butind] = a;
			delete a;
		}

		// fill in the layout array
		var specials = /^( |-|->)$/;
		for (var ind in this.wizButtonsLayout) {
			if (!specials.test(this.wizButtonsLayout[ind])) {
				this.wizButtonsLayout[ind] = this.wizButtons[this.wizButtonsLayout[ind]];
			}
		}
		this[this.wizButtonsTarget] = this.wizButtonsLayout;

		// sub-panels

		// recover the wizard cards (they become items of the cardPanel)
        var cards = this.items;
        delete this.items;

        Ext.apply( this.cardsPanel, {
        	xtype: 'ux.CardPanel'
        	,region: 'center'
        	,activeItem: this.activeIndex
            ,items: cards
        });

        this.items = [ this.cardsPanel, this.infoPanel ];

	    Ext.apply(this, {
	        layout: 'border',
		    items : [
		        this.cardsPanel,
		        this.infoPanel
		    ]
	    });

		Ext.ux.WizardPanel.superclass.initComponent.call(this);

	    this.addEvents(
            /**
             * @event cancel
             * Fires after the wizard has been cancelled
             * @param {Ext.ux.WizardPanel} this
             */
             'cancel'

            /**
             * @event finish
             * Fires after the wizard has been finished
             * @param {Ext.ux.WizardPanel} this
             */
            ,'finish'

            /**
             * @event beforechange
             * Fires before the wizard changes page. Return false to cancel.
             * @param {Ext.ux.WizardPanel} this
             * @param {Integer} newindex
             * @param {Ext.Panel} oldcard
             */
            ,'beforechange'

            /**
             * @event change
             * Fires after page has changed
             * @param {Ext.ux.WizardPanel} this
             * @param {Integer} newindex
             */
            ,'change'
	    );

		delete this.cardsPanel;
		this.cardsPanel = this.items.itemAt(0);
		delete this.infoPanel;
		this.infoPanel = this.items.itemAt(1);

		if (Ext.isArray(this.cards)) {
			this.setCards( this.cards, this.activeIndex );
		}
		else {
			this.cards = this.cardsPanel.items.items;
		}
	},

	/**
	 * Render and set active item (attach istener)
	 * @param {Ext.Container} ct Container
	 * @param {Integer} pos Position within container
	 */
	render: function( ct, pos ) {
		Ext.ux.WizardPanel.superclass.render.call(this, ct, pos);
		this.setActiveIndex( this.activeIndex );
	},

// -------- handlers

	/**
	 * handle button presses
	 * @param {Ext.Action} button
	 * @protected
	 */
	buttonHandler: function( button ) {
		switch (button.wizCommand) {
			case 'cancel':
				this.doCancel();
				break;
			case 'finish':
				this.doFinish();
				break;
			case 'back':
				this.goBack();
				break;
			case 'forward':
				this.goForward();
				break;

			default:
				throw 'Unknwon wizard command';
				break;
		}
	},

// -------- buttons

	/**
	 * Get the default value for a property of a given button. Standard is to
	 * hide 'back' for the first card, 'finish' on all but the last and 'forward'
	 * on the last. Only the properties listed in {@link wizButtonsProperties}
	 * are called.
	 * @param {String} prop Property to default
	 * @param {String} butind Name of the button
	 * @protected
	 */
	defButtonProperty: function( prop, butind ) {
		switch (prop) {
			case 'disabled':
				return false;
				break;

			case 'hidden':
				if (butind == 'back') {
					return (this.activeIndex == 0);
				}

				if (butind == 'forward') {
					return (this.activeIndex == this.cards.length-1);
				}

				if (butind == 'finish') {
					return (this.activeIndex < this.cards.length-1);
				}

				return false;
				break;
		}
	},

    /**
     * Dynamically override the disabled property of the buttons listed in
     * {@link wizValidityButtons}, depending on the current cards validity.
     * @param {Ext.FormPanel} card The card that triggered the event.
     * @param {Boolean} isValid "true", if the user input was valid, otherwise
     * "false"
     * @private
     */
	onValidation: function( card, isValid ) {
		var valbuts = card.wizValidityButtons || this.wizValidityButtons;
		for (var i=0, len=valbuts.length; i < len; i++) {
			this.wizButtons[valbuts[i]].setDisabled( !isValid );
		}
	},

// -------- helper

	/**
	 * Change the cards workflow ex post.
	 * @param {Array} cards Array of card indizes/ids to include in a workflow
	 * @param {Integer} active Card to select out of the new set
	 */
	setCards: function( cards, active ) {
		for (var i=0, len=cards.length; i < len; i++) {
			cards[i] = this.cardsPanel.getComponent(cards[i]);
		}

		delete this.cards;
		this.cards = cards;

		if (active === undefined) {
			active = 0;
		}
		this.setActiveIndex( active );
	},

	/**
	 * Change to the given card
	 * @param {Integer} index new card index
	 * @return {Boolean} success value
	 */
	setActiveIndex: function( index ) {
		if (!this.rendered) {
			this.activeIndex = index;
			return false;
		}

		// as this.cards might have changed since the last update,
		// only the *new* index is current. We cannot refer to the old index!
		var oldcard = this.cardsPanel.activeItem;
		var newcard = this.cards[index];

		// pre-checks
	    if (this.fireEvent( 'beforechange', this, index, oldcard ) === false) {
	    	return false;
	    }

		if (!this.cardsPanel.setActiveItem( this.cards[index] )) {
			return false;
		}

		// attach to card for dynamic button updates
		if (oldcard && oldcard.form) {
			oldcard.un( 'clientvalidation', this.onValidation, this );
			oldcard.stopMonitoring();
		}

		if (newcard.form) {
			newcard.on( 'clientvalidation', this.onValidation, this );
			newcard.startMonitoring();
		}

		this.activeIndex = index;

		// set static button properties
		for (var butind in this.wizButtons) {
			for( var prop in this.wizStaticProperties) {
				var isset;

				isset = newcard.wizButtons && newcard.wizButtons[butind] && newcard.wizButtons[butind][prop];
				if (isset === undefined) {
					isset = this.wizButtons[butind][prop];
					if (isset === undefined) {
						isset = this.defButtonProperty( prop, butind );
					}
				}

				if (isset !== undefined) {
					this.wizButtons[butind][this.wizStaticProperties[prop]]( isset );
				}
			}
		}

		this.fireEvent('change', this, index);

		return true;
	},

	/**
     * Returns the collected data of all cards (forms) within the wizard.
     * @return {Array}
     */
    getData: function() {
        var formValues = {};
		var cards = this.cards;
		for (var i = 0, len = cards.length; i < len; i++) {
		    if (cards[i].form) {
		        formValues[cards[i].id] = cards[i].form.getValues(false);
		    } else {
		        formValues[cards[i].id] = {};
		    }
		}

		return formValues;
    },

// ------- event commands

	/**
	 * Cancel operation. Fires the 'cancel'-event.
	 */
	doCancel: function()
	{
	    this.fireEvent( 'cancel', this );
	},

	/**
	 * Finish operation. Fires the 'finish'-event.
	 */
	doFinish: function()
	{
		this.fireEvent( 'finish', this );
	},

	/**
	 * Go back one step.
	 */
	goBack: function()
	{
		if (this.activeIndex > 0) {
			this.setActiveIndex( this.activeIndex - 1 );
		}
	},

	/**
	 * Go forward one step. Finishes the wizard
	 * if {@link broadFinish} is set.
	 */
	goForward: function()
	{
		if (this.broadFinish && this.activeIndex == this.cards.length-1) {
			this.doFinish();
		} else if (this.activeIndex < this.cards.length){
			this.setActiveIndex( this.activeIndex + 1 );
		}
	}
});

Ext.reg( 'ux.WizardPanel', Ext.ux.WizardPanel );



