/*******************************************************************************
 * Copyright (c) Sep 11, 2013 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.api.tools.internal.builder;

import java.util.ArrayList;
import java.util.Stack;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.pde.api.tools.internal.provisional.IApiJavadocTag;
import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem;

/**
 * Base class for validators
 *
 * @since 1.0.600
 */
public abstract class Validator extends ASTVisitor {

	static class Item {
		String typename;
		int flags;
		boolean visible = false;

		Item(String name, int flags, boolean vis) {
			typename = name;
			this.flags = flags;
			visible = vis;
		}
	}

	private boolean isvisible = true;
	/**
	 * Type stack that tracks types as the visitor descends
	 */
	private final Stack<Item> fStack = new Stack<>();
	/**
	 * The compilation unit we are scanning
	 */
	protected ICompilationUnit fCompilationUnit = null;
	/**
	 * Backing collection of problems, if any
	 */
	private ArrayList<IApiProblem> fProblems = null;

	@Override
	public boolean visit(AnnotationTypeDeclaration node) {
		isvisible &= !Flags.isPrivate(node.getModifiers());
		fStack.push(new Item(getTypeName(node), node.getModifiers(), isvisible));
		return true;
	}

	@Override
	public void endVisit(AnnotationTypeDeclaration node) {
		fStack.pop();
		if (!fStack.isEmpty()) {
			Item item = fStack.peek();
			isvisible = item.visible;
		}
	}

	@Override
	public boolean visit(TypeDeclaration node) {
		int flags = node.getModifiers();
		if (Flags.isPrivate(flags)) {
			isvisible &= false;
		} else {
			if (node.isMemberTypeDeclaration()) {
				isvisible &= (Flags.isPublic(flags) && Flags.isStatic(flags)) || Flags.isPublic(flags) || Flags.isProtected(flags) || node.isInterface();
			} else {
				isvisible &= (!Flags.isPrivate(flags) && !Flags.isPackageDefault(flags)) || node.isInterface();
			}
		}
		fStack.push(new Item(getTypeName(node), node.getModifiers(), isvisible));
		return true;
	}

	@Override
	public void endVisit(TypeDeclaration node) {
		fStack.pop();
		if (!fStack.isEmpty()) {
			Item item = fStack.peek();
			isvisible = item.visible;
		}
	}

	@Override
	public boolean visit(EnumDeclaration node) {
		int flags = node.getModifiers();
		if (node.isMemberTypeDeclaration()) {
			isvisible &= Flags.isPublic(flags);
		} else {
			isvisible &= !Flags.isPrivate(flags) && !Flags.isPackageDefault(flags);
		}
		fStack.push(new Item(getTypeName(node), node.getModifiers(), isvisible));
		return true;
	}

	@Override
	public void endVisit(EnumDeclaration node) {
		fStack.pop();
		if (!fStack.isEmpty()) {
			Item item = fStack.peek();
			isvisible = item.visible;
		}
	}

	@Override
	public void endVisit(CompilationUnit node) {
		fStack.clear();
	}

	/**
	 * Returns the next item on the tip of the stack
	 *
	 * @return the next {@link Item}
	 */
	protected Item getItem() {
		return fStack.peek();
	}

	/**
	 * Returns the fully qualified name of the enclosing type for the given node
	 *
	 * @return the fully qualified name of the enclosing type
	 */
	protected String getTypeName(ASTNode node) {
		return getTypeName(node, new StringBuilder());
	}

	/**
	 * Constructs the qualified name of the enclosing parent type
	 *
	 * @param node the node to get the parent name for
	 * @param buffer the buffer to write the name into
	 * @return the fully qualified name of the parent
	 */
	protected String getTypeName(ASTNode node, StringBuilder buffer) {
		if (node.getNodeType() == ASTNode.COMPILATION_UNIT) {
			CompilationUnit unit = (CompilationUnit) node;
			PackageDeclaration packageDeclaration = unit.getPackage();
			if (packageDeclaration != null) {
				buffer.insert(0, '.');
				buffer.insert(0, packageDeclaration.getName().getFullyQualifiedName());
			}
			return buffer.toString();
		} else {
			if (node instanceof AbstractTypeDeclaration typeDeclaration) {
				if (typeDeclaration.isPackageMemberTypeDeclaration()) {
					buffer.insert(0, typeDeclaration.getName().getIdentifier());
				} else {
					buffer.insert(0, typeDeclaration.getName().getFullyQualifiedName());
					buffer.insert(0, '$');
				}
			}
			return getTypeName(node.getParent(), buffer);
		}
	}

	/**
	 * Returns the {@link IApiJavadocTag} kind of the parent {@link ASTNode} for
	 * the given node or -1 if the parent is not found or not a
	 * {@link TypeDeclaration}
	 *
	 * @return the {@link IApiJavadocTag} kind of the parent or -1
	 */
	protected int getParentKind(ASTNode node) {
		if (node == null) {
			return -1;
		}
		if (node instanceof TypeDeclaration) {
			return ((TypeDeclaration) node).isInterface() ? IApiJavadocTag.TYPE_INTERFACE : IApiJavadocTag.TYPE_CLASS;
		} else if (node instanceof AnnotationTypeDeclaration) {
			return IApiJavadocTag.TYPE_ANNOTATION;
		} else if (node instanceof EnumDeclaration) {
			return IApiJavadocTag.TYPE_ENUM;
		}
		return getParentKind(node.getParent());
	}

	/**
	 * Returns the modifiers from the smallest enclosing type containing the
	 * given node
	 *
	 * @return the modifiers for the smallest enclosing type or 0
	 */
	protected int getParentModifiers(ASTNode node) {
		if (node == null) {
			return 0;
		}
		if (node instanceof AbstractTypeDeclaration type) {
			return type.getModifiers();
		}
		return getParentModifiers(node.getParent());
	}

	/**
	 * Adds a found problem to the listing
	 */
	protected void addProblem(IApiProblem problem) {
		if (fProblems == null) {
			fProblems = new ArrayList<>();
		}
		fProblems.add(problem);
	}

	/**
	 * Returns the complete listing of API annotation problems found during the
	 * scan or an empty array, never <code>null</code>
	 *
	 * @return the complete listing of API annotation problems found
	 */
	public IApiProblem[] getProblems() {
		if (fProblems == null) {
			return new IApiProblem[0];
		}
		return fProblems.toArray(new IApiProblem[fProblems.size()]);
	}
}
