/*******************************************************************************
 * Copyright (c) 2009, 2015 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:
 * 	   Sergey Prigogin (Google) - initial API and implementation
 *	   Markus Schorn (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.internal.core.dom.parser.cpp.semantics;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.ICompositeType;
import org.eclipse.cdt.core.dom.ast.IProblemBinding;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPAliasTemplateInstance;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPBase;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassScope;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassTemplatePartialSpecialization;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPFunction;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMember;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPScope;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPSpecialization;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPTemplateParameter;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPUsingDeclaration;
import org.eclipse.cdt.core.parser.util.ArrayUtil;
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPClosureType;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ClassTypeHelper;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPDeferredClassInstance;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPInternalUnknownScope;
import org.eclipse.cdt.internal.core.dom.parser.cpp.ICPPUnknownType;

/**
 * The context that determines access to private and protected class members.
 */
public class AccessContext {
	private static final int v_private = ICPPMember.v_private;
	private static final int v_protected = ICPPMember.v_protected;
	public static final int v_public = ICPPMember.v_public;

	/**
	 * Checks if a binding is accessible from a given name.
	 *
	 * @param binding  A binding to check access for.
	 * @param from A name corresponding to the binding.
	 * @return {@code true} if the binding is accessible.
	 */
	public static boolean isAccessible(IBinding binding, IASTName from) {
		CPPSemantics.pushLookupPoint(from);
		try {
			return new AccessContext(from).isAccessible(binding);
		} finally {
			CPPSemantics.popLookupPoint();
		}
	}

	/**
	 * Checks if a binding is accessible from a given name.
	 *
	 * @param binding  A binding to check access for.
	 * @param bindingVisibility visibility of the binding in the containing composite type.
	 *     Used instead of calling {@link ICPPMember#getVisibility()}.
	 * @param from A name corresponding to the binding.
	 * @return {@code true} if the binding is accessible.
	 */
	public static boolean isAccessible(IBinding binding, int bindingVisibility, IASTName from) {
		CPPSemantics.pushLookupPoint(from);
		try {
			return new AccessContext(from).isAccessible(binding, bindingVisibility);
		} finally {
			CPPSemantics.popLookupPoint();
		}
	}

	private final IASTName name;
	/**
	 * A chain of nested classes or/and a function that determine accessibility of private/protected
	 * members by participating in friendship or class inheritance relationships. If both, classes
	 * and a function are present in the context, the outermost class has to be local to
	 * the function.
	 * {@link "http://www.open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#45"}
	 */
	private IBinding[] context;
	/**
	 * A class through which the bindings are accessed (11.2.4).
	 */
	private boolean isUnqualifiedLookup;
	private boolean isPrefixLookup;
	private ICPPClassType namingClass; // Depends on the binding for which we check the access.
	// The first candidate is independent of the binding for which we do the access-check.
	private ICPPClassType firstCandidateForNamingClass;
	private DOMException initializationException;

	public AccessContext(IASTName name) {
		this(name, false);
	}

	public AccessContext(IASTName name, boolean prefixLookup) {
		this.name = name;
		this.isPrefixLookup = prefixLookup;
	}

	/**
	 * Checks if a binding is accessible in a given context.
	 *
	 * @param binding A binding to check access for.
	 * @return {@code true} if the binding is accessible.
	 */
	public boolean isAccessible(IBinding binding) {
		if (binding instanceof ICPPTemplateParameter)
			return true;

		int bindingVisibility;
		if (binding instanceof ICPPMember) {
			bindingVisibility = ((ICPPMember) binding).getVisibility();
		} else {
			while (binding instanceof ICPPSpecialization) {
				binding = ((ICPPSpecialization) binding).getSpecializedBinding();
			}
			if (binding instanceof ICPPClassTemplatePartialSpecialization) {
				// A class template partial specialization requires its primary
				// template to be visible
				if (!isAccessible(((ICPPClassTemplatePartialSpecialization) binding).getPrimaryClassTemplate()))
					return false;
			}
			if (binding instanceof ICPPAliasTemplateInstance) {
				binding = ((ICPPAliasTemplateInstance) binding).getTemplateDefinition();
			}
			IBinding owner = binding.getOwner();
			if (owner instanceof CPPClosureType)
				return true;
			if (owner instanceof ICPPClassType) {
				bindingVisibility = ((ICPPClassType) owner).getVisibility(binding);
			} else {
				bindingVisibility = v_public;
			}
		}
		return isAccessible(binding, bindingVisibility);
	}

	/**
	 * Checks if a binding is accessible in a given context.
	 *
	 * @param binding A binding to check access for.
	 * @param bindingVisibility visibility of the binding in the containing composite type.
	 *     Used instead of calling {@link ICPPMember#getVisibility()}.
	 * @return {@code true} if the binding is accessible.
	 */
	public boolean isAccessible(IBinding binding, int bindingVisibility) {
		IBinding owner;
		while ((owner = binding.getOwner()) instanceof ICompositeType && ((ICompositeType) owner).isAnonymous()) {
			binding = owner;
		}
		if (!(owner instanceof ICPPClassType)) {
			return true; // The binding is not a class member.
		}
		if (!initialize()) {
			return true; // Assume visibility if anything goes wrong.
		}
		ICPPClassType accessOwner = (ICPPClassType) owner;
		namingClass = getNamingClass(accessOwner);
		if (namingClass == null) {
			return true;
		}
		return isAccessible(binding, bindingVisibility, accessOwner, namingClass, v_public, 0);
	}

	/**
	 * @return {@code true} if initialization succeeded.
	 */
	private boolean initialize() {
		if (context == null) {
			if (initializationException != null) {
				return false;
			}
			try {
				context = getContext(name);
				firstCandidateForNamingClass = getFirstCandidateForNamingClass(name);
			} catch (DOMException e) {
				CCorePlugin.log(e);
				initializationException = e;
				return false;
			}
		}
		return true;
	}

	// Return true if 'c' is the same type as 'target', or a specialization of 'target'.
	private static boolean isSameTypeOrSpecialization(ICPPClassType c, ICPPClassType target) {
		if (!(c instanceof ICPPSpecialization)) {
			while (target instanceof ICPPSpecialization) {
				IBinding specialized = ((ICPPSpecialization) target).getSpecializedBinding();
				if (specialized instanceof ICPPClassType) {
					target = (ICPPClassType) specialized;
				}
			}
		}
		return c.isSameType(target);
	}

	private boolean isAccessible(IBinding binding, int bindingVisibility, ICPPClassType owner,
			ICPPClassType derivedClass, int accessLevel, int depth) {
		if (depth > CPPSemantics.MAX_INHERITANCE_DEPTH)
			return false;

		accessLevel = getMemberAccessLevel(derivedClass, accessLevel);

		if (isSameTypeOrSpecialization(owner, derivedClass)) {
			return isAccessible(bindingVisibility, accessLevel);
		}

		ICPPUsingDeclaration[] usingDecls = derivedClass.getUsingDeclarations();
		for (ICPPUsingDeclaration decl : usingDecls) {
			for (IBinding delegate : decl.getDelegates()) {
				if (delegate.equals(binding)) {
					derivedClass.getVisibility(decl);
					return isAccessible(derivedClass.getVisibility(decl), accessLevel);
				}
			}
		}

		ICPPBase[] bases = derivedClass.getBases();
		if (bases != null) {
			for (ICPPBase base : bases) {
				IBinding baseBinding = base.getBaseClass();
				if (baseBinding instanceof ICPPDeferredClassInstance) {
					// Support content assist for members of deferred instances.
					baseBinding = ((ICPPDeferredClassInstance) baseBinding).getTemplateDefinition();
				}
				if (!(baseBinding instanceof ICPPClassType)) {
					continue;
				}
				if (!isAccessible(base.getVisibility(), accessLevel)) {
					continue;
				}
				if (isAccessible(binding, bindingVisibility, owner, (ICPPClassType) baseBinding,
						accessLevel == v_private ? v_protected : accessLevel, depth + 1)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Returns access level to the members of a class.
	 *
	 * @param classType A class
	 * @param inheritedAccessLevel Access level inherited from derived class.
	 *     One of: v_public, v_protected, v_private.
	 * @return One of: v_public, v_protected, v_private.
	 */
	private int getMemberAccessLevel(ICPPClassType classType, int inheritedAccessLevel) {
		int accessLevel = inheritedAccessLevel;
		for (IBinding contextBinding : context) {
			if (ClassTypeHelper.isFriend(contextBinding, classType))
				return v_private;

			if (accessLevel == v_public && contextBinding instanceof ICPPClassType) {
				ICPPClassType contextClass = (ICPPClassType) contextBinding;
				if (isAccessibleBaseClass(classType, contextClass, 0))
					accessLevel = v_protected;
			}
		}
		return accessLevel;
	}

	private boolean isAccessibleBaseClass(ICPPClassType classType, ICPPClassType derived, int depth) {
		if (depth > CPPSemantics.MAX_INHERITANCE_DEPTH)
			return false;

		if (derived.isSameType(classType))
			return true;

		ICPPBase[] bases = derived.getBases();
		if (bases != null) {
			for (ICPPBase base : bases) {
				IBinding baseClass = base.getBaseClass();
				if (baseClass instanceof ICPPDeferredClassInstance) {
					baseClass = ((ICPPDeferredClassInstance) baseClass).getTemplateDefinition();
				}
				if (!(baseClass instanceof ICPPClassType)) {
					continue;
				}
				if (depth > 0 && !isAccessible(base.getVisibility(), v_protected)) {
					continue;
				}
				if (isAccessibleBaseClass(classType, (ICPPClassType) baseClass, depth + 1)) {
					return true;
				}
			}
		}
		return false;
	}

	private ICPPClassType getFirstCandidateForNamingClass(IASTName name) throws DOMException {
		LookupData data = new LookupData(name);
		isUnqualifiedLookup = !data.qualified;

		ICPPScope scope = CPPSemantics.getLookupScope(name);
		while (scope != null && !(scope instanceof ICPPClassScope)) {
			if (scope instanceof ICPPInternalUnknownScope) {
				IType scopeType = ((ICPPInternalUnknownScope) scope).getScopeType();
				if (scopeType instanceof ICPPUnknownType && isPrefixLookup) {
					scopeType = HeuristicResolver.resolveUnknownType((ICPPUnknownType) scopeType);
					if (scopeType instanceof ICPPClassType) {
						return (ICPPClassType) scopeType;
					}
				}
				if (scopeType instanceof ICPPDeferredClassInstance) {
					return ((ICPPDeferredClassInstance) scopeType).getClassTemplate();
				}
			}

			scope = CPPSemantics.getParentScope(scope, data.getTranslationUnit());
		}
		if (scope instanceof ICPPClassScope) {
			return ((ICPPClassScope) scope).getClassType();
		}
		return null;
	}

	private ICPPClassType getNamingClass(ICPPClassType accessOwner) {
		ICPPClassType classType = firstCandidateForNamingClass;
		if (classType != null && isUnqualifiedLookup) {
			IBinding owner = classType.getOwner();
			while (owner instanceof ICPPClassType
					&& !derivesFrom(classType, accessOwner, name, CPPSemantics.MAX_INHERITANCE_DEPTH)) {
				classType = (ICPPClassType) owner;
				owner = classType.getOwner();
			}
		}
		return classType;
	}

	private static boolean derivesFrom(ICPPClassType derived, ICPPClassType target, IASTNode point, int maxdepth) {
		if (derived == target || derived.isSameType(target)) {
			return true;
		}
		if (maxdepth > 0) {
			for (ICPPBase cppBase : derived.getBases()) {
				IBinding base = cppBase.getBaseClass();
				if (!(target instanceof ICPPSpecialization)) {
					while (base instanceof ICPPSpecialization) {
						base = ((ICPPSpecialization) base).getSpecializedBinding();
					}
				}
				if (base instanceof ICPPClassType) {
					ICPPClassType tbase = (ICPPClassType) base;
					if (tbase.isSameType(target)) {
						return true;
					}
					if (derivesFrom(tbase, target, point, maxdepth - 1))
						return true;
				}
			}
		}
		return false;
	}

	private static IBinding[] getContext(IASTName name) {
		IBinding[] accessibilityContext = IBinding.EMPTY_BINDING_ARRAY;
		for (IBinding binding = CPPVisitor.findEnclosingFunctionOrClass(name); binding != null; binding = binding
				.getOwner()) {
			if (binding instanceof ICPPMethod ||
			// Definition of an undeclared method.
					binding instanceof IProblemBinding && ((IProblemBinding) binding)
							.getID() == IProblemBinding.SEMANTIC_MEMBER_DECLARATION_NOT_FOUND) {
				continue;
			}
			if (binding instanceof ICPPFunction || binding instanceof ICPPClassType) {
				accessibilityContext = ArrayUtil.append(accessibilityContext, binding);
			}
		}
		return ArrayUtil.trim(accessibilityContext);
	}

	/**
	 * Checks if objects with the given visibility are accessible at the given access level.
	 *
	 * @param visibility one of: v_public, v_protected, v_private.
	 * @param accessLevel one of: v_public, v_protected, v_private.
	 * @return {@code true} if the access level is sufficiently high.
	 */
	private static boolean isAccessible(int visibility, int accessLevel) {
		// Note the ordering of numeric visibility values: v_public < v_protected < v_private.
		return accessLevel >= visibility;
	}
}
