package com.siberika.idea.pascal.lang;
import com.intellij.lang.ImportOptimizer;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.SmartList;
import com.siberika.idea.pascal.PascalBundle;
import com.siberika.idea.pascal.PascalFileType;
import com.siberika.idea.pascal.lang.parser.PascalFile;
import com.siberika.idea.pascal.lang.psi.PasLibraryModuleHead;
import com.siberika.idea.pascal.lang.psi.PasModule;
import com.siberika.idea.pascal.lang.psi.PasPackageModuleHead;
import com.siberika.idea.pascal.lang.psi.PasProgramModuleHead;
import com.siberika.idea.pascal.lang.psi.PasUsesClause;
import com.siberika.idea.pascal.lang.psi.PascalNamedElement;
import com.siberika.idea.pascal.lang.psi.PascalQualifiedIdent;
import com.siberika.idea.pascal.lang.psi.impl.PascalModule;
import com.siberika.idea.pascal.lang.psi.impl.PascalModuleImpl;
import com.siberika.idea.pascal.lang.references.PasReferenceUtil;
import com.siberika.idea.pascal.util.DocUtil;
import com.siberika.idea.pascal.util.PsiUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
/**
* Author: George Bakhtadze
* Date: 17/12/2015
*/
public class PascalImportOptimizer implements ImportOptimizer {
private static final Logger LOG = Logger.getInstance(PascalImportOptimizer.class);
private static final Pattern RE_UNITNAME_PREFIX = Pattern.compile("[{}!]");
public static final List<String> EXCLUDED_UNITS = Arrays.asList("CMEM", "HEAPTRC", "CTHREADS", "CWSTRING", "FASTMM4");
static boolean isExcludedFromCheck(PascalQualifiedIdent usedUnitName) {
if (EXCLUDED_UNITS.contains(usedUnitName.getName().toUpperCase())) {
return true;
}
PsiElement prev = usedUnitName.getPrevSibling();
return (prev instanceof PsiComment) && "{!}".equals(prev.getText());
}
public static UsedUnitStatus getUsedUnitStatus(PascalQualifiedIdent usedUnitName, Module module) {
PascalModuleImpl mod = (PascalModuleImpl) PasReferenceUtil.findUnit(usedUnitName.getProject(),
PasReferenceUtil.findUnitFiles(usedUnitName.getProject(), module), usedUnitName.getName());
if (null == mod) {
return UsedUnitStatus.UNKNOWN;
}
UsedUnitStatus res = UsedUnitStatus.USED;
if (isExcludedFromCheck(usedUnitName)) {
return res;
}
PascalModule pasModule = PsiUtil.getElementPasModule(usedUnitName);
if ((pasModule != null)) {
Pair<List<PascalNamedElement>, List<PascalNamedElement>> idents = pasModule.getIdentsFrom(usedUnitName.getName());
if (PsiUtil.belongsToInterface(usedUnitName)) {
if (idents.getFirst().size() + idents.getSecond().size() == 0) {
res = UsedUnitStatus.UNUSED;
} else if (idents.getFirst().size() == 0) {
res = UsedUnitStatus.USED_IN_IMPL;
}
} else if (idents.getSecond().size() == 0) {
res = UsedUnitStatus.UNUSED;
}
}
return res;
}
@Override
public boolean supports(PsiFile file) {
return supportsOptimization(file);
}
public static boolean supportsOptimization(PsiFile file) {
return (file instanceof PascalFile) && (file.getFileType() == PascalFileType.INSTANCE);
}
@NotNull
@Override
public Runnable processFile(final PsiFile file) {
return doProcess(file);
}
public static Runnable doProcess(final PsiFile file) {
final Map<PascalQualifiedIdent, UsedUnitStatus> units = new TreeMap<PascalQualifiedIdent, UsedUnitStatus>(new ByOffsetComparator<PascalQualifiedIdent>());
Collection<PasUsesClause> usesClauses = PsiTreeUtil.findChildrenOfType(file, PasUsesClause.class);
//noinspection unchecked
Module module = ModuleUtilCore.findModuleForPsiElement(file);
for (PascalQualifiedIdent usedUnitName : PsiUtil.findChildrenOfAnyType(PsiUtil.getElementPasModule(file), PascalQualifiedIdent.class)) {
if (PsiUtil.isUsedUnitName(usedUnitName)) {
UsedUnitStatus status = PascalImportOptimizer.getUsedUnitStatus(usedUnitName, module);
if (status != UsedUnitStatus.USED) {
units.put(usedUnitName, status);
}
}
}
PasUsesClause usesIntf = null;
PasUsesClause usesImpl = null;
for (PasUsesClause usesClause : usesClauses) {
if (PsiUtil.belongsToInterface(usesClause)) {
usesIntf = usesClause;
} else {
usesImpl = usesClause;
}
}
final PasUsesClause usesInterface = usesIntf;
final PasUsesClause usesImplementation = usesImpl;
final Document doc = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
return new CollectingInfoRunnable() {
private String status;
@Nullable
@Override
public String getUserNotificationInfo() {
return status;
}
@Override
public void run() {
if (null == doc) {
return;
}
try {
int remIntf = usesInterface != null ? usesInterface.getNamespaceIdentList().size() : 0;
int remImpl = usesImplementation != null ? usesImplementation.getNamespaceIdentList().size() : 0;
List<TextRange> toRemoveIntf = new SmartList<TextRange>();
List<TextRange> toRemoveImpl = new SmartList<TextRange>();
List<TextRange> unitRangesIntf = getUnitRanges(usesInterface);
List<TextRange> unitRangesImpl = getUnitRanges(usesImplementation);
List<String> toMove = new SmartList<String>();
for (Map.Entry<PascalQualifiedIdent, UsedUnitStatus> unit : units.entrySet()) { // perform add operations
if (unit.getValue() == UsedUnitStatus.USED_IN_IMPL) { // move from interface to implementation
toMove.add(unit.getKey().getName());
remImpl++;
}
}
TextRange addedRange = addUnitToSection(PsiUtil.getElementPasModule(file), toMove, false);
if (addedRange != null) {
unitRangesImpl.add(addedRange);
}
int unknown = 0;
for (Map.Entry<PascalQualifiedIdent, UsedUnitStatus> unit : units.entrySet()) { // collect all removal ranges
if (unit.getValue() == UsedUnitStatus.USED_IN_IMPL) { // remove due to moving to implementation
TextRange range = removeUnitFromSection(unit.getKey(), usesInterface, unitRangesIntf, remIntf);
if (range != null) {
toRemoveIntf.add(range);
remIntf--;
}
} else if (unit.getValue() == UsedUnitStatus.UNUSED) {
TextRange range = removeUnitFromSection(unit.getKey(), usesInterface, unitRangesIntf, remIntf);
if (range != null) {
remIntf--;
toRemoveIntf.add(range);
} else {
range = removeUnitFromSection(unit.getKey(), usesImplementation, unitRangesImpl, remImpl);
if (range != null) {
remImpl--;
toRemoveImpl.add(range);
}
}
} else if (unit.getValue() == UsedUnitStatus.UNKNOWN) {
unknown++;
}
}
removeUnits(doc, usesImplementation, toRemoveImpl, remImpl); // remove implementation uses clause before other modifications
removeUnits(doc, usesInterface, toRemoveIntf, remIntf);
PsiDocumentManager.getInstance(file.getProject()).commitDocument(doc);
status = String.format("%d units moved to implementation, %d removed, %d unknown",
toMove.size(), toRemoveImpl.size() + toRemoveIntf.size() - toMove.size(), unknown);
} catch (Exception e) {
LOG.info("Error", e);
}
}
};
}
private static void removeUnits(Document doc, PasUsesClause clause, List<TextRange> toRemove, int remaining) {
if ((clause != null) && (0 == remaining)) {
doc.deleteString(clause.getTextRange().getStartOffset(), DocUtil.expandRangeEnd(doc, clause.getTextRange().getEndOffset(), DocUtil.RE_LF));
} else {
Collections.sort(toRemove, new ByOffsetComparator2());
for (TextRange textRange : toRemove) {
doc.deleteString(textRange.getStartOffset(), textRange.getEndOffset());
}
}
}
public static List<TextRange> getUnitRanges(PasUsesClause usesClause) {
if (null == usesClause) {
return new SmartList<TextRange>();
}
List<TextRange> res = new ArrayList<TextRange>(usesClause.getNamespaceIdentList().size());
for (PascalQualifiedIdent ident : usesClause.getNamespaceIdentList()) {
res.add(ident.getTextRange());
}
return res;
}
public static TextRange addUnitToSection(final PasModule module, List<String> names, boolean toInterface) {
if ((null == module) || (names.isEmpty())) {
return null;
}
assert (!toInterface || (module.getModuleType() == PascalModule.ModuleType.UNIT));
final PasUsesClause uses;
if (toInterface) {
uses = PsiTreeUtil.findChildOfType(PsiUtil.getModuleInterfaceSection(module), PasUsesClause.class);
} else {
uses = PsiTreeUtil.findChildOfType((module.getModuleType() == PascalModule.ModuleType.UNIT) ? PsiUtil.getModuleImplementationSection(module) : module, PasUsesClause.class);
}
int offs = 0;
String content = StringUtils.join(names, ", ");
if (uses != null) {
offs = uses.getTextRange().getEndOffset() - 1;
content = ",\n" + content + ";";
} else {
content = "\n\nuses\n" + content + ";";
@SuppressWarnings("unchecked") PsiElement prev = PsiTreeUtil.findChildOfAnyType(module, PasProgramModuleHead.class, PasLibraryModuleHead.class, PasPackageModuleHead.class);
if (calcOffset(prev) >= 0) {
offs = calcOffset(prev);
} else {
if (toInterface) {
PsiElement section = PsiUtil.getModuleInterfaceSection(module);
offs = section != null ? section.getTextRange().getStartOffset() + "interface".length() : offs;
} else {
PsiElement section = PsiUtil.getModuleImplementationSection(module);
offs = section != null ? section.getTextRange().getStartOffset() + "implementation".length(): offs;
}
}
}
Document doc = PsiDocumentManager.getInstance(module.getProject()).getDocument(module.getContainingFile());
if (doc != null) {
DocUtil.adjustDocument(doc, offs, content);
PsiDocumentManager.getInstance(module.getProject()).commitDocument(doc);
}
DocUtil.runCommandLaterInWriteAction(module.getProject(), PascalBundle.message("action.reformat"), new Runnable() {
@Override
public void run() {
for (PasUsesClause usesClause : PsiTreeUtil.findChildrenOfType(module, PasUsesClause.class)) {
PsiManager manager = usesClause.getManager();
if (manager != null) {
CodeStyleManager.getInstance(manager).reformat(usesClause, true);
}
}
}
});
return TextRange.create(offs + 2, offs + content.length());
}
private static int calcOffset(PsiElement prev) {
return prev != null ? prev.getTextRange().getEndOffset() : -1;
}
public static TextRange removeUnitFromSection(PascalQualifiedIdent usedUnit, PasUsesClause uses, List<TextRange> unitRanges, int remaining) {
if ((0 == remaining) || (null == uses) || (null == uses.getContainingFile())) {
return null;
}
Document doc = PsiDocumentManager.getInstance(uses.getProject()).getDocument(uses.getContainingFile());
int index = getUnitIndex(uses, usedUnit);
if ((index < 0) || (null == doc)) {
return null;
}
int start = DocUtil.expandRangeStart(doc, unitRanges.get(index).getStartOffset(), RE_UNITNAME_PREFIX);
int end = unitRanges.get(index).getEndOffset(); // Single
if (index > 0) {
if (index == remaining - 1) { // Right
start = unitRanges.get(index - 1).getEndOffset();
} else {
if (index < remaining - 1) { // Middle
end = DocUtil.expandRangeStart(doc, unitRanges.get(index + 1).getStartOffset(), RE_UNITNAME_PREFIX);
}
}
} else {
if (index < unitRanges.size() - 1) { // Left
end = DocUtil.expandRangeStart(doc, unitRanges.get(index + 1).getStartOffset(), RE_UNITNAME_PREFIX);
}
}
return TextRange.create(start, end);
}
private static int getUnitIndex(PasUsesClause uses, PascalQualifiedIdent usedUnit) {
if (null == uses) {
return -1;
}
for (int i = 0; i < uses.getNamespaceIdentList().size(); i++) {
if (usedUnit.equals(uses.getNamespaceIdentList().get(i))) {
return i;
}
}
return -1;
}
private static class ByOffsetComparator<T extends PsiElement> implements Comparator<T> {
@Override
public int compare(PsiElement o1, PsiElement o2) {
return o2.getTextRange().getStartOffset() - o1.getTextRange().getStartOffset();
}
}
private static class ByOffsetComparator2 implements Comparator<TextRange> {
@Override
public int compare(TextRange o1, TextRange o2) {
return o2.getStartOffset() - o1.getStartOffset();
}
}
}