/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.plugin.ij.lang.psi.impl;
import com.intellij.extapi.psi.StubBasedPsiElementBase;
import com.intellij.lang.ASTNode;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.CheckUtil;
import com.intellij.psi.impl.light.LightClassReference;
import com.intellij.psi.impl.source.JavaDummyHolder;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
import com.intellij.psi.impl.source.tree.ChangeUtil;
import com.intellij.psi.impl.source.tree.CompositeElement;
import com.intellij.psi.impl.source.tree.FileElement;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.stubs.IStubElementType;
import com.intellij.psi.stubs.StubElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlText;
import com.intellij.util.IncorrectOperationException;
import gw.lang.parser.IParseTree;
import gw.lang.parser.IParsedElement;
import gw.lang.parser.statements.IClassFileStatement;
import gw.lang.reflect.IBlockType;
import gw.lang.reflect.IMetaType;
import gw.lang.reflect.IType;
import gw.lang.reflect.TypeSystem;
import gw.lang.reflect.gs.IGosuClass;
import gw.lang.reflect.module.IModule;
import gw.plugin.ij.lang.GosuTokenSets;
import gw.plugin.ij.lang.IGosuElementType;
import gw.plugin.ij.lang.parser.GosuCompositeElement;
import gw.plugin.ij.lang.psi.IGosuPsiElement;
import gw.plugin.ij.lang.psi.api.expressions.IGosuIdentifier;
import gw.plugin.ij.lang.psi.api.statements.IGosuStatement;
import gw.plugin.ij.lang.psi.impl.expressions.GosuIdentifierImpl;
import gw.plugin.ij.lang.psi.impl.resolvers.PsiTypeResolver;
import gw.plugin.ij.lang.psi.util.GosuPsiParseUtil;
import gw.plugin.ij.util.ExceptionUtil;
import gw.plugin.ij.util.GosuModuleUtil;
import gw.plugin.ij.util.InjectedElementEditor;
import gw.plugin.ij.util.JavaPsiFacadeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.getFirst;
public class GosuBaseElementImpl<E extends IParsedElement, T extends StubElement> extends StubBasedPsiElementBase<T> implements IGosuPsiElement {
private final IElementType _elementType;
public GosuBaseElementImpl(@NotNull GosuCompositeElement node) {
super(node);
_elementType = node.getElementType();
}
protected GosuBaseElementImpl(@NotNull T stub, @NotNull IStubElementType elementType) {
super(stub, elementType);
_elementType = elementType;
}
public IElementType getGosuElementType() {
return _elementType;
}
@Nullable
public PsiIdentifier getNameIdentifierImpl() {
final List<ASTNode> children = Arrays.asList(getNode().getChildren(null));
PsiIdentifier id = getFirst(filter(children, IGosuIdentifier.class), null);
if (id == null) {
// Look for a generic identifier to handle cases where we fudge the psi tree e.g., program's synthetic top-level class
id = findElement(this, PsiIdentifier.class);
}
return id;
}
@Nullable
public static PsiElement findElement(@NotNull PsiElement parent, IElementType elementType) {
if (parent.getNode().getElementType() == elementType) {
return parent;
}
PsiElement child = parent.getFirstChild();
while (child != null) {
PsiElement elem = findElement(child, elementType);
if (elem != null) {
return elem;
}
child = child.getNextSibling();
}
return null;
}
@Nullable
public static <P extends PsiElement, C extends Class<P>> P findElement(@NotNull PsiElement parent, @NotNull C classType) {
if (classType.isAssignableFrom(parent.getNode().getClass())) {
return (P) parent;
}
PsiElement child = parent.getFirstChild();
while (child != null) {
final PsiElement elem = findElement(child, classType);
if (elem != null) {
return (P) elem;
}
child = child.getNextSibling();
}
return null;
}
public static void findElements(@NotNull PsiElement parent, IElementType elementType, List<PsiElement> list) {
if (parent.getNode().getElementType() == elementType) {
list.add(parent);
}
PsiElement child = parent.getFirstChild();
while (child != null) {
findElements(child, elementType, list);
child = child.getNextSibling();
}
}
public void removeElements(@NotNull PsiElement[] elements) throws IncorrectOperationException {
ASTNode parentNode = getNode();
for (PsiElement element : elements) {
if (element.isValid()) {
ASTNode node = element.getNode();
if (node == null || node.getTreeParent() != parentNode) {
throw new IncorrectOperationException();
}
parentNode.removeChild(node);
}
}
}
public void removeStatement() throws IncorrectOperationException {
if (getParent() == null ||
getParent().getNode() == null) {
throw new IncorrectOperationException();
}
ASTNode parentNode = getParent().getNode();
ASTNode prevNode = getNode().getTreePrev();
parentNode.removeChild(this.getNode());
if (prevNode != null && GosuTokenSets.SEPARATORS.contains(prevNode.getElementType())) {
parentNode.removeChild(prevNode);
}
}
@Override
public PsiElement getParent() {
return getParentByStub();
}
@NotNull
public <T extends IGosuStatement> T replaceWithStatement(@NotNull T newStmt) {
PsiElement parent = getParent();
if (parent == null) {
throw new PsiInvalidElementAccessException(this);
}
return (T) replace(newStmt);
}
public void accept(@NotNull GosuElementVisitor visitor) {
visitor.visitElement(this);
}
public void acceptChildren(GosuElementVisitor visitor) {
PsiElement child = getFirstChild();
while (child != null) {
if (child instanceof IGosuPsiElement) {
((IGosuPsiElement) child).accept(visitor);
}
child = child.getNextSibling();
}
}
@Nullable
public PsiElement replace(@NotNull PsiElement newElement) throws IncorrectOperationException {
CompositeElement treeElement = calcTreeElement();
assert treeElement.getTreeParent() != null;
CheckUtil.checkWritable(this);
TreeElement elementCopy = ChangeUtil.copyToElement(newElement);
treeElement.getTreeParent().replaceChildInternal(treeElement, elementCopy);
elementCopy = ChangeUtil.decodeInformation(elementCopy);
PsiElement elem = SourceTreeToPsiMap.treeElementToPsi(elementCopy);
if (elem.getContainingFile() instanceof AbstractGosuClassFileImpl) {
((AbstractGosuClassFileImpl) elem.getContainingFile()).reparseGosuFromPsi();
}
return elem;
}
@NotNull
protected CompositeElement calcTreeElement() {
return getNode();
}
@NotNull
public GosuCompositeElement getNode() {
return (GosuCompositeElement) super.getNode();
}
@Nullable
public E getParsedElement() {
return (E) getParsedElementImpl();
}
@Nullable
public IParsedElement getParsedElementImpl() {
GosuCompositeElement node = getNode();
final FileElement fileElement = TreeUtil.getFileElement(node);
if (fileElement == null) {
throw new RuntimeException("No file element found");
}
final PsiElement plainPsiFile = fileElement.getPsi();
if (plainPsiFile instanceof JavaDummyHolder) {
return null;
}
final AbstractGosuClassFileImpl psiFile = (AbstractGosuClassFileImpl) plainPsiFile;
final GosuClassParseData parseData = psiFile.getParseData();
final String fileSource = GosuPsiImplUtil.getFileSource(psiFile);
String parsedSource = parseData.getSource();
if (!fileSource.equals(parsedSource)) {
psiFile.reparseGosuFromPsi(); // In the case of obsolete parse results
parsedSource = parseData.getSource();
}
final IClassFileStatement classFileStatement = parseData.getClassFileStatement();
if (classFileStatement == null) {
if (!GosuPsiParseUtil.TRANSIENT_PROGRAM.equals(psiFile.getName())) {
throw new RuntimeException("No latest parse information found for " + psiFile.getName());
}
} else {
verifyFileSource(fileSource, parsedSource);
final PsiLanguageInjectionHost host = InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile);
TextRange range = node.getTextRange();
boolean skip = false;
if (host instanceof XmlAttributeValue) {
String hostText = host.getText();
String uAttribute = parsedSource;
String uFrag = InjectedLanguageManager.getInstance(psiFile.getProject()).getUnescapedText(node.getPsi());
String eFrag = node.getText();
String eAttribute = hostText.substring(1, hostText.length() - 1);
TextRange newRange = adjustRange(uAttribute, uFrag, eAttribute, eFrag, range);
if (!range.equals(newRange)) {
skip = true;
range = newRange;
}
}
if (host instanceof XmlText) {
String hostText = host.getText();
String uAttribute = parsedSource;
String uFrag = InjectedLanguageManager.getInstance(psiFile.getProject()).getUnescapedText(node.getPsi());
String eFrag = node.getText();
String eAttribute = hostText.replace("<![CDATA[", "").replace("]]>", "");
TextRange newRange = adjustRange(uAttribute, uFrag, eAttribute, eFrag, range);
if (!range.equals(newRange)) {
skip = true;
range = newRange;
}
}
final IParseTree location = classFileStatement.getLocation().getMatchingElement(range.getStartOffset(), range.getLength());
if (location != null) {
final IParsedElement element = getTheRightOne(location);
if (element != null) {
if (!skip) {
verifyPsiText(parsedSource, location);
}
return element;
}
}
}
return null;
}
private TextRange adjustRange(String uAttribute, String uFrag, String eAttribute, String eFrag, TextRange eRange) {
if (uAttribute.equals(eAttribute) || eRange.getLength() == 0) {
return eRange;
}
int num = 0;
int i = 0;
//count how many times we meet eFrag in eAttribute before eRange.startOffset
while(true) {
i = eAttribute.indexOf(eFrag, i);
num++;
if (i == -1 || i == eRange.getStartOffset()) {
break;
}
i += eFrag.length();
}
int j = 0;
i = 0;
while(j < num) {
i = uAttribute.indexOf(uFrag, i);
i += uFrag.length();
j++;
}
return new TextRange(i-uFrag.length(), i);
}
@Nullable
private E getTheRightOne(@NotNull IParseTree location) {
final Class<? extends IParsedElement> peType = getParsedElementType();
final IParsedElement parsedElement = location.getParsedElement();
if (peType == null || peType.isAssignableFrom(parsedElement.getClass())) {
return (E) parsedElement;
}
// Drill down, checking the first child until we find the element that matches the footprint and is the same type
List<IParseTree> children = location.getChildren();
while (!children.isEmpty()) {
final IParseTree child = children.get(0);
if (child.getOffset() == location.getOffset() && child.getExtent() == location.getExtent()) {
final IParsedElement pe = child.getParsedElement();
if (peType.isAssignableFrom(pe.getClass())) {
return (E) pe;
} else {
children = child.getChildren();
}
} else {
return null;
}
}
return (E) parsedElement;
}
private Class<? extends IParsedElement> getParsedElementType() {
if (_elementType instanceof IGosuElementType) {
return ((IGosuElementType) _elementType).getParsedElementType();
}
throw new RuntimeException("Unknown element type: " + _elementType.getClass().getName());
}
private void verifyFileSource(@NotNull String fileSource, String parsedSource) {
if (!fileSource.equals(parsedSource)) {
ExceptionUtil.showDiffWindow(fileSource, parsedSource, "File Source", "Parsed Source", "File source does not match parsed source", getProject());
}
}
private void verifyPsiText(@NotNull String parsedSource, @NotNull IParseTree location) {
final String psiText = getText();
final String parsedText = parsedSource.substring(location.getOffset(), location.getExtent() + 1);
if (!psiText.equals(parsedText)) {
ExceptionUtil.showDiffWindow(psiText, parsedText, "Psi Text", "Parsed Text", "Psi text does not match parsed text", getProject());
}
}
private IGosuClass getTopLevelClass(@NotNull IParsedElement pe) {
IGosuClass gsClass = pe.getGosuClass();
while (gsClass.getEnclosingType() != null) {
gsClass = (IGosuClass) gsClass.getEnclosingType();
}
return gsClass;
}
@NotNull
private T findCorrespondingElementInClassFileStmt(IClassFileStatement cfs, @NotNull IParsedElement pe, @NotNull List<Integer> path) {
if (pe instanceof IClassFileStatement) {
IParsedElement csr = cfs;
for (Integer i : path) {
csr = csr.getLocation().getChildren().get(i).getParsedElement();
}
return (T) csr;
}
List<IParseTree> children = pe.getParent().getLocation().getChildren();
path.add(0, children.indexOf(pe.getLocation()));
return findCorrespondingElementInClassFileStmt(cfs, pe.getParent(), path);
}
public String toString() {
Class<? extends IParsedElement> elemType = ((IGosuElementType) _elementType).getParsedElementType();
return elemType == null ? "Unhandled" : elemType.getSimpleName();
}
@Nullable
public PsiType createType(IType type) {
return createType(type, this);
}
@Nullable
public static PsiType createType(@Nullable IType type, @Nullable PsiElement context) {
if (type == null) {
return null;
}
if( context == null ) {
return null;
}
if (type instanceof IMetaType) {
type = ((IMetaType) type).getType();
}
final PsiElementFactory factory = JavaPsiFacadeUtil.getElementFactory(context.getProject());
IModule mod = GosuModuleUtil.findModuleForPsiElement( context );
TypeSystem.pushModule( mod );
try {
if (type.getName().equals(TypeSystem.getDefaultType().getType().getName())) {
final PsiElement psiClass = PsiTypeResolver.resolveType(type, context);
return psiClass instanceof PsiClass ? factory.createType((PsiClass) psiClass) : null;
}
if (type.isPrimitive()) {
return factory.createPrimitiveType(type.getName());
}
if (type.isArray()) {
return new PsiArrayType(createType(type.getComponentType(), context));
}
if (type.isParameterizedType()) {
final IType[] parameters = type.getTypeParameters();
final PsiType[] psiParameters = new PsiType[parameters.length];
for (int i = 0; i < parameters.length; i++) {
psiParameters[i] = createType(parameters[i], context);
}
final PsiElement psiClass = PsiTypeResolver.resolveType(type.getGenericType(), context);
return psiClass instanceof PsiClass ? factory.createType((PsiClass) psiClass, psiParameters) : null;
}
final String qualifiedName = type.getName();
final GlobalSearchScope scope = GlobalSearchScope.allScope(context.getProject());
if (type instanceof IBlockType) {
GosuBlockLightClassReference reference = new GosuBlockLightClassReference(context.getManager(), qualifiedName, qualifiedName, scope);
return new GosuPsiClassReferenceType(reference, null);
}
if (context instanceof PsiIdentifier) {
final String shortName = PsiNameHelper.getShortClassName(qualifiedName);
GosuLightClassReference referenceElement = new GosuLightClassReference(
context.getManager(), shortName, qualifiedName, scope, (GosuIdentifierImpl) context);
return new GosuPsiClassReferenceType(referenceElement, null);
} else {
final String shortName = PsiNameHelper.getShortClassName(qualifiedName);
LightClassReference reference = new LightClassReference(context.getManager(), shortName, qualifiedName, scope);
return new GosuPsiClassReferenceType(reference, null);
}
}
finally {
TypeSystem.popModule( mod );
}
}
@NotNull
@Override
public PsiElement getNavigationElement() {
return InjectedElementEditor.getOriginalElement(this);
}
}