/* ###
 * 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.symbol;

import java.io.IOException;

import db.DBRecord;
import ghidra.program.database.DBObjectCache;
import ghidra.program.database.function.FunctionDB;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.OldGenericNamespaceAddress;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.Undefined;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.VariableNameFieldLocation;
import ghidra.util.Lock;
import ghidra.util.exception.InvalidInputException;
import ghidra.util.task.TaskMonitor;

/**
 * Symbol class for function variables.
 *
 * Symbol Data Usage:
 *   	String stringData - variable comment
 */
public class VariableSymbolDB extends SymbolDB {

	private VariableStorage variableStorage;
	private VariableStorageManagerDB variableMgr;
	private SymbolType type;

	/**
	 * Constructs a new VariableSymbol
	 * @param symbolMgr the symbol manager
	 * @param cache symbol object cache
	 * @param type the symbol type.
	 * @param variableMgr variable storage manager
	 * @param address the address of the symbol (stack address)
	 * @param record the record for the symbol
	 */
	public VariableSymbolDB(SymbolManager symbolMgr, DBObjectCache<SymbolDB> cache, SymbolType type,
			VariableStorageManagerDB variableMgr, Address address, DBRecord record) {
		super(symbolMgr, cache, address, record);
		this.type = type;
		this.variableMgr = variableMgr;
	}

	@Override
	public void setInvalid() {
		super.setInvalid();
		variableStorage = null;
	}

	public VariableStorage getVariableStorage() {
		lock.acquire();
		try {
			if (!checkIsValid() || variableStorage != null) {
				return variableStorage;
			}
			if (address instanceof OldGenericNamespaceAddress) {
				// old use case for upgrade
				try {
					variableStorage = new VariableStorage(symbolMgr.getProgram(),
						((OldGenericNamespaceAddress) address).getGlobalAddress(),
						getDataType().getLength());
				}
				catch (InvalidInputException e) {
					variableStorage = VariableStorage.BAD_STORAGE;
				}
			}
			else {
				variableStorage = variableMgr.getVariableStorage(address);
				if (variableStorage == null) {
					variableStorage = (type != SymbolType.PARAMETER) ? VariableStorage.BAD_STORAGE
							: VariableStorage.UNASSIGNED_STORAGE;
				}
			}
		}
		catch (IOException e) {
			symbolMgr.dbError(e);
		}
		finally {
			lock.release();
		}
		return variableStorage;
	}

	@Override
	public SymbolType getSymbolType() {
		return type;
	}

	@Override
	protected boolean refresh(DBRecord rec) {
		boolean isValid = super.refresh(rec);
		variableStorage = null;
		return isValid;
	}

	@Override
	public boolean equals(Object obj) {
		// TODO: not sure what constitutes equality since address will differ
		return obj == this;
	}

	@Override
	public boolean delete() {
		lock.acquire();
		try {
			if (checkIsValid()) {
				FunctionDB fun = getFunction();
				if (fun != null) {
					fun.doDeleteVariable(this);
				}
				super.delete();
				return true;
			}
			return false;
		}
		finally {
			lock.release();
		}
	}

	@Override
	public Variable getObject() {
		FunctionDB func = getFunction();
		if (func != null) {
			return func.getVariable(this);
		}
		return null;
	}

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

	@Override
	public boolean isExternal() {
		Symbol parentSymbol = getParentSymbol();
		return parentSymbol != null ? parentSymbol.isExternal() : false;
	}

	public FunctionDB getFunction() {
		return (FunctionDB) symbolMgr.getFunctionManager()
				.getFunction(getParentNamespace().getID());
	}

	@Override
	public ProgramLocation getProgramLocation() {
		Variable var = getObject();
		if (var != null) {
			return new VariableNameFieldLocation(var.getProgram(), var, 0);
		}
		return null;
	}

	@Override
	public boolean isValidParent(Namespace parent) {
		// symbol is locked to single function and can't be moved
		return getFunction() == parent;
	}

	private String getParamName() {
		return SymbolUtilities.getDefaultParamName(getOrdinal());
	}

	@Override
	protected String doGetName() {
		if (!checkIsValid()) {
			// TODO: SCR
			return "[Invalid VariableSymbol - Deleted!]";
		}

		if (type == SymbolType.PARAMETER) {
			if (getSource() == SourceType.DEFAULT) {
				return getParamName();
			}
			String storedName = super.doGetName();
			if (SymbolUtilities.isDefaultParameterName(storedName)) {
				return getParamName();
			}
			return storedName;
		}

		VariableStorage storage = getVariableStorage();
		if (storage == null || storage.isBadStorage()) {
			return Function.DEFAULT_LOCAL_PREFIX + "_!BAD!";
		}

		if (getSource() == SourceType.DEFAULT) {
			return SymbolUtilities.getDefaultLocalName(getProgram(), storage, getFirstUseOffset());
		}

		// TODO: we use to check for a default name and regenerate new default name but we should
		// not need to do this if source remains at default

		return super.doGetName();
	}

	@Override
	protected SourceType validateNameSource(String newName, SourceType source) {
		if (SymbolUtilities.isDefaultParameterName(newName)) {
			source = SourceType.DEFAULT;
		}
		SymbolType symType = getSymbolType();
		if (symType == SymbolType.PARAMETER && SymbolUtilities.isDefaultParameterName(newName)) {
			source = SourceType.DEFAULT;
		}
		else if (symType == SymbolType.LOCAL_VAR &&
			SymbolUtilities.isDefaultLocalName(getProgram(), newName, getVariableStorage())) {
			return SourceType.DEFAULT;
		}
		return source;
	}

	public DataType getDataType() {
		DataType dt = symbolMgr.getDataType(getDataTypeId());
		if (dt == null) {
			VariableStorage storage = getVariableStorage();
			if (storage == null) {
				dt = DataType.DEFAULT;
			}
			else if (storage.isVoidStorage()) {
				dt = DataType.VOID;
			}
			else {
				dt = Undefined.getUndefinedDataType(storage.size());
			}
		}
		return dt;
	}

	/**
	 * Change the storage address and data-type associated with this variable symbol.
	 * @param newStorage the new storage
	 * @param dt data-type
	 */
	public void setStorageAndDataType(VariableStorage newStorage, DataType dt) {
		Lock myLock = symbolMgr.getLock();
		myLock.acquire();
		try {
			checkDeleted();

			long dataTypeID = symbolMgr.getProgram().getDataTypeManager().getResolvedID(dt);

			variableStorage = newStorage;
			Address newAddr = variableMgr.getVariableStorageAddress(newStorage, true);
			setAddress(newAddr); // this may be the only symbol which changes its address

			if (dataTypeID != getDataTypeId()) {
				setDataTypeId(dataTypeID);
			}
			else {
				symbolMgr.symbolDataChanged(this);
			}
		}
		catch (IOException e) {
			symbolMgr.dbError(e);
		}
		finally {
			myLock.release();
		}
	}

	public int getFirstUseOffset() {
		return type == SymbolType.PARAMETER ? 0 : getVariableOffset();
	}

	public void setFirstUseOffset(int firstUseOffset) {
		if (type == SymbolType.LOCAL_VAR) {
			setVariableOffset(firstUseOffset);
		}
	}

	public int getOrdinal() {
		return type == SymbolType.PARAMETER ? getVariableOffset() : Integer.MIN_VALUE;
	}

	public void setOrdinal(int ordinal) {
		if (type == SymbolType.PARAMETER) {
			setVariableOffset(ordinal);
		}
	}

	@Override
	public int getReferenceCount() {
		return getReferences(null).length;
	}

	@Override
	public Reference[] getReferences(TaskMonitor monitor) {
		lock.acquire();
		try {
			checkIsValid();
			ReferenceManager rm = symbolMgr.getReferenceManager();
			return rm.getReferencesTo(getObject());
		}
		finally {
			lock.release();
		}
	}

	@Override
	public boolean hasReferences() {
		return getReferences(null).length != 0;
	}

	/**
	 * gets the generic symbol data 2 data.
	 * @return the symbol data
	 */
	protected int getVariableOffset() {
		lock.acquire();
		try {
			checkIsValid();
			if (record != null) {
				return record.getIntValue(SymbolDatabaseAdapter.SYMBOL_VAROFFSET_COL);
			}
			return 0;
		}
		finally {
			lock.release();
		}
	}

	/**
	 * Sets the symbol's variable offset. For parameters, this is the ordinal, for locals, it is 
	 * the first use offset
	 * @param offset the value to set as the symbols variable offset. 
	 */
	public void setVariableOffset(int offset) {
		lock.acquire();
		try {
			checkDeleted();
			if (record != null) {
				record.setIntValue(SymbolDatabaseAdapter.SYMBOL_VAROFFSET_COL, offset);
				updateRecord();
				symbolMgr.symbolDataChanged(this);
			}
		}
		finally {
			lock.release();
		}
	}

	/**
	 * {@return variable symbol comment}
	 */
	public String getSymbolComment() {
		validate(lock);
		return record.getString(SymbolDatabaseAdapter.SYMBOL_COMMENT_COL);
	}

	/**
	 * Update variable symbol comment (no change event is issued)
	 * @param comment variable comment
	 */
	public void setSymbolComment(String comment) {
		lock.acquire();
		try {
			checkDeleted();
			record.setString(SymbolDatabaseAdapter.SYMBOL_COMMENT_COL, comment);
			updateRecord();
		}
		finally {
			lock.release();
		}
	}

	/**
	 * Update variable symbol record fields
	 * @param record variable symbol record
	 * @param firstUseOffsetOrOrdinal first use offset or param ordinal (ignored if null)
	 * @param comment variable comment
	 */
	static void setRecordFields(DBRecord record, Integer firstUseOffsetOrOrdinal, String comment) {
		if (firstUseOffsetOrOrdinal != null) {
			record.setIntValue(SymbolDatabaseAdapter.SYMBOL_VAROFFSET_COL, firstUseOffsetOrOrdinal);
		}
		record.setString(SymbolDatabaseAdapter.SYMBOL_COMMENT_COL, comment);
	}
}
