/*
 * 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.refactoring.java.ui;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.swing.JOptionPane;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.api.fileinfo.NonRecursiveFolder;
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.source.*;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.refactoring.java.RefactoringUtils;
import org.netbeans.modules.refactoring.java.api.JavaRefactoringUtils;
import org.netbeans.modules.refactoring.spi.ui.RefactoringUI;
import org.netbeans.modules.refactoring.spi.ui.UI;
import org.openide.ErrorManager;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.nodes.Node;
import org.openide.text.CloneableEditorSupport;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.windows.TopComponent;

/**
 * TODO: should be API
 * @author Jan Becicka
 */
public final class ContextAnalyzer {

    /**
     * create analyze task.
     * @param context context of refactoring
     * @param factory for creating of RefactoringUI
     * @return
     */
    public static Runnable createTask(Lookup context, final JavaRefactoringUIFactory factory) {

        Runnable task;
        EditorCookie ec = context.lookup(EditorCookie.class);
        if (RefactoringUtils.isFromEditor(ec)) {
            task = new TextComponentTask(ec) {

                @Override
                protected RefactoringUI createRefactoringUI(TreePathHandle selectedElement, int startOffset, int endOffset, final CompilationInfo info) {
                    TreePathHandle[] handles;
                    FileObject[] file;
                    if(selectedElement != null) {
                        handles = new TreePathHandle[]{selectedElement};
                        file = new FileObject[]{selectedElement.getFileObject()};
                    } else {
                        handles = new TreePathHandle[0];
                        file = new FileObject[]{info.getFileObject()};
                    }
                    return factory.create(info, handles, file, new NonRecursiveFolder[0]);
                }
            };
        } else if (nodeHandle(context)) {
            task = new TreePathHandleTask(new HashSet<Node>(context.lookupAll(Node.class)), false) {

                RefactoringUI ui;
                private boolean created;

                @Override
                protected void treePathHandleResolved(TreePathHandle handle, CompilationInfo javac) {
                    if (size()==1) {
                        ui = factory.create(javac, new TreePathHandle[]{handle}, null, new NonRecursiveFolder[0]);
                        created = true;
                    }
                }

                @Override
                protected RefactoringUI createRefactoringUI(Collection<TreePathHandle> handles) {
                    if (!created) {
                        ui = factory.create(null, handles.toArray(new TreePathHandle[handles.size()]), null,  new NonRecursiveFolder[0]);
                    }
                    return ui;
                }
            };
        } else {
             task = new NodeToFileObjectTask(new HashSet<Node>(context.lookupAll(Node.class))) {

                RefactoringUI ui;
                private boolean created = false;

                @Override
                protected void nodeTranslated(Node node, Collection<TreePathHandle> handles, CompilationInfo javac) {
                    if (size()==1) {
                        ui = factory.create(javac, handles.toArray(new TreePathHandle[1]), new FileObject[]{handles.iterator().next().getFileObject()}, new NonRecursiveFolder[0]);
                        created = true;
                    }
                }

                @Override
                protected RefactoringUI createRefactoringUI(FileObject[] selectedElements, Collection<TreePathHandle> handles) {
                    if (!created) {
                        if(handles.isEmpty() && selectedElements.length > 0 && "package-info".equals(selectedElements[0].getName())) {
                            ui = factory.create(null, new TreePathHandle[]{}, selectedElements, new NonRecursiveFolder[0]);
                        } else {
                            ui = factory.create(null, handles.toArray(new TreePathHandle[handles.size()]), selectedElements, pkg.toArray(new NonRecursiveFolder[pkg.size()]));
                        }
                    }
                    return ui;
                }
            };
        }
        return task;
    }
    
    /**
     * utility method to perform enable/disable logic for refactoring actions
     * @param lookup
     * @param notOnlyFile action is disabled when the selection is on File
     * @param onlyFromEditor action is enabled only in editor
     * @return 
     */
    public static boolean canRefactorSingle(Lookup lookup, boolean notOnlyFile, boolean onlyFromEditor) {
        Collection<? extends Node> nodes = new HashSet<Node>(lookup.lookupAll(Node.class));
        if(nodes.size() != 1) {
            return false;
        }
        Node node = nodes.iterator().next();
        TreePathHandle tph = node.getLookup().lookup(TreePathHandle.class);
        if (tph != null) {
            if(JavaRefactoringUtils.isRefactorable(tph.getFileObject())) {
                return !onlyFromEditor || RefactoringUtils.isFromEditor(lookup.lookup(EditorCookie.class));
            } else {
                return false;
            }
        }
        DataObject dObj = node.getLookup().lookup(DataObject.class);
        if(null == dObj) {
            return false;
        }
        FileObject fileObj = dObj.getPrimaryFile();
        if(null == fileObj || !JavaRefactoringUtils.isRefactorable(fileObj)) {
            return false;
        }

        EditorCookie ec = lookup.lookup(EditorCookie.class);
        if (RefactoringUtils.isFromEditor(ec)) {
            return true;
        }
        return !notOnlyFile;
    }
    
    private static abstract class TreePathHandleTask implements Runnable, CancellableTask<CompilationController> {
        private Collection<TreePathHandle> handles = new ArrayList<TreePathHandle>();
        private TreePathHandle current;
        private FileObject file;
        boolean renameFile;
     
        public TreePathHandleTask(Collection<? extends Node> nodes) {
            this(nodes, false);
        }
        
        public int size() {
            return handles.size();
        }
        public TreePathHandleTask(Collection<? extends Node> nodes, boolean useFirstHandle) {
            for (Node n:nodes) {
                TreePathHandle temp = n.getLookup().lookup(TreePathHandle.class);
                if (temp!=null) {
                    handles.add(temp);
                    if (useFirstHandle) {
                        break;
                    }
                }
            }
            Node n = nodes.iterator().next();
            DataObject dob = n.getLookup().lookup(DataObject.class);
            file = dob != null ? dob.getPrimaryFile() : null;
        }
        
        public TreePathHandleTask(TreePathHandle tph) {
            handles.add(tph);
        }
        
        @Override
        public void cancel() {
        }
        
        @Override
        public void run(CompilationController info) throws Exception {
            info.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
            Element el = current.resolveElement(info);
            if (el!=null && el instanceof TypeElement && !((TypeElement)el).getNestingKind().isNested()) {
                if (info.getFileObject().getName().equals(el.getSimpleName().toString())) {
                    renameFile = true;
                }
            }
            treePathHandleResolved(current, info);
        }
        
        @Override
        public void run() {
            for (TreePathHandle handle:handles) {
                FileObject f = handle.getFileObject();
                if (f==null) {
                    if(file != null) {
                        f = file;
                    } else {
                        //ugly workaround for #205142
                        TopComponent top = (TopComponent) EditorRegistry.lastFocusedComponent().getParent().getParent().getParent().getParent();
                        f = top.getLookup().lookup(FileObject.class);
                    }
                }
                current = handle;
                JavaSource source = JavaSource.forFileObject(f);
                assert source != null;
                try {
                    source.runUserActionTask(this, true);
                } catch (IllegalArgumentException ex) {
                    ex.printStackTrace();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }

            TopComponent activetc = TopComponent.getRegistry().getActivated();

            RefactoringUI ui = createRefactoringUI(handles);
            if (ui!=null) {
                UI.openRefactoringUI(ui, activetc);
            } else {
                JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_CannotRenameKeyword"));
            }
        }

        /**
         * This is the place where subclasses may collect info about handles.
         * @param handle handle
         * @param javac context of running transaction
         */
        protected void treePathHandleResolved(TreePathHandle handle, CompilationInfo javac) {
        }

        protected abstract RefactoringUI createRefactoringUI(Collection<TreePathHandle> handles);
    }    
    
    private static abstract class TextComponentTask implements Runnable, CancellableTask<CompilationController> {
        private JTextComponent textC;
        private int caret;
        private int start;
        private int end;
        private RefactoringUI ui;
        private boolean selection;
        
        public TextComponentTask(EditorCookie ec) {
            this.textC = ec.getOpenedPanes()[0];
            this.caret = textC.getCaretPosition();
            this.start = textC.getSelectionStart();
            this.end = textC.getSelectionEnd();
            this.selection = start != end && (start != -1 || end != -1);
            assert caret != -1;
            assert start != -1;
            assert end != -1;
        }
        
        @Override
        public void cancel() {
        }
        
        @Override
        public void run(final CompilationController cc) throws Exception {
            cc.toPhase(JavaSource.Phase.RESOLVED);
            final int c = selection?start:this.caret;
            final TreePath[] selectedElement = new TreePath[] {null};
//            final boolean[] insideJavadoc = {false};
            final Document doc = cc.getDocument();
            doc.render(new Runnable() {
                @Override
                public void run() {
                    selectedElement[0] = validateSelection(cc, start, end);

                    if (selectedElement[0] == null) {
                        TokenSequence<JavaTokenId> ts = SourceUtils.getJavaTokenSequence(cc.getTokenHierarchy(), c);
                        int adjustedCaret = c;
                        ts.move(c);
                        if (ts.moveNext() && ts.token() != null) {
                            if (ts.token().id() == JavaTokenId.IDENTIFIER) {
                                adjustedCaret = ts.offset() + ts.token().length() / 2 + 1;
                            } /*else if (ts.token().id() == JavaTokenId.JAVADOC_COMMENT) {
                             TokenSequence<JavadocTokenId> jdts = ts.embedded(JavadocTokenId.language());
                             if (jdts != null && JavadocImports.isInsideReference(jdts, caret)) {
                             jdts.move(caret);
                             if (jdts.moveNext() && jdts.token().id() == JavadocTokenId.IDENT) {
                             adjustedCaret[0] = jdts.offset();
                             insideJavadoc[0] = true;
                             }
                             } else if (jdts != null && JavadocImports.isInsideParamName(jdts, caret)) {
                             jdts.move(caret);
                             if (jdts.moveNext()) {
                             adjustedCaret[0] = jdts.offset();
                             insideJavadoc[0] = true;
                             }
                             }
                             }*/
                        }
                        selectedElement[0] = cc.getTreeUtilities().pathFor(adjustedCaret);
                    }
                }
            });
            //workaround for issue 89064
            if (selectedElement[0].getLeaf().getKind() == Tree.Kind.COMPILATION_UNIT) {
                List<? extends Tree> decls = cc.getCompilationUnit().getTypeDecls();
                if (!decls.isEmpty()) {
                    TreePath path = TreePath.getPath(cc.getCompilationUnit(), decls.get(0));
                    if (path!=null && cc.getTrees().getElement(path)!=null) {
                        selectedElement[0] = path;
                    }
                } else {
                    selectedElement[0] = null;
                }
            }
            ui = createRefactoringUI(selectedElement[0] != null ? TreePathHandle.create(selectedElement[0], cc) : null, start, end, cc);
        }
        
        @Override
        public final void run() {
            try {
                JavaSource source = JavaSource.forDocument(textC.getDocument());
                source.runUserActionTask(this, true);
            } catch (IOException ioe) {
                ErrorManager.getDefault().notify(ioe);
                return ;
            }
            TopComponent activetc = TopComponent.getRegistry().getActivated();

            SHOW.show(ui, activetc);
        }
        
        protected abstract RefactoringUI createRefactoringUI(TreePathHandle selectedElement,int startOffset,int endOffset, CompilationInfo info);
    }

    private static final Set<TypeKind> NOT_ACCEPTED_TYPES = EnumSet.of(TypeKind.ERROR, TypeKind.NONE, TypeKind.OTHER, TypeKind.VOID);
    
    static TreePath validateSelection(CompilationInfo ci, int start, int end) {
        return validateSelection(ci, start, end, NOT_ACCEPTED_TYPES);
    }

    public static TreePath validateSelection(CompilationInfo ci, int start, int end, Set<TypeKind> ignoredTypes) {
        if(start == end) {
            TokenSequence<JavaTokenId> cts = ci.getTokenHierarchy().tokenSequence(JavaTokenId.language());

            if (cts != null) {
                cts.move(start);

                if (cts.moveNext() && cts.token().id() != JavaTokenId.WHITESPACE && cts.offset() == start) {
                    start = end += 1;
                }
            }
        }
        
        TreePath tp = ci.getTreeUtilities().pathFor(start == end? start : (start + end) / 2 + 1);

        for ( ; tp != null; tp = tp.getParentPath()) {
            Tree leaf = tp.getLeaf();

            if (   !ExpressionTree.class.isAssignableFrom(leaf.getKind().asInterface())
                && (leaf.getKind() != Tree.Kind.VARIABLE || ((VariableTree) leaf).getInitializer() == null)) {
                continue;
            }

            long treeStart = ci.getTrees().getSourcePositions().getStartPosition(ci.getCompilationUnit(), leaf);
            long treeEnd   = ci.getTrees().getSourcePositions().getEndPosition(ci.getCompilationUnit(), leaf);

            if (start != end) {
                if (treeStart != start || treeEnd != end) {
                    continue;
                }
            } else {
                if (treeStart != start && treeEnd != end) {
                    continue;
                } 
            }

            TypeMirror type = ci.getTrees().getTypeMirror(tp);

            if (type != null && type.getKind() == TypeKind.ERROR) {
                type = ci.getTrees().getOriginalType((ErrorType) type);
            }

            if (type == null || ignoredTypes.contains(type.getKind()))
                continue;

            if(tp.getLeaf().getKind() == Tree.Kind.ASSIGNMENT)
                continue;

            if (tp.getLeaf().getKind() == Tree.Kind.ANNOTATION)
                continue;

            if (!isInsideClass(tp))
                return null;

            TreePath candidate = tp;

            tp = tp.getParentPath();

            while (tp != null) {
                switch (tp.getLeaf().getKind()) {
                    case VARIABLE:
                        VariableTree vt = (VariableTree) tp.getLeaf();
                        if (vt.getInitializer() == leaf) {
                            return candidate;
                        } else {
                            return null;
                        }
                    case NEW_CLASS:
                        NewClassTree nct = (NewClassTree) tp.getLeaf();
                        
                        if (nct.getIdentifier().equals(candidate.getLeaf())) { //avoid disabling hint ie inside of anonymous class higher in treepath
                            for (Tree p : nct.getArguments()) {
                                if (p == leaf) {
                                    return candidate;
                                }
                            }

                            return null;
                        }
                }

                leaf = tp.getLeaf();
                tp = tp.getParentPath();
            }

            return candidate;
        }

        return null;
    }
    
    private static boolean isInsideClass(TreePath tp) {
        while (tp != null) {
            if (TreeUtilities.CLASS_TREE_KINDS.contains(tp.getLeaf().getKind()))
                return true;

            tp = tp.getParentPath();
        }

        return false;
    }
    
    private static abstract class NodeToElementTask implements Runnable, CancellableTask<CompilationController>  {
        private Node node;
        private RefactoringUI ui;
        
        public NodeToElementTask(Collection<? extends Node> nodes) {
            assert nodes.size() == 1;
            this.node = nodes.iterator().next();
        }
        
        @Override
        public void cancel() {
        }
        
        @Override
        public void run(CompilationController info) throws Exception {
            info.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
            CompilationUnitTree unit = info.getCompilationUnit();
            if (unit.getTypeDecls().isEmpty()) {
                ui = createRefactoringUI(null, info);
            } else {
                TreePathHandle representedObject = TreePathHandle.create(TreePath.getPath(unit, unit.getTypeDecls().get(0)),info);
                ui = createRefactoringUI(representedObject, info);
            }
        }
        
        @Override
        public final void run() {
            DataObject o = node.getCookie(DataObject.class);
            JavaSource source = JavaSource.forFileObject(o.getPrimaryFile());
            assert source != null;
            try {
                source.runUserActionTask(this, true);
            } catch (IllegalArgumentException ex) {
                ex.printStackTrace();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            if (ui!=null) {
                UI.openRefactoringUI(ui);
            } else {
                JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_NoTypeDecls"));
            }
        }
        protected abstract RefactoringUI createRefactoringUI(TreePathHandle selectedElement, CompilationInfo info);
    }
    
    private static abstract class NodeToFileObjectTask implements Runnable, CancellableTask<CompilationController> {
        private Collection<? extends Node> nodes;
        public ArrayList<NonRecursiveFolder> pkg;
        Collection<TreePathHandle> handles = new ArrayList<TreePathHandle>();
        private Node currentNode;
     
        public NodeToFileObjectTask(Collection<? extends Node> nodes) {
            this.nodes = nodes;
        }
        
        protected int size() {
            return nodes.size();
        }
        
        @Override
        public void cancel() {
        }
        
        @Override
        public void run(CompilationController info) throws Exception {
            info.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
            Collection<TreePathHandle> handlesPerNode = new ArrayList<TreePathHandle>();
            CompilationUnitTree unit = info.getCompilationUnit();
            Collection<TreePathHandle> publicHandles = new ArrayList<TreePathHandle>();
            Collection<TreePathHandle> sameNameHandles = new ArrayList<TreePathHandle>();
            for (Tree t: unit.getTypeDecls()) {
                Element e = info.getTrees().getElement(TreePath.getPath(unit, t));
                if (e == null || !(e.getKind().isClass() || e.getKind().isInterface())) {
                    // syntax errors #111195
                    continue;
                }
                if (e.getSimpleName().toString().equals(info.getFileObject().getName())) {
                    TreePathHandle representedObject = TreePathHandle.create(TreePath.getPath(unit,t),info);
                    sameNameHandles.add(representedObject);
                }
                if (e.getModifiers().contains(Modifier.PUBLIC)) {
                    TreePathHandle representedObject = TreePathHandle.create(TreePath.getPath(unit,t),info);
                    publicHandles.add(representedObject);
                }
            }
            if (!publicHandles.isEmpty()) {
                handlesPerNode.addAll(publicHandles);
            } else {
                handlesPerNode.addAll(sameNameHandles);
            }

            if (!handlesPerNode.isEmpty()) {
                handles.addAll(handlesPerNode);
                nodeTranslated(currentNode, handlesPerNode, info);
            }
        }
        
        @Override
        public void run() {
            FileObject[] fobs = new FileObject[nodes.size()];
            pkg = new ArrayList<NonRecursiveFolder>();
            int i = 0;
            for (Node node:nodes) {
                DataObject dob = node.getCookie(DataObject.class);
                if (dob!=null) {
                    fobs[i] = dob.getPrimaryFile();
                    if (RefactoringUtils.isJavaFile(fobs[i])) {
                        JavaSource source = JavaSource.forFileObject(fobs[i]);
                        assert source != null;
                        try {
                            currentNode = node;
                            // XXX this could be optimize by ClasspasthInfo in case of more than one file
                            source.runUserActionTask(this, true);
                        } catch (IllegalArgumentException ex) {
                            Exceptions.printStackTrace(ex);
                        } catch (IOException ex) {
                            Exceptions.printStackTrace(ex);
                        } finally {
                            currentNode = null;
                        }
                    }
                    final NonRecursiveFolder nonrecursivefolder = node.getLookup().lookup(NonRecursiveFolder.class);
                    if (nonrecursivefolder !=null) {
                        pkg.add(nonrecursivefolder);
                    }
                    i++;
                }
            }
            RefactoringUI ui = createRefactoringUI(fobs, handles);
            if (ui!=null) {
                UI.openRefactoringUI(ui);
            } else {
                JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_NoTypeDecls"));
            }
        }

        /**
         * Notifies subclasses about the translation.
         * This is the place where subclasses may collect info about handles.
         * @param node node that is translated
         * @param handles handles translated from the node
         * @param javac context of running translation
         */
        protected void nodeTranslated(Node node, Collection<TreePathHandle> handles, CompilationInfo javac) {
        }

        protected abstract RefactoringUI createRefactoringUI(FileObject[] selectedElement, Collection<TreePathHandle> handles);
    }    

    
    private static boolean nodeHandle(Lookup lookup) {
        Node n = lookup.lookup(Node.class);
        if (n!=null) {
            if (n.getLookup().lookup(TreePathHandle.class)!=null) {
                return true;
            }
        }
        return false;
    }

    public static ShowUI SHOW = new ShowUI() {
        @Override
        public void show(RefactoringUI ui, TopComponent activetc) {
            if (ui!=null) {
                UI.openRefactoringUI(ui, activetc);
            } else {
                JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_CannotRenameKeyword"));
            }
        }
    };
    public interface ShowUI {
        public void show(RefactoringUI ui, TopComponent activetc);
    }    
}
