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

import com.sun.source.util.TreePath;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.Fix;
import org.netbeans.spi.java.hints.HintContext;
import org.netbeans.spi.java.hints.JavaFix;
import org.netbeans.spi.java.hints.BooleanOption;
import org.netbeans.spi.java.hints.ConstraintVariableType;
import org.netbeans.spi.java.hints.Hint;
import org.netbeans.spi.java.hints.Hint.Options;
import org.netbeans.spi.java.hints.TriggerPattern;
import org.netbeans.spi.java.hints.TriggerPatterns;
import org.netbeans.spi.java.hints.UseOptions;
import org.netbeans.spi.java.hints.ErrorDescriptionFactory;
import org.netbeans.spi.java.hints.JavaFixUtilities;
import org.openide.util.NbBundle;

/**
 *
 * @author Jan Lahoda
 */
public class EqualsHint {

    private static final boolean ERASURE_PREFS_DEFAULT = true; // NOI18N
    @BooleanOption(displayName = "#LBL_org.netbeans.modules.java.hints.bugs.EqualsHint.ERASURE_PREFS_KEY", tooltip = "#TP_org.netbeans.modules.java.hints.bugs.EqualsHint.ERASURE_PREFS_KEY", defaultValue=ERASURE_PREFS_DEFAULT)
    private static final String ERASURE_PREFS_KEY = "eguals-hint-erasure"; // NOI18N
    
    @Hint(displayName = "#DN_org.netbeans.modules.java.hints.bugs.EqualsHint.arrayEquals", description = "#DESC_org.netbeans.modules.java.hints.bugs.EqualsHint.arrayEquals", category="bugs", suppressWarnings="ArrayEquals")
    @TriggerPatterns({
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="java.lang.Object[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="boolean[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="byte[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="short[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="char[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="int[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="long[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="float[]")
                        }),
        @TriggerPattern(value="$obj.equals($arr)",
                        constraints={
                            @ConstraintVariableType(variable="$obj", type="java.lang.Object"),
                            @ConstraintVariableType(variable="$arr", type="double[]")
                        })
    })
    public static ErrorDescription arrayEquals(HintContext ctx) {
        //XXX: this check should not be needed:
        TreePath arr = ctx.getVariables().get("$arr");
        TypeMirror tm = ctx.getInfo().getTrees().getTypeMirror(arr);

        if (tm == null || tm.getKind() != TypeKind.ARRAY) {
            return null;
        }
        //XXX end

        String fixArraysDisplayName = NbBundle.getMessage(EqualsHint.class, "FIX_ReplaceWithArraysEquals");
        Fix arrays = JavaFixUtilities.rewriteFix(ctx, fixArraysDisplayName, ctx.getPath(), "java.util.Arrays.equals($obj, $arr)");
        String fixInstanceDisplayName = NbBundle.getMessage(EqualsHint.class, "FIX_ReplaceWithInstanceEquals");
        Fix instance = JavaFixUtilities.rewriteFix(ctx, fixInstanceDisplayName, ctx.getPath(), "$obj == $arr");
        String displayName = NbBundle.getMessage(EqualsHint.class, "ERR_ARRAY_EQUALS");

        return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName, arrays, instance);
    }
    
    @Hint(displayName = "#DN_org.netbeans.modules.java.hints.EqualsHint", description = "#DESC_org.netbeans.modules.java.hints.EqualsHint", id="org.netbeans.modules.java.hints.EqualsHint", category="bugs", suppressWarnings={"IncompatibleEquals", "", "EqualsBetweenInconvertibleTypes"}, options=Options.QUERY)
    @UseOptions(ERASURE_PREFS_KEY)
    @TriggerPattern(value="$this.equals($par)",
                    constraints={
                        @ConstraintVariableType(variable="$this", type="java.lang.Object"),
                        @ConstraintVariableType(variable="$par", type="java.lang.Object")
                    })
    public static ErrorDescription incompatibleEquals(HintContext ctx) {
        TreePath ths = ctx.getVariables().get("$this");
        TreePath par = ctx.getVariables().get("$par");
        TypeMirror thsType;

        if (ths != null) {
            thsType = ctx.getInfo().getTrees().getTypeMirror(ths);
        } else {
            TreePath cls = ctx.getPath();

            while (cls != null && !TreeUtilities.CLASS_TREE_KINDS.contains(cls.getLeaf().getKind())) {
                cls = cls.getParentPath();
            }

            if (cls == null) {
                return null;
            }

            thsType = ctx.getInfo().getTrees().getTypeMirror(cls);
        }
        if (thsType == null || thsType.getKind() != TypeKind.DECLARED) {
            return null;
        }
        
        TypeMirror parType = ctx.getInfo().getTrees().getTypeMirror(par);
        if (parType == null || parType.getKind() != TypeKind.DECLARED) {
            return null;
        }
        if (ctx.getPreferences().getBoolean(ERASURE_PREFS_KEY, ERASURE_PREFS_DEFAULT)) {
            Types types = ctx.getInfo().getTypes();
            thsType = types.erasure(thsType);
            parType = types.erasure(parType);
        }
        boolean castable = ctx.getInfo().getTypeUtilities().isCastable(thsType, parType) || ctx.getInfo().getTypeUtilities().isCastable(parType, thsType);

        if (castable) {
            return null;
        }

        String displayName = NbBundle.getMessage(EqualsHint.class, "ERR_INCOMPATIBLE_EQUALS"); // NOI18N
        
        return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName);
    }

}
