/*
 * 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.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import java.util.Collections;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.Severity;
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.NbBundle;

/**
 *
 * @author Jaroslav tulach
 */
public class UtilityClass {
    
    @Hint(id="org.netbeans.modules.java.hints.UtilityClass_1", displayName="#MSG_UtilityClass", description="#HINT_UtilityClass", category="api", enabled=false, severity=Severity.VERIFIER, suppressWarnings="UtilityClassWithoutPrivateConstructor")
    @TriggerTreeKind(Kind.CLASS)
    public static ErrorDescription utilityClass(HintContext ctx) {
        CompilationInfo compilationInfo = ctx.getInfo();
        TreePath treePath = ctx.getPath();
        Element e = compilationInfo.getTrees().getElement(treePath);
        if (e == null) {
            return null;
        }
        
        if (!isUtilityClass(compilationInfo, e)) return null;
        
        for (ExecutableElement c : ElementFilter.constructorsIn(e.getEnclosedElements())) {
            if (!compilationInfo.getElementUtilities().isSynthetic(c)) {
                return null;
            }
        }

        return ErrorDescriptionFactory.forName(ctx,
                                               treePath,
                                               NbBundle.getMessage(UtilityClass.class, "MSG_UtilityClass"),
                                               new FixImpl(true,
                                                           TreePathHandle.create(e, compilationInfo)
                                               ).toEditorFix());
    }
    
    @Hint(id="org.netbeans.modules.java.hints.UtilityClass_2", displayName="#MSG_PublicConstructor", description="#HINT_PublicConstructor", category="api", enabled=false, severity=Severity.HINT, suppressWarnings="UtilityClassWithPublicConstructor")
    @TriggerTreeKind(Kind.METHOD)
    public static ErrorDescription constructor(HintContext ctx) {
        CompilationInfo compilationInfo = ctx.getInfo();
        TreePath treePath = ctx.getPath();
        Element e = compilationInfo.getTrees().getElement(treePath);
        if (e == null) {
            return null;
        }
        if (   e.getKind() != ElementKind.CONSTRUCTOR
            || compilationInfo.getElementUtilities().isSynthetic(e)
            || (!e.getModifiers().contains(Modifier.PROTECTED) && !e.getModifiers().contains(Modifier.PUBLIC))) {
            return null;
        }
        
        if (!isUtilityClass(compilationInfo, e.getEnclosingElement())) return null;
        
        return ErrorDescriptionFactory.forName(ctx,
                                               treePath,
                                               NbBundle.getMessage(UtilityClass.class, "MSG_PublicConstructor"),
                                               new FixImpl(false,
                                                           TreePathHandle.create(e, compilationInfo)
                                               ).toEditorFix());
    }
    
    private static boolean isMainMethod(ExecutableElement m) {
        return m.getModifiers().contains(Modifier.STATIC) &&
               m.getSimpleName().contentEquals("main") &&
               (m.getReturnType().getKind() == TypeKind.VOID) &&
               (m.getParameters().size() == 1) &&
               (m.getParameters().get(0).asType().toString().equals("java.lang.String[]"));
    }

    private static boolean isUtilityClass(CompilationInfo compilationInfo, Element clazz) {
        if (clazz.getKind() != ElementKind.CLASS) {
            return false;
        }
        
        TypeMirror supr = ((TypeElement) clazz).getSuperclass();
        if (supr == null) {
            return false;
        }
        Element superElem = compilationInfo.getTypes().asElement(supr);
        if (superElem instanceof TypeElement) {
            Name superName = compilationInfo.getElements().getBinaryName((TypeElement) superElem);
            if (superName != null && !superName.contentEquals("java.lang.Object")) {
                return false;
            }
        }

        int cnt = 0;
        for (Element m : clazz.getEnclosedElements()) {
            if (m.getKind() == ElementKind.METHOD && isMainMethod(((ExecutableElement) m))) return false;
            if (m.getKind() == ElementKind.METHOD || m.getKind() == ElementKind.FIELD) {
                if (!m.getModifiers().contains(Modifier.STATIC)) return false;
                cnt++;
            }
        }

        return cnt > 0;
    }
    
    private static final class FixImpl extends JavaFix {
        private boolean clazz;

        public FixImpl(boolean clazz, TreePathHandle handle) {
            super(handle);
            this.clazz = clazz;
        }

        public String getText() {
            return NbBundle.getMessage(UtilityClass.class, clazz ? "MSG_PrivateConstructor" : "MSG_MakePrivate"); // NOI18N
        }

        @Override public String toString() {
            return "FixUtilityClass"; // NOI18N
        }

        @Override
        protected void performRewrite(TransformationContext ctx) {
            WorkingCopy wc = ctx.getWorkingCopy();
            TreePath tp = ctx.getPath();
            Element e = wc.getTrees().getElement(tp);
            if (e == null) {
                return;
            }
            Tree outer = wc.getTrees().getTree(e);
            if (clazz) {
                if (outer == null || !TreeUtilities.CLASS_TREE_KINDS.contains(outer.getKind())) {
                    return;
                }
                ClassTree cls = (ClassTree)outer;

                ModifiersTree modifiers = wc.getTreeMaker().Modifiers(Collections.singleton(Modifier.PRIVATE));
                MethodTree m = wc.getTreeMaker().Constructor(
                    modifiers,
                    Collections.<TypeParameterTree>emptyList(),
                    Collections.<VariableTree>emptyList(),
                    Collections.<ExpressionTree>emptyList(),
                    wc.getTreeMaker().Block(Collections.<StatementTree>emptyList(), false)
                );
                wc.rewrite(cls, wc.getTreeMaker().addClassMember(cls, m));
            } else {
                if (outer == null || outer.getKind() != Kind.METHOD) {
                    return;
                }
                MethodTree met = (MethodTree)outer;

                ModifiersTree modifiers = wc.getTreeMaker().Modifiers(Collections.singleton(Modifier.PRIVATE), met.getModifiers().getAnnotations());
                wc.rewrite(met.getModifiers(), modifiers);
            }
        }
    }

}
