/*******************************************************************************
 * 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.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.ModificationLog.Tag;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.Database;

/**
 * Represents a 1-to-0..1 relationship in a Nd database.
 */
public class FieldOneToOne<T extends INdStruct> extends BaseField implements IDestructableField, IRefCountedField {
	public final StructDef<T> nodeType;
	FieldOneToOne<?> backPointer;
	private boolean pointsToOwner;
	private final Tag putTag;
	private final Tag destructTag;

	/**
	 * @param nodeType
	 * @param backPointer
	 */
	private FieldOneToOne(StructDef<T> nodeType, FieldOneToOne<?> backPointer, boolean pointsToOwner) {
		this.nodeType = nodeType;

		if (backPointer != null) {
			if (backPointer.backPointer != null && backPointer.backPointer != this) {
				throw new IllegalArgumentException(
					"Attempted to construct a FieldOneToOne referring to a backpointer list that is already in use" //$NON-NLS-1$
						+ " by another field"); //$NON-NLS-1$
			}
			backPointer.backPointer = this;
		}
		this.backPointer = backPointer;
		this.pointsToOwner = pointsToOwner;
		setFieldName("field " + nodeType.getNumFields() + ", a " + getClass().getSimpleName() //$NON-NLS-1$//$NON-NLS-2$
				+ " in struct " + nodeType.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> FieldOneToOne<T> create(StructDef<B> builder,
			StructDef<T> nodeType, FieldOneToOne<B> forwardPointer) {

		FieldOneToOne<T> result = new FieldOneToOne<T>(nodeType, forwardPointer, false);
		builder.add(result);
		builder.addDestructableField(result);
		return result;
	}

	public static <T extends INdStruct, B extends INdStruct> FieldOneToOne<T> createOwner(StructDef<B> builder,
			StructDef<T> nodeType, FieldOneToOne<B> forwardPointer) {

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

	public T get(Nd nd, long address) {
		long ptr = nd.getDB().getRecPtr(address + this.offset);
		return NdNode.load(nd, ptr, this.nodeType);
	}

	public void put(Nd nd, long address, T target) {
		Database db = nd.getDB();
		db.getLog().start(this.putTag);
		try {
			cleanup(nd, address);
			if (target == null) {
				db.putRecPtr(address + this.offset, 0);
				if (this.pointsToOwner) {
					nd.scheduleDeletion(address);
				}
			} else {
				db.putRecPtr(address + this.offset, target.getAddress());
				db.putRecPtr(target.getAddress() + this.backPointer.offset, address);
			}
		} finally {
			db.getLog().end(this.putTag);
		}
	}

	@Override
	public void destruct(Nd nd, long address) {
		Database db = nd.getDB();
		db.getLog().start(this.destructTag);
		try {
			cleanup(nd, address);
		} finally {
			db.getLog().end(this.destructTag);
		}
	}

	private void cleanup(Nd nd, long address) {
		Database db = nd.getDB();
		long ptr = db.getRecPtr(address + this.offset);
		if (ptr != 0) {
			db.putRecPtr(ptr + this.backPointer.offset, 0);
			// If we own our target, delete it
			if (this.backPointer.pointsToOwner) {
				nd.scheduleDeletion(ptr);
			}
		}
	}

	@Override
	public int getRecordSize() {
		return Database.PTR_SIZE;
	}

	@Override
	public boolean hasReferences(Nd nd, long address) {
		if (this.pointsToOwner) {
			long ptr = nd.getDB().getRecPtr(address + this.offset);
			return ptr != 0;
		}
		return false;
	}
}
