/**
 * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module engine/dev-utils/operationreplayer
 */

/* global setTimeout */

import OperationFactory from '../model/operation/operationfactory';

/**
 * Operation replayer is a development tool created for easy replaying of operations on the document from stringified operations.
 */
export default class OperationReplayer {
	/**
	 * @param {module:engine/model/model~Model} model Data model.
	 * @param {String} logSeparator Separator between operations.
	 * @param {String} stringifiedOperations Operations to replay.
	 */
	constructor( model, logSeparator, stringifiedOperations ) {
		this._model = model;
		this._logSeparator = logSeparator;
		this.setStringifiedOperations( stringifiedOperations );
	}

	/**
	 * Parses the given string containing stringified operations and sets parsed operations as operations to replay.
	 *
	 * @param {String} stringifiedOperations Stringified operations to replay.
	 */
	setStringifiedOperations( stringifiedOperations ) {
		if ( stringifiedOperations === '' ) {
			this._operationsToReplay = [];

			return;
		}

		this._operationsToReplay = stringifiedOperations
			.split( this._logSeparator )
			.map( stringifiedOperation => JSON.parse( stringifiedOperation ) );
	}

	/**
	 * Returns operations to replay.
	 *
	 * @returns {Array.<module:engine/model/operation/operation~Operation>}
	 */
	getOperationsToReplay() {
		return this._operationsToReplay;
	}

	/**
	 * Applies all operations with a delay between actions.
	 *
	 * @param {Number} timeInterval Time between applying operations.
	 * @returns {Promise}
	 */
	play( timeInterval = 1000 ) {
		const operationReplayer = this; // eslint-disable-line consistent-this

		return new Promise( ( res, rej ) => {
			play();

			function play() {
				operationReplayer.applyNextOperation().then( isFinished => {
					if ( isFinished ) {
						return res();
					}

					setTimeout( play, timeInterval );
				} ).catch( err => {
					rej( err );
				} );
			}
		} );
	}

	/**
	 * Applies `numberOfOperations` operations, beginning after the last applied operation (or first, if no operations were applied).
	 *
	 * @param {Number} numberOfOperations The number of operations to apply.
	 * @returns {Promise}
	 */
	applyOperations( numberOfOperations ) {
		if ( numberOfOperations <= 0 ) {
			return;
		}

		return this.applyNextOperation()
			.then( isFinished => {
				if ( !isFinished ) {
					return this.applyOperations( numberOfOperations - 1 );
				}
			} );
	}

	/**
	 * Applies all operations to replay at once.
	 *
	 * @returns {Promise}
	 */
	applyAllOperations() {
		return this.applyNextOperation()
			.then( isFinished => {
				if ( !isFinished ) {
					return this.applyAllOperations();
				}
			} );
	}

	/**
	 * Applies the next operation to replay. Returns a promise with the `isFinished` parameter that is `true` if the last
	 * operation in the replayer has been applied, `false` otherwise.
	 *
	 * @returns {Promise.<Boolean>}
	 */
	applyNextOperation() {
		const model = this._model;

		return new Promise( res => {
			model.enqueueChange( writer => {
				const operationJson = this._operationsToReplay.shift();

				if ( !operationJson ) {
					return res( true );
				}

				const operation = OperationFactory.fromJSON( operationJson, model.document );

				writer.batch.addOperation( operation );
				model.applyOperation( operation );

				res( false );
			} );
		} );
	}
}
