// Validation and canonicalization should be put here or in TourBuilder whenever possible.
( function () {
	const gt = mw.guidedTour;

	/**
	 * @class mw.guidedTour.StepBuilder
	 * @classdesc A builder for defining a step of a guided tour
	 *
	 * @constructor
	 * @description Constructs a StepBuilder
	 * @param {mw.guidedTour.Tour} tour Tour the corresponding Step belongs to
	 * @param {Object} stepSpec See {mw.guidedTour.TourBuilder#step} for details.
	 * @throws {mw.guidedTour.TourDefinitionError} On invalid step name
	 */
	function StepBuilder( tour, stepSpec ) {
		/**
		 * Tour the corresponding step belongs to
		 *
		 * @property {mw.guidedTour.Tour}
		 * @private
		 */
		this.tour = tour;

		if ( typeof stepSpec.name !== 'string' || /[.-]/.test( stepSpec.name ) ) {
			throw new gt.TourDefinitionError( '\'stepSpec.name\' must be a string, the step name, without the characters \'.\' and \'-\'.' );
		}

		if ( stepSpec.attachTo && stepSpec.position === undefined ) {
			throw new gt.TourDefinitionError( 'If you specify an \'attachTo\', you must also specify \'position\'; see TourBuilder.step' );
		}

		/**
		 * Step being built by this StepBuilder
		 *
		 * @property {mw.guidedTour.Step}
		 * @private
		 */
		this.step = new gt.Step( tour, stepSpec );
	}

	// TODO (mattflaschen, 2014-03-18): Tour-level listeners and jQuery listeners (at
	// both levels)
	/**
	 * Tell the step to listen for one or more mw.hook types
	 *
	 * @param {...string} hookNames hook names to listen for, with each as a
	 *   separate parameter
	 *
	 * @return {mw.guidedTour.StepBuilder}
	 * @chainable
	 */
	StepBuilder.prototype.listenForMwHooks = function () {
		let i;
		for ( i = 0; i < arguments.length; i++ ) {
			this.step.listenForMwHook( arguments[ i ] );
		}
		return this;
	};

	/**
	 * Canonicalizes and checks a step reference passed to the builder before
	 * it is transitioned to
	 *
	 * @private
	 *
	 * @param {string|mw.guidedTour.StepBuilder} rawStep Step to canoncialize
	 * @param {string} exceptionPrefix Prefix, used if an exception is thrown
	 * @param {string} direction The direction being transitioned in ("next"
	 * or "back"). Null when processing the result of another transition
	 *
	 * @return {mw.guidedTour.Step|null} Canonicalized step, or null if
	 * the step transitioned to nothing.
	 *
	 * @throws {mw.guidedTour.TourDefinitionError} If there is no step with this name,
	 *  or the StepBuilder is not part of the current tour
	 */
	StepBuilder.prototype.canonicalizeStep = function ( rawStep, exceptionPrefix, direction ) {
		let step;

		if ( typeof rawStep === 'string' ) {
			// Step name
			step = rawStep;
		} else {
			// StepBuilder
			step = rawStep.step;
		}

		try {
			// Ensures it's a Step (could be a step name)
			// and checks for validity.
			step = this.tour.getStep( step );
		} catch ( ex ) {
			throw new gt.TourDefinitionError( exceptionPrefix + ': ' + ex.message );
		}
		if ( this.tour.emitTransitionOnStep && direction ) {
			const transitionEvent = new gt.TransitionEvent();
			transitionEvent.type = gt.TransitionEvent.BUILTIN;
			if ( direction === 'next' ) {
				transitionEvent.subtype = gt.TransitionEvent.TRANSITION_NEXT;
			} else {
				transitionEvent.subtype = gt.TransitionEvent.TRANSITION_BACK;
			}
			step = step.checkTransition( transitionEvent );
		}

		return step;
	};

	/**
	 * Tell the step how to determine the next step.
	 * Calling 'next' in the tour definition now automatically creates a next button
	 * if one isn't specified already.
	 *
	 * Invalid values or return values from the callback (a step name that does not
	 * refer to a valid step, a StepBuilder that is not part of the same tour) will
	 * cause an mw.guidedTour.TourDefinitionError exception to be thrown when the
	 * step is requested.
	 *
	 * @param {mw.guidedTour.StepBuilder|string|Function} nextValue Value used to
	 *  determine the next step.  Either:
	 *
	 *  - a mw.guidedTour.StepBuilder; the corresponding step is always next; this must
	 *   belong to the same tour
	 *  - a step name as string; the corresponding step is always next
	 *  - a Function that returns one of the above; this allows the next step to vary
	 *    dynamically
	 *
	 * @chainable
	 * @return {mw.guidedTour.StepBuilder}
	 * @throws {mw.guidedTour.TourDefinitionError} If this direction callback has has already
	 *  been set
	 * @memberof mw.guidedTour.StepBuilder
	 * @method next
	 */
	StepBuilder.prototype.next = function ( nextValue ) {
		return this.setDirectionCallback( 'next', nextValue );
	};

	/**
	 * Tell the step how to determine the back step
	 * Calling 'back' in the tour definition now automatically creates a back button
	 * if one isn't specified already.
	 *
	 * Invalid values or return values from the callback (a step name that does not
	 * refer to a valid step, a StepBuilder that is not part of the same tour) will
	 * cause an mw.guidedTour.TourDefinitionError exception to be thrown when the
	 * step is requested.
	 *
	 * @param {mw.guidedTour.StepBuilder|string|Function} backValue Value used to
	 *  determine the back step.  Either:
	 *
	 *  - a mw.guidedTour.StepBuilder; the corresponding step is always back; this must
	 *   belong to the same tour
	 *  - a step name as string; the corresponding step is always back
	 *  - a Function that returns one of the above; this allows the back step to vary
	 *    dynamically
	 *
	 * @chainable
	 * @return {mw.guidedTour.StepBuilder}
	 * @throws {mw.guidedTour.TourDefinitionError} If this direction callback has has already
	 *  been set
	 * @memberof mw.guidedTour.StepBuilder
	 * @method back
	 */
	StepBuilder.prototype.back = function ( backValue ) {
		return this.setDirectionCallback( 'back', backValue );
	};

	/**
	 * Set the callback of the step direction.
	 *
	 * Invalid values or return values from the callback (a step name that does not
	 * refer to a valid step, a StepBuilder that is not part of the same tour) will
	 * cause an mw.guidedTour.TourDefinitionError exception to be thrown when the
	 * step is requested.
	 *
	 * @private
	 * @param {string} direction Name of direction. Currently 'next' and 'back' are supported.
	 * @param {mw.guidedTour.StepBuilder|string|Function} step Value used to
	 *  determine the step callback for the specified direction.  Either:
	 *
	 *  - a mw.guidedTour.StepBuilder;  this must belong to the same tour
	 *  - a step name as string;
	 *  - a Function that returns one of the above; this allows the step callback to vary
	 *    dynamically
	 *
	 * @chainable
	 * @return {mw.guidedTour.StepBuilder}
	 * @throws {mw.guidedTour.TourDefinitionError} If this direction callback has has already
	 *  been set
	 */
	StepBuilder.prototype.setDirectionCallback = function ( direction, step ) {
		const currentStep = this.step;

		if ( currentStep.hasCallback( direction ) ) {
			throw new gt.TourDefinitionError( '.' + direction + '() can not be called more than once per StepBuilder' );
		}

		let callback;
		if ( typeof step === 'function' ) {
			callback = () => {
				const directionReturn = step();
				return this.canonicalizeStep(
					directionReturn,
					'Callback passed to .' + direction + '() returned invalid value',
					direction,
				);
			};
		} else {
			// This allows for forward references (passing the name of a step
			// that isn't built yet) in the tour script.  Validation is done
			// when the step change is requested.
			callback = () => this.canonicalizeStep(
				step,
				'Value passed to .' + direction + '() does not refer to a valid step',
				direction,
			);
		}
		currentStep.setCallback( direction, callback );
		return this;
	};

	// TODO (mattflaschen, 2014-03-14): Extend to allow tour to transition when step does
	// nothing, and to support jQuery events (either just with DOM selectors, or also
	// with plain objects).
	/**
	 * Tell the step what to do at possible transition points, such as when hooks and events
	 * that are being listened to fire.
	 *
	 * The passed in callback is called to check whether the tour should move to a new
	 * step.  The callback can return the step to move to, as a
	 * mw.guidedTour.StepBuilder or a step name.  It may also return two special values:
	 *
	 * - gt.TransitionAction.HIDE - Hides the tour, but keeps the stored user state
	 * - gt.TransitionAction.END - Ends the tour, clearing the user state
	 *
	 * The callback may also return nothing. In that case, it will not transition.
	 *
	 * Invalid return values from the callback (a step name that does not refer to a
	 * valid step, a StepBuilder that is not part of the same tour, or a number that
	 * is not a valid TransitionAction) will cause a mw.guidedTour.TourDefinitionError
	 * exception to be thrown when the tour checks to see if it should transition.
	 *
	 * @param {Function} callback Callback called to determine whether to transition, and if
	 *  so what to do (either move to another step or do a TransitionAction)
	 * @param {mw.guidedTour.TransitionEvent} callback.transitionEvent Event that triggered the
	 *  check; see mw.guidedTour.TransitionEvent for fields
	 * @param {mw.guidedTour.StepBuilder|mw.guidedTour.TransitionAction|string} callback.return
	 *  Step to move to, as StepBuilder or step name, a mw.guidedTour.TransitionAction for a
	 *  special action, or falsy for no requested transition (see above).
	 *
	 * @chainable
	 * @return {mw.guidedTour.StepBuilder}
	 * @throws {mw.guidedTour.TourDefinitionError} If StepBuilder.transition() has already
	 *  been called, or callback is not a function
	 * @memberof mw.guidedTour.StepBuilder
	 * @method transition
	 */
	StepBuilder.prototype.transition = function ( callback ) {
		const currentStep = this.step;

		if ( currentStep.hasCallback( 'transition' ) ) {
			throw new gt.TourDefinitionError( '.transition() can not be called more than once per StepBuilder' );
		}

		// next and transition have different signatures, so try to catch some issues up front
		if ( typeof callback !== 'function' ) {
			throw new gt.TourDefinitionError( '.transition() takes one argument, a function' );
		}

		currentStep.setCallback( 'transition', ( transitionEvent ) => {
			const transitionReturn = callback( transitionEvent );

			if ( typeof transitionReturn === 'number' ) {
				if (
					transitionReturn !== gt.TransitionAction.HIDE &&
					transitionReturn !== gt.TransitionAction.END
				) {
					throw new gt.TourDefinitionError( 'Callback passed to .transition() returned a number that is not a valid TransitionAction' );
				}
				return transitionReturn;
			} else if ( !transitionReturn ) {
				// Mainly intended for not doing anything (implicitly
				// returning undefined), which means 'don't transition'.
				// Same behavior for any falsy value.
				return currentStep;
			} else {
				return this.canonicalizeStep(
					transitionReturn,
					'Callback passed to .transition() returned invalid value',
					null,
				);
			}
		} );
		return this;
	};

	mw.guidedTour.StepBuilder = StepBuilder;
}() );