/*******************************************************************************
 * Copyright (c) 2015, 2016 Google, Inc and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Stefan Xenos (Google) - Initial implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.core.nd.field;

import org.aspectj.org.eclipse.jdt.internal.core.nd.INdStruct;
import org.aspectj.org.eclipse.jdt.internal.core.nd.ITypeFactory;
import org.aspectj.org.eclipse.jdt.internal.core.nd.Nd;
import org.aspectj.org.eclipse.jdt.internal.core.nd.NdNode;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.ModificationLog;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.Database;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.ModificationLog.Tag;

/**
 * Holds the n side of a n..1 relationship. Declares a Nd field which is a pointer of a NdNode of the specified
 * type. {@link FieldManyToOne} forms a one-to-many relationship with {@link FieldOneToMany}. Whenever a
 * {@link FieldManyToOne} points to an object, the inverse pointer is automatically inserted into the matching back
 * pointer list.
 */
public class FieldManyToOne<T extends INdStruct> extends BaseField implements IDestructableField, IRefCountedField {
	public final static FieldPointer TARGET;
	public final static FieldInt BACKPOINTER_INDEX;

	StructDef<T> targetType;
	final StructDef<? extends INdStruct> localType;
	FieldOneToMany<?> backPointer;
	@SuppressWarnings("rawtypes")
	private final static StructDef<FieldManyToOne> type;
	/**
	 * True iff the other end of this pointer should delete this object when its end of the pointer is cleared.
	 */
	public final boolean pointsToOwner;
	private final Tag putTag;
	private final Tag destructTag;
	private boolean permitsNull = true;

	static {
		type = StructDef.createAbstract(FieldManyToOne.class);
		TARGET = type.addPointer();
		BACKPOINTER_INDEX = type.addInt();
		type.done();
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private FieldManyToOne(StructDef<? extends INdStruct> localType, FieldOneToMany<?> backPointer, boolean pointsToOwner) {
		this.localType = localType;
		this.pointsToOwner = pointsToOwner;

		if (backPointer != null) {
			if (backPointer.forwardPointer != null && backPointer.forwardPointer != this) {
				throw new IllegalArgumentException(
						"Attempted to construct a FieldNodePointer referring to a backpointer list that is already in use" //$NON-NLS-1$
								+ " by another field"); //$NON-NLS-1$
			}
			backPointer.targetType = (StructDef) localType;
			this.targetType = (StructDef) backPointer.localType;
			backPointer.forwardPointer = this;
		}
		this.backPointer = backPointer;
		setFieldName("field " + localType.getNumFields() + ", a " + getClass().getSimpleName() //$NON-NLS-1$//$NON-NLS-2$
				+ " in struct " + localType.getStructName()); //$NON-NLS-1$
		this.putTag = ModificationLog.createTag("Writing " + getFieldName()); //$NON-NLS-1$
		this.destructTag = ModificationLog.createTag("Destructing " + getFieldName()); //$NON-NLS-1$
	}

	public static <T extends INdStruct, B extends INdStruct> FieldManyToOne<T> createNonNull(StructDef<B> builder,
			FieldOneToMany<B> forwardPointer) {
		FieldManyToOne<T> result = create(builder, forwardPointer);
		result.permitsNull = false;
		return result;
	}

	public static <T extends INdStruct, B extends INdStruct> FieldManyToOne<T> create(StructDef<B> builder,
			FieldOneToMany<B> forwardPointer) {
		FieldManyToOne<T> result = new FieldManyToOne<T>(builder, forwardPointer, false);
		builder.add(result);
		builder.addDestructableField(result);
		return result;
	}

	/**
	 * Creates a many-to-one pointer which points to this object's owner. If the pointer is non-null when the owner is
	 * deleted, this object will be deleted too.
	 *
	 * @param builder the struct to which the field will be added
	 * @param forwardPointer the field which holds the pointer in the other direction
	 * @return a newly constructed field
	 */
	public static <T extends INdStruct, B extends INdStruct> FieldManyToOne<T> createOwner(StructDef<B> builder,
			FieldOneToMany<B> forwardPointer) {
		// Although it would work to have a non-NdNode owned in this manner, we currently have no legitimate use-cases
		// for this to occur. If this happens it is almost certainly an accidental copy-paste error where someone
		// intended to call create but called this method instead. If we ever discover a legitimate use-case for it,
		// this could be removed and things would probably still work.
		if (!NdNode.class.isAssignableFrom(builder.getStructClass())) {
			throw new IllegalArgumentException(FieldManyToOne.class.getSimpleName() + " can't be the owner of " //$NON-NLS-1$
					+ builder.getStructClass().getSimpleName() + " because the latter isn't a subclass of " //$NON-NLS-1$
					+ NdNode.class.getSimpleName());
		}

		FieldManyToOne<T> result = new FieldManyToOne<T>(builder, forwardPointer, true);
		builder.add(result);
		builder.addDestructableField(result);
		builder.addOwnerField(result);
		return result;
	}

	/**
	 * Sets whether or not this field permits nulls to be assigned.
	 *
	 * @param permitted true iff the field permits nulls
	 * @return this
	 */
	public FieldManyToOne<T> permitNull(boolean permitted) {
		this.permitsNull = permitted;
		return this;
	}

	public T get(Nd nd, long address) {
		return NdNode.load(nd, getAddress(nd, address), this.targetType);
	}

	public long getAddress(Nd nd, long address) {
		long result = nd.getDB().getRecPtr(address + this.offset);
		if (!this.permitsNull && result == 0) {
			throw nd.describeProblem()
				.addProblemAddress(this, address)
				.build("Database contained a null in a non-null field"); //$NON-NLS-1$
		}
		return result;
	}

	/**
	 * Directs this pointer to the given target. Also removes this pointer from the old backpointer list (if any) and
	 * inserts it into the new backpointer list (if any)
	 */
	public void put(Nd nd, long address, T value) {
		if (value != null) {
			put(nd, address, value.getAddress());
		} else if (this.permitsNull) {
			put(nd, address, 0);
		} else {
			throw new IllegalArgumentException("Attempted to write a null into a non-null field"); //$NON-NLS-1$
		}
	}

	public void put(Nd nd, long address, long newTargetAddress) {
		Database db = nd.getDB();
		db.getLog().start(this.putTag);
		try {
			long fieldStart = address + this.offset;
			if (this.backPointer == null) {
				throw new IllegalStateException(
						getClass().getSimpleName() + " must be associated with a " + FieldOneToMany.class.getSimpleName()); //$NON-NLS-1$
			}

			long oldTargetAddress = TARGET.get(nd, fieldStart);
			if (oldTargetAddress == newTargetAddress) {
				return;
			}

			detachFromOldTarget(nd, address, oldTargetAddress);

			TARGET.put(nd, fieldStart, newTargetAddress);
			if (newTargetAddress != 0) {
				// Note that newValue is the address of the backpointer list and record (the address of the struct
				// containing the forward pointer) is the value being inserted into the list.
				BACKPOINTER_INDEX.put(nd, fieldStart, this.backPointer.add(nd, newTargetAddress, address));
			} else {
				if (this.pointsToOwner) {
					nd.scheduleDeletion(address);
				}
			}
		} finally {
			db.getLog().end(this.putTag);
		}
	}

	protected void detachFromOldTarget(Nd nd, long address, long oldTargetAddress) {
		long fieldStart = address + this.offset;
		if (oldTargetAddress != 0) {
			int oldIndex = BACKPOINTER_INDEX.get(nd, fieldStart);

			this.backPointer.remove(nd, oldTargetAddress, oldIndex);

			if (this.targetType.isNdNode()) {
				short targetTypeId = NdNode.NODE_TYPE.get(nd, oldTargetAddress);
				ITypeFactory<? extends NdNode> typeFactory = nd.getTypeFactory(targetTypeId);

				if (typeFactory.getDeletionSemantics() == StructDef.DeletionSemantics.REFCOUNTED
						&& typeFactory.isReadyForDeletion(nd, oldTargetAddress)) {
					nd.scheduleDeletion(oldTargetAddress);
				}
			}
		}
	}

	/**
	 * Called when the index of this forward pointer has moved in the backpointer list. Adjusts the index.
	 * <p>
	 * Not intended to be called by clients. This is invoked by {@link FieldOneToMany} whenever it reorders elements in
	 * the array.
	 */
	void adjustIndex(Nd nd, long address, int index) {
		BACKPOINTER_INDEX.put(nd, address + this.offset, index);
	}

	@Override
	public void destruct(Nd nd, long address) {
		Database db = nd.getDB();
		db.getLog().start(this.destructTag);
		try {
			long fieldStart = address + this.offset;
			long oldTargetAddress = TARGET.get(nd, fieldStart);
			detachFromOldTarget(nd, address, oldTargetAddress);
			TARGET.put(nd, fieldStart, 0);
		} finally {
			db.getLog().end(this.destructTag);
		}
	}

	void clearedByBackPointer(Nd nd, long address) {
		long fieldStart = this.offset + address;
		FieldManyToOne.TARGET.put(nd, fieldStart, 0);
		FieldManyToOne.BACKPOINTER_INDEX.put(nd, fieldStart, 0);
	}

	@Override
	public int getRecordSize() {
		return type.size();
	}

	@Override
	public boolean hasReferences(Nd nd, long address) {
		long fieldStart = this.offset + address;
		long target = TARGET.get(nd, fieldStart);
		return target != 0;
	}
}
