import { Subject } from '../Subject';
import { ObjectLiteral } from '../../common/ObjectLiteral';
import { ObjectUtils } from '../../util/ObjectUtils';

/**
 * Finds all cascade operations of the given subject and cascade operations of the found cascaded subjects,
 * e.g. builds a cascade tree and creates a subjects for them.
 */
export class CascadesSubjectBuilder {
	// ---------------------------------------------------------------------
	// Constructor
	// ---------------------------------------------------------------------

	constructor(protected allSubjects: Subject[]) {}

	// ---------------------------------------------------------------------
	// Public Methods
	// ---------------------------------------------------------------------

	/**
	 * Builds a cascade subjects tree and pushes them in into the given array of subjects.
	 */
	build(subject: Subject, operationType: 'save' | 'remove' | 'soft-remove' | 'recover') {
		subject.metadata
			.extractRelationValuesFromEntity(subject.entity!, subject.metadata.relations) // todo: we can create EntityMetadata.cascadeRelations
			.forEach(([relation, relationEntity, relationEntityMetadata]) => {
				// we need only defined values and insert, update, soft-remove or recover cascades of the relation should be set
				if (
					relationEntity === undefined ||
					relationEntity === null ||
					(!relation.isCascadeInsert &&
						!relation.isCascadeUpdate &&
						!relation.isCascadeSoftRemove &&
						!relation.isCascadeRecover)
				)
					return;

				// if relation entity is just a relation id set (for example post.tag = 1)
				// then we don't really need to check cascades since there is no object to insert or update
				if (!ObjectUtils.isObject(relationEntity)) return;

				// if we already has this entity in list of operated subjects then skip it to avoid recursion
				const alreadyExistRelationEntitySubject = this.findByPersistEntityLike(
					relationEntityMetadata.target,
					relationEntity,
				);
				if (alreadyExistRelationEntitySubject) {
					if (alreadyExistRelationEntitySubject.canBeInserted === false)
						// if its not marked for insertion yet
						alreadyExistRelationEntitySubject.canBeInserted =
							relation.isCascadeInsert === true && operationType === 'save';
					if (alreadyExistRelationEntitySubject.canBeUpdated === false)
						// if its not marked for update yet
						alreadyExistRelationEntitySubject.canBeUpdated =
							relation.isCascadeUpdate === true && operationType === 'save';
					if (alreadyExistRelationEntitySubject.canBeSoftRemoved === false)
						// if its not marked for removal yet
						alreadyExistRelationEntitySubject.canBeSoftRemoved =
							relation.isCascadeSoftRemove === true && operationType === 'soft-remove';
					if (alreadyExistRelationEntitySubject.canBeRecovered === false)
						// if its not marked for recovery yet
						alreadyExistRelationEntitySubject.canBeRecovered =
							relation.isCascadeRecover === true && operationType === 'recover';
					return;
				}

				// mark subject with what we can do with it
				// and add to the array of subjects to load only if there is no same entity there already
				const relationEntitySubject = new Subject({
					metadata: relationEntityMetadata,
					parentSubject: subject,
					entity: relationEntity,
					canBeInserted: relation.isCascadeInsert === true && operationType === 'save',
					canBeUpdated: relation.isCascadeUpdate === true && operationType === 'save',
					canBeSoftRemoved:
						relation.isCascadeSoftRemove === true && operationType === 'soft-remove',
					canBeRecovered: relation.isCascadeRecover === true && operationType === 'recover',
				});
				this.allSubjects.push(relationEntitySubject);

				// go recursively and find other entities we need to insert/update
				this.build(relationEntitySubject, operationType);
			});
	}

	// ---------------------------------------------------------------------
	// Protected Methods
	// ---------------------------------------------------------------------

	/**
	 * Finds subject where entity like given subject's entity.
	 * Comparison made by entity id.
	 */
	protected findByPersistEntityLike(
		entityTarget: Function | string,
		entity: ObjectLiteral,
	): Subject | undefined {
		return this.allSubjects.find((subject) => {
			if (!subject.entity) return false;

			if (subject.entity === entity) return true;

			return (
				subject.metadata.target === entityTarget &&
				subject.metadata.compareEntities(subject.entityWithFulfilledIds!, entity)
			);
		});
	}
}
