/**
 * Id$
 *
 * Rearranger plugin for IntelliJ IDEA.
 *
 * Source code may be freely copied and reused.  Please copy credits, and send any bug fixes to the author.
 *
 * @author Dave Kriewall, WRQ, Inc.
 * October, 2003
 */
package com.wrq.rearranger.entry;

import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.search.SearchScope;
import com.intellij.psi.util.PsiSuperMethodUtil;
import com.wrq.rearranger.ModifierConstants;
import com.wrq.rearranger.popup.IFilePopupEntry;
import com.wrq.rearranger.rearrangement.Emitter;
import com.wrq.rearranger.rearrangement.GenericRearranger;
import com.wrq.rearranger.ruleinstance.IRuleInstance;
import com.wrq.rearranger.settings.RearrangerSettings;
import com.wrq.rearranger.settings.attributeGroups.IHasGetterSetterDefinition;
import com.wrq.rearranger.settings.attributeGroups.IRestrictMethodExtraction;
import com.wrq.rearranger.settings.attributeGroups.IRule;
import com.wrq.rearranger.util.MethodUtil;
import com.wrq.rearranger.util.ModifierUtils;
import org.apache.log4j.Logger;

import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import java.util.ArrayList;
import java.util.ListIterator;

/**
 * Describes an entire class's range and type.  This information is used when reordering outer classes.
 */
public class ClassEntry
        extends RangeEntry
        implements IFilePopupEntry
{
    private static final Logger logger = Logger.getLogger("com.wrq.rearranger.entry.ClassEntry");
    ArrayList/*<ClassContentsEntry or RangeEntry>*/ contents;
    ArrayList/*<IRuleInstance>*/ resultRuleInstances;
    final RearrangerSettings settings;

    public ClassEntry(final PsiElement start,
                      final PsiElement end,
                      final int modifiers,
                      final String name,
                      RearrangerSettings settings)
    {
        super(start, end, modifiers, name, "");
        this.settings = settings;
        contents = new ArrayList/*<ClassContentsEntry>*/();
    }

    public String getIconName()
    {
        if (end instanceof PsiClass)
        {
            if (((PsiClass) end).isInterface())
                return "interface";
        }
        return "class";
    }

    public JLabel getPopupEntryText(RearrangerSettings settings)
    {
        return new JLabel(name);
    }

    protected final ArrayList/*<ClassEntry>*/ parseClass(final Project project,
                                                         final PsiElement psiClass,
                                                         final ArrayList commentList,
                                                         final int nestingLevel)
    {
        final int startingIndex = 0;
        parseRemainingClassContents(project, startingIndex, psiClass, nestingLevel);
        return contents;
    }

    protected void parseRemainingClassContents(final Project project,
                                               int startingIndex,
                                               final PsiElement psiClass,
                                               int nestingLevel)
    {
        final PsiManager psiManager = PsiManager.getInstance(project);
        final PsiSearchHelper psh = psiManager.getSearchHelper();
        int lastIndex = startingIndex;
        /**
         * if option indicates, don't parse inner class contents; leave them unchanged.
         */
        if (!settings.isRearrangeInnerClasses() && nestingLevel > 1) {
            // the following trick puts everything in the "miscellaneous text" trailer.
            // start with the left brace of the class.
            lastIndex = 0;
            while (psiClass.getChildren()[lastIndex] != ((PsiClass)psiClass).getLBrace())
            {
                lastIndex++;
            }
            lastIndex++; // skip left brace, it will be emitted in header
            startingIndex = psiClass.getChildren().length;
        }
        for (int i = startingIndex; i < psiClass.getChildren().length; i++)
        {
            PsiElement child = psiClass.getChildren()[i];
            if (child instanceof PsiJavaToken && child.getText().equals("{"))
            {
                lastIndex = i + 1; // first item of a class starts immediately after the class left brace.
            }
            logger.debug(psiClass.toString() + " child " + i + ":" + child.toString());
            if (child instanceof PsiField ||
                    child instanceof PsiMethod ||
                    child instanceof PsiClass ||
                    child instanceof PsiClassInitializer)
            {
                MemberAttributes attributes = new MemberAttributes();
                PsiElement startElement = psiClass.getChildren()[lastIndex];

                RangeEntry classContentsEntry = null;
                if (child instanceof PsiField)
                {
                    i = parseField(child, attributes, i, (PsiClass) psiClass);
                    // a field declaration with multiple fields like "int x, y;" must advance child to "y;"
                    // to prevent splitting.
                    child = psiClass.getChildren()[i];
                    classContentsEntry = new FieldEntry(
                            startElement,
                            child,
                            attributes.modifiers, attributes.name, attributes.type
                    );
                }
                if (child instanceof PsiMethod)
                {
                    parseMethod(child, attributes, psh);
                    classContentsEntry = new MethodEntry(
                            startElement,
                            child, attributes.modifiers, attributes.name, attributes.type,
                            attributes.nParameters, attributes.interfaceName
                    );
                }
                if (child instanceof PsiClass)
                {
                    parseClass(child, attributes);
                    ClassEntry entry = new ClassEntry(
                            startElement,
                            ((PsiClass) child).getLBrace(),
                            attributes.modifiers, attributes.name,
                            settings
                    );
                    classContentsEntry = entry;
                    entry.parseClass(project,
                            (PsiClass) child,
                            settings.getItemOrderAttributeList(),
                            nestingLevel + 1);
                }
                if (child instanceof PsiClassInitializer)
                {
                    parseClassInitializer(child, attributes);
                    classContentsEntry = new ClassInitializerEntry(
                            startElement, child, attributes.modifiers, attributes.name, attributes.type
                    );
                }
                contents.add(classContentsEntry);
                classContentsEntry.checkForComment();
                lastIndex = i + 1; // next class includes everything since the end of the prior class.
            }
        }
        if (lastIndex < psiClass.getChildren().length)
        {
            if (lastIndex < 0)
            {
                lastIndex = 0;
            }
            // create a dummy trailer entry to cover anything after the last class.
            final MiscellaneousTextEntry miscellaneousTextEntry = new MiscellaneousTextEntry(
                    psiClass.getChildren()[lastIndex],
                    psiClass.getChildren()[psiClass.getChildren().length - 1],
                    false, true
            );
            contents.add(miscellaneousTextEntry);
            miscellaneousTextEntry.checkForComment();
        }
    }

    private int parseField(PsiElement child, MemberAttributes attributes, int i, final PsiClass psiClass)
    {
        attributes.field = (PsiField) child;
        attributes.name = attributes.field.getName();
        attributes.type = attributes.field.getTypeElement().getText();
        attributes.modifiers = ModifierUtils.getModifierMask(attributes.field.getModifierList().getText());
        if (attributes.field.getInitializer() instanceof PsiNewExpression)
        {
            final PsiElement lc = attributes.field.getInitializer().getLastChild();
            if (lc instanceof PsiAnonymousClass)
            {
                attributes.modifiers |= ModifierConstants.INIT_TO_ANON_CLASS;
            }
        }
        /**
         * handle multiple declarations here.  While this field declaration does not end
         * in a semicolon, skip whitespace and intervening comma; move child ahead to
         * next field.  This prevents declarations like "int b, a;" from being split.
         */
        PsiElement myChild = child;
        boolean done = false;
        while (!done)
        {
            PsiElement lastElement = myChild.getLastChild();
            if (lastElement instanceof PsiJavaToken &&
                    ((PsiJavaToken) lastElement).getTokenType() == PsiJavaToken.SEMICOLON)
                break;
            if (lastElement instanceof PsiComment)
                break;
            // skip to next field
            while (++i < psiClass.getChildren().length)
            {
                myChild = psiClass.getChildren()[i];
                if (myChild instanceof PsiField)
                {
                    done = true;
                    break;
                }
            }
        }
        return i;
    }

    private void parseMethod(PsiElement child, MemberAttributes attributes, final PsiSearchHelper psh)
    {
        attributes.method = (PsiMethod) child;
        attributes.name = attributes.method.getName();
        if (attributes.method.getReturnTypeElement() == null)
        {
            attributes.type = "null";
        }
        else
        {
            attributes.type = attributes.method.getReturnTypeElement().getText();
        }
        attributes.modifiers = ModifierUtils.getModifierMask(attributes.method.getModifierList().getText());
        attributes.nParameters = attributes.method.getParameterList().getParameters().length;
        // set any additional "modifier" flags.
        final PsiMethod[] superMethods = PsiSuperMethodUtil.findSuperMethods(attributes.method);
        attributes.modifiers |= isCanonicalOrInterface(attributes.method, attributes);
        final SearchScope ss = psh.getAccessScope(child);
        final PsiMethod[] overriders = psh.findOverridingMethods(attributes.method, ss, true);
        if (overriders.length > 0)
        {
            logger.debug(
                    "method " +
                    attributes.method.toString() +
                    " is overridden; has " +
                    overriders.length + " overriding methods:"
            );
            dumpMethodNames(overriders);
            attributes.modifiers |= ModifierConstants.OVERRIDDEN;
        }
        // determine if this method overrides another.
        logger.debug("method " + attributes.method.toString() + " has " + superMethods.length + " supermethods");
        dumpMethodNames(superMethods);
        if (superMethods.length > 0)
        {
            attributes.modifiers |= ModifierConstants.OVERRIDING;
        }
        if (attributes.method.isConstructor())
        {
            attributes.modifiers |= ModifierConstants.CONSTRUCTOR;
            logger.debug("method " + attributes.method.toString() + " is constructor");
        }
        /** getter/setter cannot be determined here because definition of what a getter/setter
         * is can vary from rule to rule.  Do it at the time of rule matching.
         */
        else if ((attributes.modifiers & ModifierConstants.CANONICAL) == 0)
        {
            logger.debug("method " + attributes.method.toString() + " is other");
            attributes.modifiers |= ModifierConstants.OTHER_METHOD;
        }
    }

    private int isCanonicalOrInterface(PsiMethod method, MemberAttributes attributes)
    {
        PsiElement methodParent = method.getParent();
        logger.debug("checking to see if " + method.getName() + " of " + methodParent + " is canonical");
        PsiMethod[] superMethods = PsiSuperMethodUtil.findSuperMethods(method);
        if (superMethods.length > 0)
        {
            // check to see if this method is canonical (inherited from Object).
            for (int j = 0; j < superMethods.length; j++)
            {
                PsiMethod m = superMethods[j];
                PsiElement parent = m.getParent();
                logger.debug(
                        "supermethod " +
                        m.getName() +
                        " belongs to " +
                        parent
                );
                if (parent instanceof PsiClass)
                {
                    final PsiClass psiClass = ((PsiClass) parent);
                    if (psiClass.isInterface())
                    {
                        logger.debug("method " + method.toString() + " implements interface");
                        attributes.interfaceName = psiClass.getName();
                    }
                    PsiElement superclass = psiClass.getSuperClass();
                    logger.debug("Superclass is " + superclass);
                    if (superclass == null)
                    {
                        // m's class must be java.lang.Object; it's the only class with a null
                        // superclass.
                        logger.debug("method " + method.toString() + " is canonical");
                        return ModifierConstants.CANONICAL;
                    }
                    else
                    {
                        // superclass is PsiClass:Object.
                        int result = 0;
                        if (!superclass.toString().equalsIgnoreCase("psiclass:object"))
                            result = isCanonicalOrInterface(m, attributes);
                        logger.debug("Returning result from connanical check as " + result);
                        if (result > 0)
                            return result;
                    }
                }
            }
        }
        return 0;
    }

    private void dumpMethodNames(PsiMethod[] methods)
    {
        for (int j = 0; j < methods.length; j++)
        {
            logger.debug(
                    j +
                    ":" +
                    methods[j].toString() +
                    " of class " +
                    methods[j].getParent().toString()
            );
        }
    }

    private void parseClass(PsiElement child, MemberAttributes attributes)
    {
        attributes.childClass = (PsiClass) child;
        attributes.name = attributes.childClass.getName();
        if (attributes.name == null) attributes.name = "";
        attributes.modifiers = ModifierUtils.getModifierMask(attributes.childClass.getModifierList().getText());
    }

    private void parseClassInitializer(PsiElement child, MemberAttributes attributes)
    {
        attributes.classInitializer = (PsiClassInitializer) child;
        attributes.classInitializer.getModifierList();
        attributes.name = "";
        attributes.modifiers = ModifierUtils.getModifierMask(attributes.classInitializer.getModifierList().getText()) |
                ModifierConstants.STATIC_INITIALIZER;
    }

    public void emit(Emitter emitter)
    {
        // first emit the text up to and including the left brace.
        super.emit(emitter);
        // now emit all children.
        emitter.emitRuleInstances(getResultRuleInstances());
    }

    public DefaultMutableTreeNode addToPopupTree(DefaultMutableTreeNode node, RearrangerSettings settings)
    {
        DefaultMutableTreeNode result = super.addToPopupTree(node, settings);
        // now add class contents, if any
        if (getResultRuleInstances() != null)
        {
            ListIterator li = getResultRuleInstances().listIterator();
            while (li.hasNext())
            {
                IRuleInstance instance = (IRuleInstance) li.next();
                instance.addRuleInstanceToPopupTree(result, settings);
            }
        }
        return result;
    }

    /**
     * rearranges the contents of this PsiClass according to supplied rules.
     */
    public void rearrangeContents()
    {
        buildMethodCallGraph();
        ListIterator/*<RangeEntry>*/ li = getContents().listIterator();
        while (li.hasNext())
        {
            RangeEntry contentsEntry = (RangeEntry) li.next();
            if (contentsEntry instanceof MethodEntry)
            {
                MethodEntry me = (MethodEntry) contentsEntry;
                if (me.isGetter())
                {
                    if (settings.isKeepGettersSettersTogether())
                    {
                        me.determineSetter(getContents());
                    }
                }
                if (!me.isNoExtractedMethods() &&
                        settings.getRelatedMethodsSettings().isMoveExtractedMethods())
                {
                    me.determineMethodCalls(getContents(), settings);
                }
            }
        }
        logger.debug("determining extracted methods");
        li = getContents().listIterator();
        while (li.hasNext())
        {
            RangeEntry contentsEntry = (RangeEntry) li.next();
            if (contentsEntry instanceof MethodEntry)
            {
                MethodEntry me = (MethodEntry) contentsEntry;
                me.determineExtractedMethod(settings.getRelatedMethodsSettings());
            }
        }
        /**
         * check for overloaded extracted methods; if configured to be kept together, attach subsequent
         * methods to the first and remove them from consideration for other alignment.
         */
        MethodEntry.handleOverloadedMethods(getContents(), settings);
        final GenericRearranger classContentsRearranger =
                new GenericRearranger(settings.getItemOrderAttributeList(), contents)
                {
                    public void rearrangeRelatedItems(ArrayList entries,
                                                      ArrayList/*<IRuleInstance>*/ ruleInstanceList)
                    {
                        ListIterator ruleIterator = ruleInstanceList.listIterator();
                        while (ruleIterator.hasNext())
                        {
                            IRuleInstance ruleInstance = (IRuleInstance) ruleIterator.next();
                            ruleInstance.rearrangeRuleItems(entries, settings);
                        }
                    }
                };
        resultRuleInstances = classContentsRearranger.rearrangeEntries();
    }


    private void buildMethodCallGraph()
    {
        if (settings.getRelatedMethodsSettings().isMoveExtractedMethods() ||
                settings.isKeepGettersSettersTogether())
        {
            logger.debug("building method call & getter-setter graph");
            ListIterator/*<RangeEntry>*/ li = getContents().listIterator();
            while (li.hasNext())
            {
                RangeEntry contentsEntry = (RangeEntry) li.next();
                if (contentsEntry instanceof MethodEntry)
                {
                    MethodEntry me = (MethodEntry) contentsEntry;
                    /**
                     * check all rules to find first that this method matches.  If that rule specifies
                     * that matching methods are excluded from extracted method treatment, then skip this
                     * step.  If this method is a getter (according to that rule's definition) then
                     * determine its setter (according to that rule's definition) at this point also.
                     */
                    me.setNoExtractedMethods(false);
                    me.setGetter(
                            MethodUtil.isGetter(
                                    (PsiMethod) me.end,
                                    settings.getDefaultGSDefinition()
                            )
                    );
                    me.setSetter(
                            MethodUtil.isSetter(
                                    (PsiMethod) me.end,
                                    settings.getDefaultGSDefinition()
                            )
                    );
                    ListIterator/*<AttributeGroup>*/ ri = settings.getItemOrderAttributeList().listIterator();
                    while (ri.hasNext())
                    {
                        final IRule rule = (IRule) ri.next();
                        if (rule instanceof IRestrictMethodExtraction)
                        {
                            if (rule.isMatch(me))
                            {
                                if (((IRestrictMethodExtraction) rule).isNoExtractedMethods())
                                {
                                    logger.debug(
                                            "excluding " +
                                            me.end.toString() +
                                            " from extracted method consideration"
                                    );
                                    me.setNoExtractedMethods(true);
                                }
                                me.setGetter(false);
                                if (rule instanceof IHasGetterSetterDefinition)
                                {
                                    if (MethodUtil.isGetter(
                                            (PsiMethod) me.getEnd(),
                                            ((IHasGetterSetterDefinition) rule).getGetterSetterDefinition()
                                    ))
                                    {
                                        if (settings.isKeepGettersSettersTogether())
                                        {
                                            me.setGetter(true);
                                        }
                                    }
                                    me.setSetter(
                                            MethodUtil.isSetter(
                                                    (PsiMethod) me.getEnd(),
                                                    ((IHasGetterSetterDefinition) rule).getGetterSetterDefinition()
                                            )
                                    );
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
// End Methods of Interface IFilePopupEntry

    public final ArrayList/*<ClassContentsEntry>*/ getContents()
    {
        return contents;
    }

    public ArrayList/*<ClassContentsEntry>*/ getResultRuleInstances()
    {
        return resultRuleInstances;
    }


}
