/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.java.hints;

import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.LineMap;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.tools.Diagnostic;
import org.netbeans.api.editor.EditorActionNames;
import org.netbeans.api.editor.EditorActionRegistration;
import org.netbeans.api.java.source.*;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.ModificationResult.Difference;
import org.netbeans.api.progress.ProgressUtils;
import org.netbeans.editor.BaseAction;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.GuardedDocument;
import org.netbeans.editor.MarkBlock;
import org.netbeans.editor.MarkBlockChain;
import org.netbeans.modules.editor.java.JavaKit;
import org.netbeans.modules.java.source.parsing.Hacks;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.Fix;
import org.netbeans.spi.java.hints.ErrorDescriptionFactory;
import org.netbeans.spi.java.hints.Hint;
import org.netbeans.spi.java.hints.HintContext;
import org.netbeans.spi.java.hints.JavaFix;
import org.netbeans.spi.java.hints.TriggerTreeKind;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;

/**
 *
 * @author Dusan Balek
 */
@Hint(displayName = "#DN_org.netbeans.modules.java.hints.OrganizeMembers", description = "#DESC_org.netbeans.modules.java.hints.OrganizeMembers", category = "class_structure", enabled = false)
public class OrganizeMembers {

    @TriggerTreeKind(Kind.CLASS)
    public static ErrorDescription checkMembers(final HintContext context) {
        for (Diagnostic<?> d : context.getInfo().getDiagnostics()) {
            if (Hacks.isSyntaxError(d)) {
                return null;
            }
        }
        Source source = context.getInfo().getSnapshot().getSource();
        try {
            ModificationResult result = ModificationResult.runModificationTask(Collections.singleton(source), new UserTask() {
                @Override
                public void run(ResultIterator resultIterator) throws Exception {
                    WorkingCopy copy = WorkingCopy.get(resultIterator.getParserResult());
                    copy.toPhase(Phase.RESOLVED);
                    doOrganizeMembers(copy, context.getPath());
                }
            });
            List<? extends Difference> diffs = result.getDifferences(source.getFileObject());
            if (diffs != null && !diffs.isEmpty() && !checkGuarded(context.getInfo().getDocument(), diffs)) {
                Fix fix = new OrganizeMembersFix(context.getInfo(), context.getPath()).toEditorFix();
                SourcePositions sp = context.getInfo().getTrees().getSourcePositions();
                int offset = diffs.get(0).getStartPosition().getOffset();
                LineMap lm = context.getInfo().getCompilationUnit().getLineMap();
                long lno = lm.getLineNumber(offset);
                if (lno >= 1) {
                    offset = (int)lm.getStartPosition(lno);
                }
                CompilationUnitTree cut = context.getPath().getCompilationUnit();
                ClassTree clazz = (ClassTree) context.getPath().getLeaf();
                for (Tree member : clazz.getMembers()) {
                    if (context.getInfo().getTreeUtilities().isSynthetic(new TreePath(context.getPath(), member))) continue;
                    if (sp.getStartPosition(cut, member) >= offset) {
                        return ErrorDescriptionFactory.forTree(context, member, NbBundle.getMessage(OrganizeMembers.class, "MSG_OragnizeMembers"), fix); //NOI18N
                    }
                }
                return ErrorDescriptionFactory.forTree(context, clazz, NbBundle.getMessage(OrganizeMembers.class, "MSG_OragnizeMembers"), fix); //NOI18N
            }
        } catch (Exception ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }

    private static void doOrganizeMembers(WorkingCopy copy, TreePath path) {
        GeneratorUtilities gu = GeneratorUtilities.get(copy);
        
        ClassTree clazz = (ClassTree) path.getLeaf();
        clazz = gu.importComments(clazz, copy.getCompilationUnit());
        TreeMaker maker = copy.getTreeMaker();
        ClassTree nue = maker.Class(clazz.getModifiers(), clazz.getSimpleName(), clazz.getTypeParameters(), clazz.getExtendsClause(), clazz.getImplementsClause(), Collections.<Tree>emptyList());
        List<Tree> members = new ArrayList<>(clazz.getMembers().size());
        Map<Tree, Tree> memberMap = new HashMap<>(clazz.getMembers().size());
        
        List<Tree> enumValues = new ArrayList<>();
        for (Tree tree : clazz.getMembers()) {
            if (copy.getTreeUtilities().isSynthetic(new TreePath(path, tree))) {
                continue;
            }
            Tree member;
            switch (tree.getKind()) {
                case CLASS:
                case INTERFACE:
                case ENUM:
                case ANNOTATION_TYPE:
                    member = maker.setLabel(tree, ((ClassTree)tree).getSimpleName());
                    break;
                case VARIABLE:
                    member = maker.setLabel(tree, ((VariableTree)tree).getName());
                    if (copy.getTreeUtilities().isEnumConstant((VariableTree)tree)) {
                        enumValues.add(member);
                    }
                    break;
                case METHOD:
                    member = maker.setLabel(tree, ((MethodTree)tree).getName());
                    break;
                case BLOCK:
                    member = maker.asReplacementOf(maker.Block(((BlockTree)tree).getStatements(), ((BlockTree)tree).isStatic()), tree, true);
                    break;
                default:
                    member = tree;    
            }
            members.add(member);
            memberMap.put(member, tree);
        }
        // fool the generator utilities with cloned members, so it does not take positions into account
        if (enumValues.isEmpty()) {
            nue = GeneratorUtilities.get(copy).insertClassMembers(nue, members);
        } else {
            members.removeAll(enumValues);
            int max = nue.getMembers().size();
            // insert the enum values in the original order
            for (Tree t : enumValues) {
                nue = maker.insertClassMember(nue, max++, t);
            }
            nue = GeneratorUtilities.get(copy).insertClassMembers(nue, members);
        }
        // now create a new class, based on the original one - retain the order decided by GeneratorUtilities.
        ClassTree changed = maker.Class(clazz.getModifiers(), clazz.getSimpleName(), clazz.getTypeParameters(), clazz.getExtendsClause(), clazz.getImplementsClause(), Collections.<Tree>emptyList());
        int index = 0;
        for (Tree t : nue.getMembers()) {
            Tree orig = memberMap.get(t);
            changed = maker.insertClassMember(changed, index, orig);
            index++;
        }
        copy.rewrite(clazz, changed);
    }
    
    private static boolean checkGuarded(Document doc, List<? extends Difference> diffs) {
        if (doc instanceof GuardedDocument) {
            MarkBlockChain chain = ((GuardedDocument) doc).getGuardedBlockChain();
            for (Difference diff : diffs) {
                if ((chain.compareBlock(diff.getStartPosition().getOffset(), diff.getEndPosition().getOffset()) & MarkBlock.OVERLAP) != 0) {
                    return true;
                }
            }
        }
        return false;
    }

    private static class OrganizeMembersFix extends JavaFix {

        public OrganizeMembersFix(CompilationInfo info, TreePath tp) {
            super(info, tp);
        }

        @Override
        public String getText() {
            return NbBundle.getMessage(OrganizeMembers.class, "FIX_OrganizeMembers"); //NOI18N
        }

        @Override
        protected void performRewrite(TransformationContext ctx) {
            doOrganizeMembers(ctx.getWorkingCopy(), ctx.getPath());
        }
    }

    @EditorActionRegistration(name = EditorActionNames.organizeMembers,
                              mimeType = JavaKit.JAVA_MIME_TYPE,
                              menuPath = "Source",
                              menuPosition = 2437,
                              menuText = "#" + EditorActionNames.organizeMembers + "_menu_text")
    public static class OrganizeMembersAction extends BaseAction {

        @Override
        public void actionPerformed(final ActionEvent evt, final JTextComponent component) {
            if (component == null || !component.isEditable() || !component.isEnabled()) {
                Toolkit.getDefaultToolkit().beep();
                return;
            }
            final BaseDocument doc = (BaseDocument) component.getDocument();
            final Source source = Source.create(doc);
            if (source != null) {
                final AtomicBoolean cancel = new AtomicBoolean();
                ProgressUtils.runOffEventDispatchThread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            ModificationResult result = ModificationResult.runModificationTask(Collections.singleton(source), new UserTask() {
                                @Override
                                public void run(ResultIterator resultIterator) throws Exception {
                                    WorkingCopy copy = WorkingCopy.get(resultIterator.getParserResult());
                                    copy.toPhase(Phase.RESOLVED);
                                    TreeUtilities tu = copy.getTreeUtilities();
                                    TreePath path = tu.pathFor(component.getCaretPosition());
                                    path = tu.getPathElementOfKind(EnumSet.of(Kind.CLASS, Kind.ENUM, Kind.INTERFACE, Kind.ANNOTATION_TYPE), path);
                                    if (path != null) {
                                        doOrganizeMembers(copy, path);
                                    } else {
                                        CompilationUnitTree cut = copy.getCompilationUnit();
                                        List<? extends Tree> typeDecls = cut.getTypeDecls();
                                        if (typeDecls.isEmpty()) {
                                            Toolkit.getDefaultToolkit().beep();
                                        } else {
                                            doOrganizeMembers(copy, copy.getTrees().getPath(cut, typeDecls.get(0)));
                                        }
                                    }
                                }
                            });
                            List<? extends Difference> diffs = result.getDifferences(source.getFileObject());
                            if (diffs != null && !diffs.isEmpty() && !checkGuarded(doc, diffs)) {
                                result.commit();
                            } else {
                                Toolkit.getDefaultToolkit().beep();
                            }
                        } catch (Exception ex) {
                            Toolkit.getDefaultToolkit().beep();
                        }
                    }
                }, NbBundle.getMessage(OrganizeMembers.class, "MSG_OragnizeMembers"), cancel, false); //NOI18N
            }
        }
    }
}
