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

/**
 * Finds what columns are changed in the subject entities.
 */
export class SubjectChangedColumnsComputer {
	// -------------------------------------------------------------------------
	// Public Methods
	// -------------------------------------------------------------------------

	/**
	 * Finds what columns are changed in the subject entities.
	 */
	compute(subjects: Subject[]) {
		subjects.forEach((subject) => {
			this.computeDiffColumns(subject);
			this.computeDiffRelationalColumns(subjects, subject);
		});
	}

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

	/**
	 * Differentiate columns from the updated entity and entity stored in the database.
	 */
	protected computeDiffColumns(subject: Subject): void {
		// if there is no persisted entity then nothing to compute changed in it
		if (!subject.entity) return;

		subject.metadata.columns.forEach((column) => {
			// ignore special columns
			if (
				column.isVirtual ||
				column.isDiscriminator // ||
				// column.isUpdateDate ||
				// column.isVersion ||
				// column.isCreateDate
			)
				return;

			const changeMap = subject.changeMaps.find((changeMap) => changeMap.column === column);
			if (changeMap) {
				subject.changeMaps.splice(subject.changeMaps.indexOf(changeMap), 1);
			}

			// get user provided value - column value from the user provided persisted entity
			const entityValue = column.getEntityValue(subject.entity!);

			// we don't perform operation over undefined properties (but we DO need null properties!)
			if (entityValue === undefined) return;

			// if there is no database entity then all columns are treated as new, e.g. changed
			if (subject.databaseEntity) {
				// skip transform database value for json / jsonb for comparison later on
				const shouldTransformDatabaseEntity = column.type !== 'json' && column.type !== 'jsonb';

				// get database value of the column
				let databaseValue = column.getEntityValue(
					subject.databaseEntity,
					shouldTransformDatabaseEntity,
				);

				// filter out "relational columns" only in the case if there is a relation object in entity
				if (column.relationMetadata) {
					const value = column.relationMetadata.getEntityValue(subject.entity!);
					if (value !== null && value !== undefined) return;
				}
				let normalizedValue = entityValue;
				// normalize special values to make proper comparision
				if (entityValue !== null) {
					switch (column.type) {
						case 'date':
							normalizedValue = DateUtils.mixedDateToDateString(entityValue);
							break;

						case 'time':
						case 'time with time zone':
						case 'time without time zone':
						case 'timetz':
							normalizedValue = DateUtils.mixedDateToTimeString(entityValue);
							break;

						case 'datetime':
						case Date:
						case 'timestamp':
						case 'timestamp without time zone':
						case 'timestamp with time zone':
						case 'timestamptz':
							normalizedValue = DateUtils.mixedDateToUtcDatetimeString(entityValue);
							databaseValue = DateUtils.mixedDateToUtcDatetimeString(databaseValue);
							break;

						case 'json':
						case 'jsonb':
							// JSON.stringify doesn't work because postgresql sorts jsonb before save.
							// If you try to save json '[{"messages": "", "attribute Key": "", "level":""}] ' as jsonb,
							// then postgresql will save it as '[{"level": "", "message":"", "attributeKey": ""}]'
							if (OrmUtils.deepCompare(entityValue, databaseValue)) return;
							break;

						case 'simple-array':
							normalizedValue = DateUtils.simpleArrayToString(entityValue);
							databaseValue = DateUtils.simpleArrayToString(databaseValue);
							break;
						case 'simple-enum':
							normalizedValue = DateUtils.simpleEnumToString(entityValue);
							databaseValue = DateUtils.simpleEnumToString(databaseValue);
							break;
						case 'simple-json':
							normalizedValue = DateUtils.simpleJsonToString(entityValue);
							databaseValue = DateUtils.simpleJsonToString(databaseValue);
							break;
					}

					if (column.transformer) {
						normalizedValue = ApplyValueTransformers.transformTo(column.transformer, entityValue);
					}
				}

				// if value is not changed - then do nothing
				if (Buffer.isBuffer(normalizedValue) && Buffer.isBuffer(databaseValue)) {
					if (normalizedValue.equals(databaseValue)) {
						return;
					}
				} else {
					if (normalizedValue === databaseValue) return;
				}
			}

			if (!subject.diffColumns.includes(column)) subject.diffColumns.push(column);

			subject.changeMaps.push({
				column: column,
				value: entityValue,
			});
		});
	}

	/**
	 * Difference columns of the owning one-to-one and many-to-one columns.
	 */
	protected computeDiffRelationalColumns(allSubjects: Subject[], subject: Subject): void {
		// if there is no persisted entity then nothing to compute changed in it
		if (!subject.entity) return;

		subject.metadata.relationsWithJoinColumns.forEach((relation) => {
			// get the related entity from the persisted entity
			let relatedEntity = relation.getEntityValue(subject.entity!);

			// we don't perform operation over undefined properties (but we DO need null properties!)
			if (relatedEntity === undefined) return;

			// if there is no database entity then all relational columns are treated as new, e.g. changed
			if (subject.databaseEntity) {
				// here we cover two scenarios:
				// 1. related entity can be another entity which is natural way
				// 2. related entity can be just an entity id
				// if relation entity is just a relation id set (for example post.tag = 1)
				// then we create an id map from it to make a proper comparision
				let relatedEntityRelationIdMap: ObjectLiteral = relatedEntity;
				if (relatedEntityRelationIdMap !== null && ObjectUtils.isObject(relatedEntityRelationIdMap))
					relatedEntityRelationIdMap = relation.getRelationIdMap(relatedEntityRelationIdMap)!;

				// get database related entity. Since loadRelationIds are used on databaseEntity
				// related entity will contain only its relation ids
				const databaseRelatedEntityRelationIdMap = relation.getEntityValue(subject.databaseEntity);

				// if relation ids are equal then we don't need to update anything
				const areRelatedIdsEqual = OrmUtils.compareIds(
					relatedEntityRelationIdMap,
					databaseRelatedEntityRelationIdMap,
				);
				if (areRelatedIdsEqual) {
					return;
				} else {
					subject.diffRelations.push(relation);
				}
			}

			// if there is an inserted subject for the related entity of the persisted entity then use it as related entity
			// this code is used for related entities without ids to be properly inserted (and then updated if needed)
			const valueSubject = allSubjects.find(
				(subject) => subject.mustBeInserted && subject.entity === relatedEntity,
			);
			if (valueSubject) relatedEntity = valueSubject;

			// find if there is already a relation to be changed
			const changeMap = subject.changeMaps.find((changeMap) => changeMap.relation === relation);
			if (changeMap) {
				// and update its value if it was found
				changeMap.value = relatedEntity;
			} else {
				// if it wasn't found add a new relation for change
				subject.changeMaps.push({
					relation: relation,
					value: relatedEntity,
				});
			}
		});
	}
}
