/*
 * 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.beans.addproperty;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.swing.JButton;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.StyledDocument;
import org.netbeans.api.editor.guards.GuardedSection;
import org.netbeans.api.editor.guards.GuardedSectionManager;
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.source.CodeStyle;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.editor.GuardedException;
import org.netbeans.modules.editor.indent.api.Reformat;
import org.netbeans.spi.editor.codegen.CodeGenerator;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;

/**
 *
 * @author lahvac
 */
public class AddPropertyCodeGenerator implements CodeGenerator {

    private JTextComponent component;
    private String className;
    private List<String> existingFields;
    private String[] pcsName;
    private String[] vcsName;

    public AddPropertyCodeGenerator(JTextComponent component, String className, List<String> existingFields, String[] pcsName, String[] vcsName) {
        this.component = component;
        this.className = className;
        this.existingFields = existingFields;
        this.pcsName = pcsName;
        this.vcsName = vcsName;
    }

    public String getDisplayName() {
        return NbBundle.getMessage(AddPropertyCodeGenerator.class, "DN_AddProperty");
    }

    public void invoke() {
        Object o = component.getDocument().getProperty(Document.StreamDescriptionProperty);

        if (o instanceof DataObject) {
            DataObject d = (DataObject) o;

            perform(d.getPrimaryFile(), component);
        }
    }

    public void perform(FileObject file, JTextComponent pane) {
        JButton ok = new JButton(NbBundle.getMessage(AddPropertyCodeGenerator.class, "LBL_ButtonOK"));
        CodeStyle cs = CodeStyle.getDefault(pane.getDocument());
        if(cs == null) {
            cs = CodeStyle.getDefault(file);
        }
        final AddPropertyPanel addPropertyPanel = new AddPropertyPanel(file, className, cs, existingFields, pcsName, vcsName, ok);
        String caption = NbBundle.getMessage(AddPropertyCodeGenerator.class, "CAP_AddProperty");
        String cancel = NbBundle.getMessage(AddPropertyCodeGenerator.class, "LBL_ButtonCancel");
        DialogDescriptor dd = new DialogDescriptor(addPropertyPanel,caption, true, new Object[] {ok,cancel}, ok, DialogDescriptor.DEFAULT_ALIGN, null, null);
        if (DialogDisplayer.getDefault().notify(dd) == ok) {
            insertCode2(file, pane, addPropertyPanel.getAddPropertyConfig(), cs);
        }
    }

    /**
     * work around for {@link #insertCode}.
     * @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=162853">162853</a>
     * @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=162630">162630</a>
     */
    static void insertCode2(final FileObject file, final JTextComponent pane, final AddPropertyConfig config, CodeStyle cs) {
            final Document doc = pane.getDocument();
            final Reformat r = Reformat.get(pane.getDocument());
            final String code = new AddPropertyGenerator().generate(config, cs);
            final Position[] bounds = new Position[2];
            
            final int offset[] = new int[2];
            
        try {
            JavaSource.forFileObject(file).runUserActionTask(new Task<CompilationController>() {
                @Override
                public void run(CompilationController parameter) throws Exception {
                    parameter.toPhase(Phase.PARSED);
                    Trees trees = parameter.getTrees();
                    TreeUtilities treeUtils = parameter.getTreeUtilities();
                    offset[0] = pane.getCaretPosition();
                    offset[1] = -1;
                    TreePath path = parameter.getTreeUtilities().pathFor(offset[0]);
                    
                    if (path==null || path.getLeaf().getKind() == Tree.Kind.CLASS) {
                        return;
                    }
                    CompilationUnitTree cut = path.getCompilationUnit();
                    if(path.getLeaf().getKind() != Tree.Kind.ENUM) {
                        while (path != null && path.getParentPath()!=null &&
                                (path.getParentPath().getLeaf().getKind() != Tree.Kind.CLASS &&
                                path.getParentPath().getLeaf().getKind() != Tree.Kind.ENUM)) {
                            path = path.getParentPath();
                        }
                    }
                    
                    int enumconstantEnd = -1;
                    int otherStart = -1;
                    if(path.getLeaf().getKind() == Tree.Kind.ENUM ||
                            (path.getParentPath() != null &&
                            path.getParentPath().getLeaf().getKind() == Tree.Kind.ENUM)) {
                        TreePath clazzPath = path.getLeaf().getKind() != Tree.Kind.ENUM ? path.getParentPath() : path;
                        ClassTree clazz = (ClassTree) clazzPath.getLeaf();
                        for (Tree tree : clazz.getMembers()) {
                            TreePath treePath = new TreePath(clazzPath, tree);
                            if(treeUtils.isSynthetic(treePath)) {
                                continue;
                            }
                            Element element;
                            if(tree.getKind() == Tree.Kind.VARIABLE && (element = trees.getElement(treePath)) != null &&
                                   element.getKind() == ElementKind.ENUM_CONSTANT) {
                                int endPosition = (int) trees.getSourcePositions().getEndPosition(cut, tree);
                                enumconstantEnd = Math.max(enumconstantEnd, endPosition);
                            } else if(otherStart == -1) {
                                otherStart = (int) trees.getSourcePositions().getStartPosition(cut, tree);
                            }
                        }
                        
                        if(enumconstantEnd == -1) {
                            enumconstantEnd = treeUtils.findBodySpan(clazz)[0] + 1;
                        }
                        
                        if(otherStart == -1) {
                            otherStart = (int) trees.getSourcePositions().getEndPosition(cut, clazz);
                        }
                        
                        int semicolon = scanForSemicolon(doc, offset, enumconstantEnd, otherStart);

                        if (semicolon == -1) {
                            offset[1] = enumconstantEnd;
                            if (offset[0] <= enumconstantEnd) {
                                offset[0] = enumconstantEnd + 1;
                            }
                        } else {
                            if (offset[0] <= semicolon) {
                                offset[0] = semicolon + 1;
                            } else if(path.getLeaf().getKind() != Tree.Kind.ENUM) {
                                Tree current = path.getLeaf();
                                offset[0] = (int) trees.getSourcePositions().getEndPosition(cut, current);
                            }
                        }
                        return;
                    }
                    
                    Tree current = path.getLeaf();
                    offset[0] = (int) trees.getSourcePositions().getEndPosition(cut, current);
                }
            }, true);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
            
            r.lock();
            try {
                NbDocument.runAtomicAsUser((StyledDocument) doc, new Runnable() {
                    public void run() {
                        try {
                            GuardedSectionManager manager = GuardedSectionManager.getInstance((StyledDocument) doc);
                            if (manager != null) {
                                for (GuardedSection guard : manager.getGuardedSections()) {
                                    if (guard.contains(doc.createPosition(offset[0]), true)) {
                                        offset[0] = guard.getEndPosition().getOffset() + 1;
                                        break;
                                    }
                                }
                            }
                            
                            doc.insertString(offset[0], code, null);
                            if(offset[1] != -1) {
                                doc.insertString(offset[1], ";", null);
                            }
                            Position start = doc.createPosition(offset[0]);
                            Position end = doc.createPosition(offset[0] + code.length());
//                            r.reformat(Utilities.getRowStart(pane, start.getOffset()), Utilities.getRowEnd(pane, end.getOffset()));
                            r.reformat(start.getOffset(), end.getOffset());
                            bounds[0] = start;
                            bounds[1] = end;
                        } catch (GuardedException ex) {
                            //workaround for bug 205193
                        } catch (BadLocationException ex) {
                            Exceptions.printStackTrace(ex);
                        }
                    }
                });

            } catch (BadLocationException ex) {
                Exceptions.printStackTrace(ex);
            } finally {
                r.unlock();
            }

            if (bounds[0] != null) {
                // code insertion to document passed
                try {
                    JavaSource.forFileObject(file).runModificationTask(new Task<WorkingCopy>() {
                        public void run(WorkingCopy workingCopy) throws Exception {
                            workingCopy.toPhase(Phase.RESOLVED);

                            Position start = bounds[0];
                            Position end = bounds[1];
                            
                            new ImportFQNsHack(workingCopy, start.getOffset(), end.getOffset()).scan(workingCopy.getCompilationUnit(), null);

                            CompilationUnitTree cut = workingCopy.getCompilationUnit();

                            workingCopy.rewrite(cut, workingCopy.getTreeMaker().CompilationUnit(cut.getPackageAnnotations(), cut.getPackageName(), cut.getImports(), cut.getTypeDecls(), cut.getSourceFile()));
                        }
                    }).commit();
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
    }
    
    private static int scanForSemicolon(Document doc, int[] offset, int start, int end) throws BadLocationException {
        TokenHierarchy<Document> th = doc != null ? TokenHierarchy.get(doc) : null;
        List<TokenSequence<?>> embeddedSequences = th != null ? th.embeddedTokenSequences(offset[0], false) : null;
        TokenSequence<?> seq = embeddedSequences != null ? embeddedSequences.get(embeddedSequences.size() - 1) : null;

        if (seq == null) {
            return offset[0];
        }   

        seq.move(start);

        int semicolon = -1;
        while(seq.moveNext()) {
            int tokenOffset = seq.offset();
            if(tokenOffset > end) {
                break;
            }
            Token<?> t = seq.token();
            if(t != null && t.id() == JavaTokenId.SEMICOLON ) {
                semicolon = tokenOffset;
                break;
            }
        }
        return semicolon;
    }

    private static final class ImportFQNsHack extends ErrorAwareTreePathScanner<Void, Void> {

        private WorkingCopy wc;
        private int start;
        private int end;

        public ImportFQNsHack(WorkingCopy wc, int start, int end) {
            this.wc = wc;
            this.start = start;
            this.end = end;
        }

        @Override
        public Void visitMemberSelect(MemberSelectTree node, Void p) {
            int s = (int) wc.getTrees().getSourcePositions().getStartPosition(wc.getCompilationUnit(), node);
            int e = (int) wc.getTrees().getSourcePositions().getEndPosition(wc.getCompilationUnit(), node);

            if (s >= start && e <= end) {
                Element el = wc.getTrees().getElement(getCurrentPath());

                if (el != null && (el.getKind().isClass() || el.getKind().isInterface()) && ((TypeElement) el).asType().getKind() != TypeKind.ERROR) {
                    wc.rewrite(node, wc.getTreeMaker().QualIdent(el));
                    return null;
                }
            }

            return super.visitMemberSelect(node, p);
        }

        @Override
        public Void visitClass(ClassTree node, Void p) {
            final SourcePositions sourcePositions = wc.getTrees().getSourcePositions();
            final TreeMaker make = wc.getTreeMaker();
            List<Tree> members = new LinkedList<Tree>();
            ClassTree classTree = node;
            for (Tree member : node.getMembers()) {
                int s = (int) sourcePositions.getStartPosition(wc.getCompilationUnit(), member);
                int e = (int) sourcePositions.getEndPosition(wc.getCompilationUnit(), member);
                if (s >= start && e <= end) {
                    classTree = make.removeClassMember(classTree, member);
                    members.add(member);
                }
            }
            classTree = GeneratorUtils.insertClassMembers(wc, classTree, members, start);
            wc.rewrite(node, classTree);
            return super.visitClass(classTree, p);
        }
    }

    public static final class Factory implements CodeGenerator.Factory {

        public List<? extends CodeGenerator> create(Lookup context) {
            JTextComponent component = context.lookup(JTextComponent.class);
            CompilationController cc = context.lookup(CompilationController.class);
            TreePath path = context.lookup(TreePath.class);
            while (path != null && !TreeUtilities.CLASS_TREE_KINDS.contains(path.getLeaf().getKind())) {
                path = path.getParentPath();
            }

            if (component == null || cc == null || path == null) {
                return Collections.emptyList();
            }
            
            //find PropertyChangeSupport, VetoableChangeSupport
            //list all fields to detect collisions
            Element e = cc.getTrees().getElement(path);
            
            if (e == null || !e.getKind().isClass()) {
                return Collections.emptyList();
        }
        
            TypeMirror pcs = resolve(cc, "java.beans.PropertyChangeSupport"); //NOI18N
            TypeMirror vcs = resolve(cc, "java.beans.VetoableChangeSupport"); //NOI18N
            
            if (pcs == null || vcs == null) {
                return Collections.emptyList();
            }
            
            List<String> existingFields = new LinkedList<String>();
            String[] pcsName = new String[2];
            String[] vcsName = new String[2];
            
            for (VariableElement field : ElementFilter.fieldsIn(e.getEnclosedElements())) {
                existingFields.add(field.getSimpleName().toString());
                
                if (field.asType().equals(pcs)) {
                    int i = field.getModifiers().contains(Modifier.STATIC) ? 1 : 0;
                    
                    pcsName[i] = field.getSimpleName().toString();
                }
                
                if (field.asType().equals(vcs)) {
                    int i = field.getModifiers().contains(Modifier.STATIC) ? 1 : 0;

                    vcsName[i] = field.getSimpleName().toString();
                }
            }
            
            String className = ((TypeElement) e).getQualifiedName().toString();
            
            return Collections.singletonList(new AddPropertyCodeGenerator(component, className, existingFields, pcsName,vcsName));
        }
        
        private static TypeMirror resolve(CompilationInfo info, String s) {
            TypeElement te = info.getElements().getTypeElement(s);
            
            if (te == null) return null;
            
            return te.asType();
        }
    }
};
