/*******************************************************************************
 * Copyright (c) 2006, 2014 QNX Software Systems 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:
 *     QNX - Initial API and implementation
 *     Markus Schorn (Wind River Systems)
 *     Andrew Ferguson (Symbian)
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.core.pdom.dom;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.IPDOMVisitor;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorMacroDefinition;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorUndefStatement;
import org.eclipse.cdt.core.dom.ast.IMacroBinding;
import org.eclipse.cdt.core.index.IIndexFile;
import org.eclipse.cdt.core.index.IIndexMacro;
import org.eclipse.cdt.core.index.IndexLocationFactory;
import org.eclipse.cdt.core.parser.Keywords;
import org.eclipse.cdt.core.parser.util.CharArrayUtils;
import org.eclipse.cdt.internal.core.index.IIndexBindingConstants;
import org.eclipse.cdt.internal.core.index.IIndexFragment;
import org.eclipse.cdt.internal.core.index.IIndexFragmentBinding;
import org.eclipse.cdt.internal.core.index.IIndexFragmentName;
import org.eclipse.cdt.internal.core.index.IIndexScope;
import org.eclipse.cdt.internal.core.parser.scanner.CharArray;
import org.eclipse.cdt.internal.core.parser.scanner.MacroDefinitionParser;
import org.eclipse.cdt.internal.core.pdom.PDOM;
import org.eclipse.cdt.internal.core.pdom.db.Database;
import org.eclipse.cdt.internal.core.pdom.db.IString;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;

/**
 * Represents macro definitions. They are stored with the file and with a PDOMMacroContainer.
 * The latter also contains the references to all macros with the same name.
 */
public class PDOMMacro implements IIndexMacro, IPDOMBinding {
	private static final int CONTAINER = 0;
	private static final int FILE = 4;
	private static final int PARAMETERS = 8;
	private static final int EXPANSION = 12;
	private static final int NEXT_IN_FILE = 16;
	private static final int NEXT_IN_CONTAINER = 20;
	private static final int PREV_IN_CONTAINER = 24;
	private static final int NAME_OFFSET = 28;
	private static final int NAME_LENGTH = 32; // short

	private static final int RECORD_SIZE = 34;
	private static final char[][] UNINITIALIZED = {};
	private static final char[] UNINITIALIZED1 = {};

	private final PDOMLinkage fLinkage;
	private final long fRecord;

	private char[][] fParameterList = UNINITIALIZED;
	private char[] fExpansion = UNINITIALIZED1;
	private PDOMMacroContainer fContainer;
	private PDOMMacroDefinitionName fDefinition;

	public PDOMMacro(PDOMLinkage linkage, long record) {
		fLinkage = linkage;
		fRecord = record;
	}

	public PDOMMacro(PDOMLinkage linkage, PDOMMacroContainer container, IASTPreprocessorMacroDefinition macro,
			PDOMFile file) throws CoreException {
		this(linkage, container, file, macro.getName());

		final IASTName name = macro.getName();
		final IMacroBinding binding = (IMacroBinding) name.getBinding();
		final char[][] params = binding.getParameterList();

		final Database db = linkage.getDB();
		db.putRecPtr(fRecord + EXPANSION, db.newString(binding.getExpansionImage()).getRecord());
		if (params != null) {
			StringBuilder buf = new StringBuilder();
			for (char[] param : params) {
				buf.append(param);
				buf.append(',');
			}
			db.putRecPtr(fRecord + PARAMETERS, db.newString(buf.toString().toCharArray()).getRecord());
		}
	}

	public PDOMMacro(PDOMLinkage linkage, PDOMMacroContainer container, IASTPreprocessorUndefStatement undef,
			PDOMFile file) throws CoreException {
		this(linkage, container, file, undef.getMacroName());
	}

	private PDOMMacro(PDOMLinkage linkage, PDOMMacroContainer container, PDOMFile file, IASTName name)
			throws CoreException {
		final Database db = linkage.getDB();
		fLinkage = linkage;
		fRecord = db.malloc(RECORD_SIZE);
		fContainer = container;

		final IASTFileLocation fileloc = name.getFileLocation();
		db.putRecPtr(fRecord + CONTAINER, container.getRecord());
		db.putRecPtr(fRecord + FILE, file.getRecord());
		db.putInt(fRecord + NAME_OFFSET, fileloc.getNodeOffset());
		db.putShort(fRecord + NAME_LENGTH, (short) fileloc.getNodeLength());

		container.addDefinition(this);
	}

	@Override
	public PDOM getPDOM() {
		return fLinkage.getPDOM();
	}

	@Override
	public long getRecord() {
		return fRecord;
	}

	@Override
	public void delete(PDOMLinkage linkage) throws CoreException {
		// Delete from the binding chain
		PDOMMacro prevName = getPrevInContainer();
		PDOMMacro nextName = getNextInContainer();
		if (prevName != null) {
			prevName.setNextInContainer(nextName);
		} else {
			PDOMMacroContainer container = getContainer();
			container.setFirstDefinition(nextName);
			if (nextName == null && container.isOrphaned()) {
				container.delete(linkage);
			}
		}
		if (nextName != null)
			nextName.setPrevInContainer(prevName);

		final IString expansion = getExpansionInDB();
		if (expansion != null) {
			expansion.delete();
		}
		final IString params = getParamListInDB();
		if (params != null) {
			params.delete();
		}
		linkage.getDB().free(fRecord);
	}

	public PDOMMacroContainer getContainer() throws CoreException {
		if (fContainer == null) {
			fContainer = new PDOMMacroContainer(fLinkage, fLinkage.getDB().getRecPtr(fRecord + CONTAINER));
		}
		return fContainer;
	}

	private IString getExpansionInDB() throws CoreException {
		Database db = fLinkage.getDB();
		long rec = db.getRecPtr(fRecord + EXPANSION);
		return rec == 0 ? null : db.getString(rec);
	}

	private IString getParamListInDB() throws CoreException {
		Database db = fLinkage.getDB();
		long rec = db.getRecPtr(fRecord + PARAMETERS);
		return rec == 0 ? null : db.getString(rec);
	}

	public PDOMMacro getNextMacro() throws CoreException {
		long rec = fLinkage.getDB().getRecPtr(fRecord + NEXT_IN_FILE);
		return rec != 0 ? new PDOMMacro(fLinkage, rec) : null;
	}

	public void setNextMacro(PDOMMacro macro) throws CoreException {
		setNextMacro(macro != null ? macro.getRecord() : 0);
	}

	private void setNextMacro(long rec) throws CoreException {
		fLinkage.getDB().putRecPtr(fRecord + NEXT_IN_FILE, rec);
	}

	private PDOMMacro getPrevInContainer() throws CoreException {
		return getMacroField(PREV_IN_CONTAINER);
	}

	void setPrevInContainer(PDOMMacro macro) throws CoreException {
		setMacroField(PREV_IN_CONTAINER, macro);
	}

	public PDOMMacro getNextInContainer() throws CoreException {
		return getMacroField(NEXT_IN_CONTAINER);
	}

	void setNextInContainer(PDOMMacro macro) throws CoreException {
		setMacroField(NEXT_IN_CONTAINER, macro);
	}

	private void setMacroField(int offset, PDOMMacro macro) throws CoreException {
		long namerec = macro != null ? macro.getRecord() : 0;
		fLinkage.getDB().putRecPtr(fRecord + offset, namerec);
	}

	private PDOMMacro getMacroField(int offset) throws CoreException {
		long namerec = fLinkage.getDB().getRecPtr(fRecord + offset);
		return namerec != 0 ? new PDOMMacro(fLinkage, namerec) : null;
	}

	@Override
	public char[][] getParameterList() {
		if (fParameterList == UNINITIALIZED) {
			fParameterList = null;
			try {
				IString plist = getParamListInDB();
				if (plist != null) {
					List<char[]> paramList = new ArrayList<>();
					final char[] cplist = plist.getChars();
					final int end = cplist.length;
					int from = 0;
					int to = CharArrayUtils.indexOf(',', cplist, from, end);
					while (to > from) {
						paramList.add(CharArrayUtils.extract(cplist, from, to - from));
						from = to + 1;
						to = CharArrayUtils.indexOf(',', cplist, from, end);
					}
					fParameterList = paramList.toArray(new char[paramList.size()][]);
				}
			} catch (CoreException e) {
				CCorePlugin.log(e);
			}
		}
		return fParameterList;
	}

	public boolean isMacroDefinition() throws CoreException {
		if (fExpansion == UNINITIALIZED1) {
			return fLinkage.getDB().getRecPtr(fRecord + EXPANSION) != 0;
		}
		return fExpansion != null;
	}

	@Override
	public char[] getExpansionImage() {
		if (fExpansion == UNINITIALIZED1) {
			try {
				final IString expansionInDB = getExpansionInDB();
				fExpansion = expansionInDB == null ? null : expansionInDB.getChars();
			} catch (CoreException e) {
				CCorePlugin.log(e);
				fExpansion = CharArrayUtils.EMPTY;
			}
		}
		return fExpansion;
	}

	@Override
	public char[] getNameCharArray() {
		try {
			return getContainer().getNameCharArray();
		} catch (CoreException e) {
			CCorePlugin.log(e);
			return new char[] { ' ' };
		}
	}

	@Override
	public String getName() {
		return new String(getNameCharArray());
	}

	@Override
	public PDOMFile getFile() throws CoreException {
		long filerec = fLinkage.getDB().getRecPtr(fRecord + FILE);
		return filerec != 0 ? new PDOMFile(fLinkage, filerec) : null;
	}

	public long getFileRecord() throws CoreException {
		return fLinkage.getDB().getRecPtr(fRecord + FILE);
	}

	void setFile(PDOMFile file) throws CoreException {
		fLinkage.getDB().putRecPtr(fRecord + FILE, file != null ? file.getRecord() : 0);
	}

	@Override
	public IASTFileLocation getFileLocation() {
		try {
			IIndexFile file = getFile();
			if (file == null) {
				return null;
			}
			// We need to specify what this method can return to know
			// how to implement this. Existing implementations return
			// the absolute path, so here we attempt to do the same.
			IPath location = IndexLocationFactory.getAbsolutePath(file.getLocation());
			if (location == null) {
				return null;
			}
			String filename = location.toOSString();
			return new PDOMASTFileLocation(filename, getNodeOffset(), getNodeLength());
		} catch (CoreException e) {
			CCorePlugin.log(e);
		}
		return null;
	}

	public int getNodeLength() {
		try {
			return fLinkage.getDB().getShort(fRecord + NAME_LENGTH);
		} catch (CoreException e) {
			CCorePlugin.log(e);
			return 0;
		}
	}

	public int getNodeOffset() {
		try {
			return fLinkage.getDB().getInt(fRecord + NAME_OFFSET);
		} catch (CoreException e) {
			CCorePlugin.log(e);
			return 0;
		}
	}

	@Override
	public char[] getExpansion() {
		char[] expansionImage = getExpansionImage();
		return MacroDefinitionParser.getExpansion(new CharArray(expansionImage), 0, expansionImage.length);
	}

	@Override
	public char[][] getParameterPlaceholderList() {
		char[][] params = getParameterList();
		if (params != null && params.length > 0) {
			char[] lastParam = params[params.length - 1];
			if (CharArrayUtils.equals(lastParam, 0, Keywords.cpELLIPSIS.length, Keywords.cpELLIPSIS)) {
				char[][] result = new char[params.length][];
				System.arraycopy(params, 0, result, 0, params.length - 1);
				result[params.length - 1] = lastParam.length == Keywords.cpELLIPSIS.length ? Keywords.cVA_ARGS
						: CharArrayUtils.extract(lastParam, Keywords.cpELLIPSIS.length,
								lastParam.length - Keywords.cpELLIPSIS.length);
				return result;
			}
		}
		return params;
	}

	@Override
	public boolean isFunctionStyle() {
		return getParameterList() != null;
	}

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

	@Override
	public PDOMLinkage getLinkage() {
		return fLinkage;
	}

	@Override
	public IIndexScope getScope() {
		return null;
	}

	@Override
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Object getAdapter(Class adapter) {
		if (adapter.isAssignableFrom(PDOMMacro.class)) {
			return this;
		}
		return null;
	}

	@Override
	public IIndexFragmentName getDefinition() throws CoreException {
		if (!isMacroDefinition()) {
			return null;
		}
		if (fDefinition == null) {
			fDefinition = new PDOMMacroDefinitionName(this);
		}
		return fDefinition;
	}

	@Override
	public IIndexFile getLocalToFile() throws CoreException {
		return null;
	}

	@Override
	public String[] getQualifiedName() {
		return new String[] { getName() };
	}

	@Override
	public boolean isFileLocal() throws CoreException {
		return false;
	}

	@Override
	public int getBindingConstant() {
		return IIndexBindingConstants.MACRO_DEFINITION;
	}

	@Override
	public IIndexFragment getFragment() {
		return fLinkage.getPDOM();
	}

	@Override
	public boolean hasDeclaration() throws CoreException {
		return false;
	}

	@Override
	public boolean hasDefinition() throws CoreException {
		return true;
	}

	@Override
	public IIndexFragmentBinding getOwner() {
		return null;
	}

	@Override
	public void accept(IPDOMVisitor visitor) {
	}

	@Override
	public long getBindingID() {
		return fRecord;
	}
}
