/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ghidra.program.database.data;

import java.io.IOException;
import java.util.Collection;

import org.apache.commons.lang3.StringUtils;

import db.DBRecord;
import ghidra.docking.settings.*;
import ghidra.program.database.DBObjectCache;
import ghidra.program.database.DatabaseObject;
import ghidra.program.model.data.*;
import ghidra.program.model.mem.MemBuffer;
import ghidra.util.*;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.NotYetImplementedException;

/**
 * Base class for data types that are Database objects.
 *
 *
 */
abstract class DataTypeDB extends DatabaseObject implements DataType {

	protected DBRecord record;
	protected final DataTypeManagerDB dataMgr;
	protected volatile Settings defaultSettings;
	private final static SettingsDefinition[] EMPTY_DEFINITIONS = new SettingsDefinition[0];
	private final static TypeDefSettingsDefinition[] EMPTY_TYPEDEF_DEFINITIONS =
		new TypeDefSettingsDefinition[0];
	protected boolean resolving;
	protected boolean deleting;
	protected boolean pointerPostResolveRequired;
	protected Lock lock;
	private volatile String name;
	private volatile Category category;

	protected DataTypeDB(DataTypeManagerDB dataMgr, DBObjectCache<DataTypeDB> cache,
			DBRecord record) {
		super(cache, record.getKey());
		this.dataMgr = dataMgr;
		this.record = record;
		this.lock = dataMgr.lock;
	}

	/**
	 * Invoked by {@link DataTypeManagerDB} when operation has started to delete this datatype.
	 * In this state any invocation of other datatype changes should be ignored as reflected
	 * by {@code deleting} variable.  Once this state is changed and cannot be reverted for this 
	 * instance.
	 */
	protected final void deleteStarted() {
		deleting = true;
	}

	/**
	 * Clears the current name so that the next invocation of {@link #getName()} will
	 * force its update via {@link #doGetName()}.  It is important that {@link #doGetName()}
	 * does not get invoked during a {@link #refresh()} to avoid problematic recursion during the
	 * refresh caused by recursive use of {@link #checkIsValid()} on the same object.
	 * <P>
	 * NOTE: This must only be invoked while in a locked-state
	 */
	protected void refreshName() {
		name = null;
	}

	/**
	 * Subclasses implement this to either read the name from the database record or compute if it
	 * is a derived name such as a pointer or array. Implementers can assume that the database lock
	 * will be acquired when this method is called.
	 */
	protected abstract String doGetName();

	/**
	 * Subclasses implement this to read the category path from the database record.Implementers can
	 * assume that the database lock will be acquired when this method is called.
	 */
	protected abstract long doGetCategoryID();

	/**
	 * Subclasses implement this to update the category path ID to the database. Implementers can
	 * assume that the database lock will be acquired when this method is called.
	 */
	protected abstract void doSetCategoryPathRecord(long categoryID) throws IOException;

	/**
	 * Subclasses implement this to update the to the database. Implementers can assume that the
	 * database lock will be acquired when this method is called.
	 * 
	 * @param newName new data type name
	 */
	protected abstract void doSetNameRecord(String newName)
			throws IOException, InvalidNameException;

	/**
	 * Subclasses implement this to read the source archive id from the record. Implementers can
	 * assume that the database lock will be acquired when this method is called.
	 */
	protected abstract UniversalID getSourceArchiveID();

	/**
	 * Subclasses implement this to update the source archive id from the record. Implementers can
	 * assume that the database lock will be acquired when this method is called.
	 */
	protected abstract void setSourceArchiveID(UniversalID id);

	@Override
	public final DataOrganization getDataOrganization() {
		return dataMgr.getDataOrganization();
	}

	@Override
	protected void setDeleted() {
		defaultSettings = null;
		super.setDeleted();
	}

	@Override
	protected boolean refresh() {
		category = null;
		defaultSettings = null;
		refreshName();
		return true;
	}

	@Override
	public boolean isNotYetDefined() {
		return false;
	}

	@Override
	public boolean isZeroLength() {
		return false;
	}

	@Override
	public String getDisplayName() {
		return getName();
	}

	@Override
	public String toString() {
		return getDisplayName();
	}

	@Override
	public final String getName() {
		String n = name;
		if (n != null && !isInvalid()) {
			return n;
		}
		lock.acquire();
		try {
			checkIsValid();
			if (name == null) {
				name = doGetName();
			}
			return name;
		}
		finally {
			lock.release();
		}
	}

	@Override
	public Class<?> getValueClass(Settings settings) {
		return null;
	}

	/**
	 * Get the current name without refresh. This is intended to be used for event generation when
	 * an old-name is needed.
	 * 
	 * @return old name
	 */
	protected final String getOldName() {
		return name;
	}

	protected Settings doGetDefaultSettings() {
		return new DataTypeSettingsDB(dataMgr, this, key);
	}

	@Override
	public Settings getDefaultSettings() {
		Settings localDefaultSettings = defaultSettings;
		if (localDefaultSettings != null && !isInvalid()) {
			return localDefaultSettings;
		}
		lock.acquire();
		try {
			if (checkIsValid()) {
				defaultSettings = doGetDefaultSettings();
			}
			else {
				defaultSettings = SettingsImpl.NO_SETTINGS; // deleted datatype - keep everyone happy
			}
			return defaultSettings;
		}
		finally {
			lock.release();
		}
	}

	/**
	 * Set the data in the form of the appropriate Object for this DataType.
	 *
	 * @param buf the data buffer.
	 * @param settings the display settings for the current value.
	 * @param length the number of bytes to set the value from.
	 * @param value the new value to set object
	 */

	public void setValue(MemBuffer buf, Settings settings, int length, Object value) {
		throw new NotYetImplementedException("setValue() not implemented");
	}

	@Override
	public SettingsDefinition[] getSettingsDefinitions() {
		return EMPTY_DEFINITIONS;
	}

	@Override
	public TypeDefSettingsDefinition[] getTypeDefSettingsDefinitions() {
		return EMPTY_TYPEDEF_DEFINITIONS;
	}

	@Override
	public boolean isDeleted() {
		return isDeleted(lock);
	}

	@Override
	public void dataTypeSizeChanged(DataType dt) {
		// do nothing
	}

	@Override
	public void dataTypeAlignmentChanged(DataType dt) {
		// do nothing
	}

	@Override
	public DataTypeManager getDataTypeManager() {
		return dataMgr;
	}

	@Override
	public int getAlignment() {
		int length = getLength();
		if (length < 0) {
			return 1;
		}
		DataOrganization dataOrganization = dataMgr.getDataOrganization();
		return dataOrganization.getAlignment(this);
	}

	@Override
	public String getPathName() {
		return getDataTypePath().getPath();
	}

	protected void checkValidName(String newName) throws InvalidNameException {
		if (!DataUtilities.isValidDataTypeName(newName)) {
			throw new InvalidNameException();
		}
	}

	protected DataType resolve(DataType dt) {
		// complex types should keep equivalence checks to a minimum while resolving
		// and when post-resolve required for pointers
		resolving = true;
		try {
			dt = dataMgr.resolve(dt, dataMgr.getDependencyConflictHandler());
		}
		finally {
			resolving = false;
		}
		return dt;
	}

	protected void postPointerResolve(DataType definitionDt, DataTypeConflictHandler handler) {
		throw new UnsupportedOperationException("post-resolve of pointers not implemented");
	}

	@Override
	public CategoryPath getCategoryPath() {
		Category cat = category;
		if (cat != null && !isInvalid()) {
			return cat.getCategoryPath();
		}
		lock.acquire();
		try {
			checkIsValid();
			if (category == null) {
				category = dataMgr.getCategory(doGetCategoryID());
			}
			if (category == null) {
				category = dataMgr.getRootCategory();
			}
			return category.getCategoryPath();
		}
		finally {
			lock.release();
		}
	}

	@Override
	public DataTypePath getDataTypePath() {
		return new DataTypePath(getCategoryPath(), getName());
	}

	@Override
	public void setName(String newName) throws InvalidNameException, DuplicateNameException {
		lock.acquire();
		try {
			checkDeleted();
			if (getName().equals(newName)) {
				return;
			}
			CategoryPath categoryPath = getCategoryPath();
			if (dataMgr.getDataType(categoryPath, newName) != null) {
				throw new DuplicateNameException("DataType named " + newName +
					" already exists in category " + categoryPath.getPath());
			}
			doSetName(newName);
		}
		finally {
			lock.release();
		}
	}

	private final void doSetName(String newName) throws InvalidNameException {
		String oldName = getName();
		if (newName.equals(oldName)) {
			return;
		}
		checkValidName(newName);
		try {
			doSetNameRecord(newName);
			this.name = newName;
			notifyNameChanged(oldName);
		}
		catch (IOException e) {
			dataMgr.dbError(e);
		}

	}

	@Override
	public void setCategoryPath(CategoryPath path) throws DuplicateNameException {
		lock.acquire();
		try {
			checkDeleted();
			if (getCategoryPath().equals(path)) {
				return;
			}
			String currentName = getName();
			if (dataMgr.getDataType(path, currentName) != null) {
				throw new DuplicateNameException("DataType named " + currentName +
					" already exists in category " + path.getPath());
			}
			doSetCategoryPath(path);
		}
		finally {
			lock.release();
		}
	}

	private void doSetCategoryPath(CategoryPath path) {
		CategoryPath myPath = getCategoryPath();
		if (path.equals(myPath)) {
			return;
		}

		long oldCatId = doGetCategoryID();
		Category cat = dataMgr.createCategory(path);
		try {
			doSetCategoryPathRecord(cat.getID());
			category = cat;
			dataMgr.dataTypeCategoryPathChanged(this, myPath, oldCatId);
		}
		catch (IOException e) {
			dataMgr.dbError(e);
		}
	}

	@Override
	public void setNameAndCategory(CategoryPath path, String name)
			throws InvalidNameException, DuplicateNameException {
		lock.acquire();
		try {
			checkDeleted();
			DataType dt = dataMgr.getDataType(path, name);
			if (dt != null) {
				if (dt == this) {
					return; // unchanged
				}
				throw new DuplicateNameException(
					"DataType named " + name + " already exists in category " + path.getPath());
			}

			// generate a name that would not cause a duplicate in either the current path
			// or
			// the new path. Use the new name if possible.
			String uniqueName = dataMgr.getTemporaryUniqueName(path, getCategoryPath(), name);
			doSetName(uniqueName);

			// set the path - this is guaranteed to work since we make a name that won't
			// conflict
			doSetCategoryPath(path);

			// now, if necessary, rename it to the desired name - guaranteed to work since
			// we checked before we started changing things.
			if (!uniqueName.equals(name)) {
				doSetName(name);
			}
		}
		finally {
			lock.release();
		}
	}

	/**
	 * Updates the path for this datatype
	 * 
	 * @param dt the dataType whose path has changed.
	 */
	protected void updatePath(DataTypeDB dt) {
		// no-op
	}

	@Override
	public void addParent(DataType dt) {
		if (dt instanceof DataTypeDB && dt.getDataTypeManager() == dataMgr) {
			dataMgr.addParentChildRecord(((DataTypeDB) dt).key, key);
		}
	}

	@Override
	public void removeParent(DataType dt) {
		if (dt instanceof DataTypeDB && dt.getDataTypeManager() == dataMgr) {
			dataMgr.removeParentChildRecord(((DataTypeDB) dt).key, key);
		}
	}

	/**
	 * Notify all parents that the size of this datatype has changed or other significant change
	 * that may affect a parent containing this datatype.
	 * 
	 * @param isAutoChange true if changes are in response to another datatype's change.
	 */
	protected void notifySizeChanged(boolean isAutoChange) {
		for (DataType dt : dataMgr.getParentDataTypes(key)) {
			dt.dataTypeSizeChanged(this);
		}
		dataMgr.dataTypeChanged(this, isAutoChange);
	}

	/**
	 * Notification that this composite data type's alignment has changed.
	 * 
	 * @param isAutoChange true if changes are in response to another datatype's change.
	 */
	protected void notifyAlignmentChanged(boolean isAutoChange) {
		for (DataType dt : dataMgr.getParentDataTypes(key)) {
			dt.dataTypeAlignmentChanged(this);
		}
		dataMgr.dataTypeChanged(this, isAutoChange);
	}

	protected void notifyNameChanged(String oldName) {
		for (DataType dt : dataMgr.getParentDataTypes(key)) {
			dt.dataTypeNameChanged(this, oldName);
		}
		dataMgr.dataTypeNameChanged(this, oldName);
	}

	protected void notifyDeleted() {
		for (DataType dt : dataMgr.getParentDataTypes(key)) {
			dt.dataTypeDeleted(this);
		}
	}

	@Override
	public Collection<DataType> getParents() {
		return dataMgr.getParentDataTypes(key);
	}

	@Override
	public boolean dependsOn(DataType dt) {
		return false;
	}

	@Override
	public String getDefaultLabelPrefix() {
		return null;
	}

	@Override
	public String getDefaultAbbreviatedLabelPrefix() {
		return getDefaultLabelPrefix();
	}

	@Override
	public String getDefaultLabelPrefix(MemBuffer buf, Settings settings, int len,
			DataTypeDisplayOptions options) {
		return getDefaultLabelPrefix();
	}

	@Override
	public String getDefaultOffcutLabelPrefix(MemBuffer buf, Settings settings, int len,
			DataTypeDisplayOptions options, int offcutLength) {
		// By default we will do nothing different for offcut values
		return getDefaultLabelPrefix(buf, settings, len, options);
	}

	@Override
	public void setSourceArchive(SourceArchive archive) {
		archive = getDataTypeManager().resolveSourceArchive(archive);
		UniversalID id = archive == null ? DataTypeManager.LOCAL_ARCHIVE_UNIVERSAL_ID
				: archive.getSourceArchiveID();
		if (id.equals(getDataTypeManager().getUniversalID())) {
			id = DataTypeManager.LOCAL_ARCHIVE_UNIVERSAL_ID;
		}

		setSourceArchiveID(id);
	}

	@Override
	public SourceArchive getSourceArchive() {
		return dataMgr.getSourceArchive(getSourceArchiveID());
	}

	@Override
	public void replaceWith(DataType dataType) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Sets a String briefly describing this DataType. <br>
	 * If a data type that extends this class wants to allow the description to be changed, then it
	 * must override this method.
	 * 
	 * @param description a one-liner describing this DataType.
	 */
	@Override
	public void setDescription(String description) {
		// no-op
	}

	/**
	 * setUniversalID is a package level method that allows you to change a data type's universal
	 * ID. This is only intended to be used when transforming a newly parsed data type archive so
	 * that it can be used as a replacement of the archive from a previous software release.
	 * 
	 * @param oldUniversalID the old universal ID value that the user is already referencing with
	 *            their data types. This is the universal ID that we want the new data type to be
	 *            known by.
	 */
	abstract void setUniversalID(UniversalID oldUniversalID);

	@Override
	public int hashCode() {
		return getName().hashCode();
	}

	@Override
	public final boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof DataType)) {
			return false;
		}
		DataType otherDt = (DataType) obj;
		return otherDt.getDataTypeManager() == getDataTypeManager() &&
			getCategoryPath().equals(otherDt.getCategoryPath()) &&
			getName().equals(otherDt.getName()) && isEquivalent(otherDt);
	}

	@Override
	public boolean isEncodable() {
		return false;
	}

	@Override
	public byte[] encodeValue(Object value, MemBuffer buf, Settings settings, int length)
			throws DataTypeEncodeException {
		throw new DataTypeEncodeException("Encoding not supported", value, this);
	}

	@Override
	public byte[] encodeRepresentation(String repr, MemBuffer buf, Settings settings, int length)
			throws DataTypeEncodeException {
		throw new DataTypeEncodeException("Encoding not supported", repr, this);
	}

	/**
	 * Perform equivalence check while resolving the specified dataType.  If the specified conflict 
	 * handler under a conflict situation indicates that the existing data type (i.e., this type)
	 * be used in place of the specified dataType this method will return true.
	 * @param dataType datatype being resolved
	 * @param handler resolve conflict handler (if null perform normal {@link #isEquivalent(DataType)}
	 * @return true if the specified dataType should be considered equivalent to this datatype.
	 */
	protected abstract boolean isEquivalent(DataType dataType, DataTypeConflictHandler handler);

	/**
	 * If possible, perform equivalence check while resolving the specified dataType if the 
	 * existingDataType is an instance of DataTypeDB.  Otherwise, perform a normal 
	 * isEquivalent operation.  If the specified conflict 
	 * handler under a conflict situation indicates that the existing data type (i.e., this type)
	 * be used in place of the specified dataType this method will return true.
	 * @param existingDataType existing datatype
	 * @param otherDataType datatype being resolved
	 * @param handler resolve conflict handler (if null perform normal {@link #isEquivalent(DataType)}
	 * @return true if the specified dataType should be considered equivalent to this datatype.
	 */
	static boolean isEquivalent(DataType existingDataType, DataType otherDataType,
			DataTypeConflictHandler handler) {
		if (existingDataType instanceof DataTypeDB existingDataTypeDB) {
			return existingDataTypeDB.isEquivalent(otherDataType, handler);
		}
		return existingDataType.isEquivalent(otherDataType);
	}

	static String prependComment(String additionalComment, String oldComment) {
		String comment = additionalComment;
		if (!StringUtils.isBlank(oldComment)) {
			comment += "; " + oldComment;
		}
		return comment;
	}

}
