/**
 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @fileOverview Handles the indentation of block elements.
 */

( function() {
	'use strict';

	var $listItem = CKEDITOR.dtd.$listItem,
		$list = CKEDITOR.dtd.$list,
		TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
		TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;

	CKEDITOR.plugins.add( 'indentblock', {
		requires: 'indent',
		init: function( editor ) {
			var globalHelpers = CKEDITOR.plugins.indent,
				classes = editor.config.indentClasses;

			// Register commands.
			globalHelpers.registerCommands( editor, {
				indentblock: new commandDefinition( editor, 'indentblock', true ),
				outdentblock: new commandDefinition( editor, 'outdentblock' )
			} );

			function commandDefinition() {
				globalHelpers.specificDefinition.apply( this, arguments );

				this.allowedContent = {
					'div h1 h2 h3 h4 h5 h6 ol p pre ul': {
						// Do not add elements, but only text-align style if element is validated by other rule.
						propertiesOnly: true,
						styles: !classes ? 'margin-left,margin-right' : null,
						classes: classes || null
					}
				};

				this.contentTransformations = [
					[ 'div: splitMarginShorthand' ],
					[ 'h1: splitMarginShorthand' ],
					[ 'h2: splitMarginShorthand' ],
					[ 'h3: splitMarginShorthand' ],
					[ 'h4: splitMarginShorthand' ],
					[ 'h5: splitMarginShorthand' ],
					[ 'h6: splitMarginShorthand' ],
					[ 'ol: splitMarginShorthand' ],
					[ 'p: splitMarginShorthand' ],
					[ 'pre: splitMarginShorthand' ],
					[ 'ul: splitMarginShorthand' ]
				];

				if ( this.enterBr )
					this.allowedContent.div = true;

				this.requiredContent = ( this.enterBr ? 'div' : 'p' ) +
					( classes ? '(' + classes.join( ',' ) + ')' : '{margin-left}' );

				this.jobs = {
					'20': {
						refresh: function( editor, path ) {
							var firstBlock = path.block || path.blockLimit;

							// Switch context from somewhere inside list item to list item,
							// if not found just assign self (doing nothing).
							if ( !firstBlock.is( $listItem ) ) {
								var ascendant = firstBlock.getAscendant( $listItem );

								firstBlock = ( ascendant && path.contains( ascendant ) ) || firstBlock;
							}

							// Switch context from list item to list
							// because indentblock can indent entire list
							// but not a single list element.

							if ( firstBlock.is( $listItem ) )
								firstBlock = firstBlock.getParent();

							//	[-] Context in the path or ENTER_BR
							//
							//		Don't try to indent if the element is out of
							//		this plugin's scope. This assertion is omitted
							//		if ENTER_BR is in use since there may be no block
							//		in the path.

							if ( !this.enterBr && !this.getContext( path ) )
								return TRISTATE_DISABLED;

							else if ( classes ) {

								//	[+] Context in the path or ENTER_BR
								//	[+] IndentClasses
								//
								//		If there are indentation classes, check if reached
								//		the highest level of indentation. If so, disable
								//		the command.

								if ( indentClassLeft.call( this, firstBlock, classes ) )
									return TRISTATE_OFF;
								else
									return TRISTATE_DISABLED;
							} else {

								//	[+] Context in the path or ENTER_BR
								//	[-] IndentClasses
								//	[+] Indenting
								//
								//		No indent-level limitations due to indent classes.
								//		Indent-like command can always be executed.

								if ( this.isIndent )
									return TRISTATE_OFF;

								//	[+] Context in the path or ENTER_BR
								//	[-] IndentClasses
								//	[-] Indenting
								//	[-] Block in the path
								//
								//		No block in path. There's no element to apply indentation
								//		so disable the command.

								else if ( !firstBlock )
									return TRISTATE_DISABLED;

								//	[+] Context in the path or ENTER_BR
								//	[-] IndentClasses
								//	[-] Indenting
								//	[+] Block in path.
								//
								//		Not using indentClasses but there is firstBlock.
								//		We can calculate current indentation level and
								//		try to increase/decrease it.

								else {
									return CKEDITOR[
										( getIndent( firstBlock ) || 0 ) <= 0 ? 'TRISTATE_DISABLED' : 'TRISTATE_OFF'
									];
								}
							}
						},

						exec: function( editor ) {
							var selection = editor.getSelection(),
								range = selection && selection.getRanges()[ 0 ],
								nearestListBlock;

							// If there's some list in the path, then it will be
							// a full-list indent by increasing or decreasing margin property.
							if ( ( nearestListBlock = editor.elementPath().contains( $list ) ) )
								indentElement.call( this, nearestListBlock, classes );

							// If no list in the path, use iterator to indent all the possible
							// paragraphs in the range, creating them if necessary.
							else {
								var iterator = range.createIterator(),
									enterMode = editor.config.enterMode,
									block;

								iterator.enforceRealBlocks = true;
								iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR;

								while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) {
									if ( !block.isReadOnly() )
										indentElement.call( this, block, classes );
								}
							}

							return true;
						}
					}
				};
			}

			CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, {
				// Elements that, if in an elementpath, will be handled by this
				// command. They restrict the scope of the plugin.
				context: { div: 1, dl: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, p: 1, pre: 1, table: 1 },

				// A regex built on config#indentClasses to detect whether an
				// element has some indentClass or not.
				classNameRegex: classes ? new RegExp( '(?:^|\\s+)(' + classes.join( '|' ) + ')(?=$|\\s)' ) : null
			} );
		}
	} );

	// Generic indentation procedure for indentation of any element
	// either with margin property or config#indentClass.
	function indentElement( element, classes, dir ) {
		if ( element.getCustomData( 'indent_processed' ) )
			return;

		var editor = this.editor,
			isIndent = this.isIndent;

		if ( classes ) {
			// Transform current class f to indent step index.
			var indentClass = element.$.className.match( this.classNameRegex ),
				indentStep = 0;

			if ( indentClass ) {
				indentClass = indentClass[ 1 ];
				indentStep = CKEDITOR.tools.indexOf( classes, indentClass ) + 1;
			}

			// Operate on indent step index, transform indent step index
			// back to class name.
			if ( ( indentStep += isIndent ? 1 : -1 ) < 0 )
				return;

			indentStep = Math.min( indentStep, classes.length );
			indentStep = Math.max( indentStep, 0 );
			element.$.className = CKEDITOR.tools.ltrim( element.$.className.replace( this.classNameRegex, '' ) );

			if ( indentStep > 0 )
				element.addClass( classes[ indentStep - 1 ] );
		} else {
			var indentCssProperty = getIndentCss( element, dir ),
				currentOffset = parseInt( element.getStyle( indentCssProperty ), 10 ),
				indentOffset = editor.config.indentOffset || 40;

			if ( isNaN( currentOffset ) )
				currentOffset = 0;

			currentOffset += ( isIndent ? 1 : -1 ) * indentOffset;

			if ( currentOffset < 0 )
				return;

			currentOffset = Math.max( currentOffset, 0 );
			currentOffset = Math.ceil( currentOffset / indentOffset ) * indentOffset;

			element.setStyle(
				indentCssProperty,
				currentOffset ? currentOffset + ( editor.config.indentUnit || 'px' ) : ''
			);

			if ( element.getAttribute( 'style' ) === '' )
				element.removeAttribute( 'style' );
		}

		CKEDITOR.dom.element.setMarker( this.database, element, 'indent_processed', 1 );

		return;
	}

	// Method that checks if current indentation level for an element
	// reached the limit determined by config#indentClasses.
	function indentClassLeft( node, classes ) {
		var indentClass = node.$.className.match( this.classNameRegex ),
			isIndent = this.isIndent;

		// If node has one of the indentClasses:
		//	* If it holds the topmost indentClass, then
		//	  no more classes have left.
		//	* If it holds any other indentClass, it can use the next one
		//	  or the previous one.
		//	* Outdent is always possible. We can remove indentClass.
		if ( indentClass )
			return isIndent ? indentClass[ 1 ] != classes.slice( -1 ) : true;

		// If node has no class which belongs to indentClasses,
		// then it is at 0-level. It can be indented but not outdented.
		else
			return isIndent;
	}

	// Determines indent CSS property for an element according to
	// what is the direction of such element. It can be either `margin-left`
	// or `margin-right`.
	function getIndentCss( element, dir ) {
		return ( dir || element.getComputedStyle( 'direction' ) ) == 'ltr' ? 'margin-left' : 'margin-right';
	}

	// Return the numerical indent value of margin-left|right of an element,
	// considering element's direction. If element has no margin specified,
	// NaN is returned.
	function getIndent( element ) {
		return parseInt( element.getStyle( getIndentCss( element ) ), 10 );
	}
} )();

/**
 * A list of classes to use for indenting the contents. If set to `null`, no classes will be used
 * and instead the {@link #indentUnit} and {@link #indentOffset} properties will be used.
 *
 *		// Use the 'Indent1', 'Indent2', 'Indent3' classes.
 *		config.indentClasses = ['Indent1', 'Indent2', 'Indent3'];
 *
 * @cfg {Array} [indentClasses=null]
 * @member CKEDITOR.config
 */

/**
 * The size in {@link CKEDITOR.config#indentUnit indentation units} of each indentation step.
 *
 *		config.indentOffset = 4;
 *
 * @cfg {Number} [indentOffset=40]
 * @member CKEDITOR.config
 */

/**
 * The unit used for {@link CKEDITOR.config#indentOffset indentation offset}.
 *
 *		config.indentUnit = 'em';
 *
 * @cfg {String} [indentUnit='px']
 * @member CKEDITOR.config
 */
