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

import com.sun.source.tree.*;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.netbeans.api.java.source.*;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.modules.refactoring.api.Problem;
import org.netbeans.modules.refactoring.java.api.InvertBooleanRefactoring;
import org.netbeans.modules.refactoring.java.api.JavaRefactoringUtils;
import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
import org.netbeans.modules.refactoring.spi.RefactoringPlugin;
import org.netbeans.spi.java.hints.support.TransformationSupport;
import org.openide.util.Exceptions;
import org.openide.util.MapFormat;
import org.openide.util.NbBundle;
import static org.netbeans.modules.refactoring.java.plugins.Bundle.*;

/**
 *
 * @author lahvac
 */
public class InvertBooleanRefactoringPlugin implements RefactoringPlugin { //extends JackpotBasedRefactoring {

    private final InvertBooleanRefactoring invertBooleanRefactoring;
    
    protected final AtomicBoolean cancel = new AtomicBoolean();
    
    public InvertBooleanRefactoringPlugin(InvertBooleanRefactoring invertBooleanRefactoring) {
        this.invertBooleanRefactoring = invertBooleanRefactoring;
    }
    
    @Override
    @NbBundle.Messages({"ERR_InvertMethodPolymorphic=Cannot invert polymorphic method.",
        "ERR_InvertMethodInInterface=Cannot invert method from interface.",
        "ERR_InvertMethodAbstract=Cannot invert abstract method."})
    public Problem preCheck() {
        final TreePathHandle treePathHandle = this.invertBooleanRefactoring.getRefactoringSource().lookup(TreePathHandle.class);
        JavaSource js = JavaSource.forFileObject(treePathHandle.getFileObject());
        if (js == null) {
            return null;
        }
        final Problem preCheckProblem[] = {null};
        try {
            js.runUserActionTask(new Task<CompilationController>() {

                @Override
                public void run(CompilationController javac) throws Exception {
                    javac.toPhase(JavaSource.Phase.RESOLVED);
                    Element element = treePathHandle.resolveElement(javac);
                    preCheckProblem[0] = isElementAvail(treePathHandle, javac);
                    if (preCheckProblem[0] != null) {
                        return;
                    }

                    preCheckProblem[0] = JavaPluginUtils.isSourceElement(element, javac);
                    if (preCheckProblem[0] != null) {
                        return;
                    }

                    switch (element.getKind()) {
                        case METHOD:
                            // Method can not be in annotation or interface

                            if (element.getEnclosingElement().getKind().isInterface()) {
                                preCheckProblem[0] = createProblem(preCheckProblem[0], true, ERR_InvertMethodInInterface());
                                return;
                            }
                            if (element.getModifiers().contains(Modifier.ABSTRACT)) {
                                preCheckProblem[0] = createProblem(preCheckProblem[0], true, ERR_InvertMethodAbstract());
                                return;
                            }
                            // Method can not be polymorphic
                            Collection<ExecutableElement> overridenMethods = JavaRefactoringUtils.getOverriddenMethods((ExecutableElement) element, javac);
                            Collection<ExecutableElement> overridingMethods = JavaRefactoringUtils.getOverridingMethods((ExecutableElement) element, javac, new AtomicBoolean());
                            if (overridenMethods.size() > 0 || overridingMethods.size() > 0) {
                                preCheckProblem[0] = createProblem(preCheckProblem[0], true, ERR_InvertMethodPolymorphic());
                                return;
                            }
                    }
                }
            }, true);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        return preCheckProblem[0];
    }
    
    /**
     * Copy from JavaRefactoringPlugin.
     */
    private Problem createProblem(Problem result, boolean isFatal, String message) {
        Problem problem = new Problem(isFatal, message);
        return JavaPluginUtils.chainProblems(result, problem);
    }

    /**
     * Copy from JavaRefactoringPlugin.
     */
    private Problem isElementAvail(TreePathHandle e, CompilationInfo info) {
        if (e==null) {
            //element is null or is not valid.
            return new Problem(true, NbBundle.getMessage(FindVisitor.class, "DSC_ElNotAvail")); // NOI18N
        } else {
            Element el = e.resolveElement(info);
            String elName = el != null ? el.getSimpleName().toString() : null;
            if (el == null || el.asType().getKind() == TypeKind.ERROR || "<error>".equals(elName)) { // NOI18N
                return new Problem(true, NbBundle.getMessage(FindVisitor.class, "DSC_ElementNotResolved"));
            }
            
            if ("this".equals(elName) || "super".equals(elName)) { // NOI18N
                return new Problem(true, NbBundle.getMessage(FindVisitor.class, "ERR_CannotRefactorThis", el.getSimpleName()));
            }
            
            // element is still available
            return null;
        }
    }
    
    @Override
    public Problem checkParameters() {
        return null;
    }

    @Override
    public Problem fastCheckParameters() {
        String name = invertBooleanRefactoring.getNewName();
        
        if (name == null || name.length() == 0 || !SourceVersion.isIdentifier(name)) {
            return new Problem(true, NbBundle.getMessage(InvertBooleanRefactoringPlugin.class, "ERR_InvalidIdentifier", name));
        }
        return null;
    }

    private static final String INVERT_FIXES =
            "=> ${newName-with-enclosing}$ $left != $right; :: matchesWithBind($val, \"$left == $right\")\n" +
            "=> ${newName-with-enclosing}$ $left == $right; :: matchesWithBind($val, \"$left != $right\")\n" +
            "=> ${newName-with-enclosing}$ $op; :: matchesWithBind($val, \"!($op)\")" +
            "=> ${newName-with-enclosing}$ $op; :: matchesWithBind($val, \"(!$op)\")" +
            "=> ${newName-with-enclosing}$ $op; :: !matchesAny($val, \"!($op)\") && matchesWithBind($val, \"!$op\")\n" +
            "=> ${newName-with-enclosing}$ false; :: matchesAny($val, \"true\")\n" +
            "=> ${newName-with-enclosing}$ true; :: matchesAny($val, \"false\")\n" +
            "=> ${newName-with-enclosing}$ !$val; :: otherwise\n";

    private static final String VAR_SCRIPT_TEMPLATE =
            "   $enclosing.${originalName}$ :: $enclosing instanceof ${enclosing}$ && !parentMatches(\"$enclosing.${originalName}$ = $newVal\") && !parentMatches(\"!$enclosing.${originalName}$\")\n" +
            "=> !$enclosing.${newName}$\n" +
            ";;\n" +
            "   !$enclosing.${originalName}$ :: $enclosing instanceof ${enclosing}$\n" +
            "=> $enclosing.${newName}$\n" +
            ";;\n" +
            "   $enclosing.${originalName}$ = $val :: $enclosing instanceof ${enclosing}$ && !matchesAny($val, \"!$enclosing.${originalName}$\")\n" +
            INVERT_FIXES.replace(";", "").replace("${newName-with-enclosing}$", "$enclosing.${newName}$ =") +
            ";;\n";

    private static final String VAR_SCRIPT_TEMPLATE_STATIC =
            "   ${enclosing}$.${originalName}$ :: !parentMatches(\"$enclosing.${originalName}$ = $newVal\") && !parentMatches(\"!$enclosing.${originalName}$\")\n" +
            "=> !${enclosing}$.${newName}$\n" +
            ";;\n" +
            "   !${enclosing}$.${originalName}$\n" +
            "=> ${enclosing}$.${newName}$\n" +
            ";;\n" +
            "   ${enclosing}$.${originalName}$ = $val :: !matchesAny($val, \"!$enclosing.${originalName}$\")\n" +
            INVERT_FIXES.replace(";", "").replace("${newName-with-enclosing}$", "${enclosing}$.${newName}$ =") +
            ";;\n";

    private static final String VAR_INIT =
            "   $mods$ $type ${originalName}$ = $val;" +
            INVERT_FIXES.replace("${newName-with-enclosing}$", "$mods$ $type ${newName}$  =") +
            ";;";

    private static final String MTH_INIT =
            "   return $val;" +
            INVERT_FIXES.replace("${newName-with-enclosing}$", "return ") +
            ";;";


    public final Problem prepare(RefactoringElementsBag refactoringElements) {
        cancel.set(false);
//        TreePathHandle tph = refactoring.getRefactoringSource().lookup(TreePathHandle.class);
//        FileObject file = tph.getFileObject();
//        ClassPath source = ClassPath.getClassPath(file, ClassPath.SOURCE);
//        FileObject sourceRoot = source.findOwnerRoot(file);
        final TreePathHandle original = invertBooleanRefactoring.getRefactoringSource().lookup(TreePathHandle.class);
        final String[] ruleCode = new String[1];
        final String[] toCode = new String[1];

        try {
            ModificationResult mod = JavaSource.forFileObject(original.getFileObject()).runModificationTask(new Task<WorkingCopy>() {

                @Override
                public void run(final WorkingCopy parameter) throws Exception {
                   parameter.toPhase(Phase.RESOLVED);

                    final TreePath path = original.resolve(parameter);
                    Map<String, String> arguments = new HashMap<String, String>();
                    String scriptTemplate;
                    Tree leaf = path.getLeaf();
                    TypeElement parent = (TypeElement) parameter.getTrees().getElement(path.getParentPath());
                    // XXX: parent should be checked ?
                    arguments.put("newName", invertBooleanRefactoring.getNewName());
                    arguments.put("enclosing", parent.getQualifiedName().toString());

                    if (leaf.getKind() == Kind.VARIABLE) {
                        VariableTree var = (VariableTree) leaf;

                        scriptTemplate = var.getModifiers().getFlags().contains(Modifier.STATIC) ? VAR_SCRIPT_TEMPLATE_STATIC : VAR_SCRIPT_TEMPLATE;
                        arguments.put("originalName", var.getName().toString());

                        if (var.getInitializer() != null) {
                            MapFormat format = new MapFormat(arguments);
                            format.setLeftBrace("${");
                            format.setRightBrace("}$");
                            String initFormat = format.format(VAR_INIT);

                            TransformationSupport.create(initFormat).setCancel(cancel).transformTreePath(parameter, path);
                        }
                    } else if (leaf.getKind() == Kind.METHOD) {
                        MethodTree mt = (MethodTree) leaf;

                        arguments.put("originalName", mt.getName().toString());

                        MapFormat format = new MapFormat(arguments);
                        format.setLeftBrace("${");
                        format.setRightBrace("}$");
                        final String mthFormat = format.format(MTH_INIT);

                        new ErrorAwareTreePathScanner<Void, Void>() {
                            @Override public Void visitReturn(ReturnTree node, Void p) {
                                TransformationSupport.create(mthFormat).setCancel(cancel).transformTreePath(parameter, getCurrentPath());
                                return super.visitReturn(node, p);
                            }
                            @Override public Void visitClass(ClassTree node, Void p) {
                                return null;
                            }
                        }.scan(path, null);

                        parameter.rewrite(leaf, parameter.getTreeMaker().setLabel(leaf, invertBooleanRefactoring.getNewName()));

                        StringBuilder parameters = new StringBuilder();
                        StringBuilder constraints = new StringBuilder();
                        int count = 1;
                        for (VariableTree vt : mt.getParameters()) {
                            if (count > 1) {
                                parameters.append(", ");
                                constraints.append(" && ");
                            }
                            parameters.append("$").append(count);
                            TypeMirror type = parameter.getTrees().getTypeMirror(new TreePath(new TreePath(path, vt), vt.getType()));
                            type = parameter.getTypes().erasure(type);
                            constraints.append("$").append(count).append(" instanceof ").append(type);
                            count++;
                        }

                        String andConstraints = (constraints.length() > 0 ? " && " : "") + constraints;

                        StringBuilder script = new StringBuilder();

                        if (mt.getModifiers().getFlags().contains(Modifier.STATIC)) {
                            script.append("   ${enclosing}$.<$T$>${originalName}$(").append(parameters).append(") :: !parentMatches(\"!$enclosing.${originalName}$($args$)\")").append(andConstraints);
                            script.append("=> !${enclosing}$.<$T$>${newName}$(").append(parameters).append(") :: !parentMatches(\"$enclosing.${originalName}$($args$);\")");
                            script.append("=> ${enclosing}$.<$T$>${newName}$(").append(parameters).append(")\n");
                            script.append(";;\n");
                            script.append("   !${enclosing}$.<$T$>${originalName}$(").append(parameters).append(") :: ").append(constraints);
                            script.append("=> ${enclosing}$.<$T$>${newName}$(").append(parameters).append(")\n");
                            script.append(";;\n");
                        } else {
                            script.append("   $enclosing.<$T$>${originalName}$(").append(parameters).append(") :: $enclosing instanceof ${enclosing}$ && !parentMatches(\"!$enclosing.${originalName}$($args$)\")").append(andConstraints);
                            script.append("=> !$enclosing.<$T$>${newName}$(").append(parameters).append(") :: !parentMatches(\"$enclosing.${originalName}$($args$);\")");
                            script.append("=> $enclosing.<$T$>${newName}$(").append(parameters).append(")\n");
                            script.append(";;\n");
                            script.append("   !$enclosing.<$T$>${originalName}$(").append(parameters).append(") :: $enclosing instanceof ${enclosing}$ ").append(andConstraints);
                            script.append("=> $enclosing.<$T$>${newName}$(").append(parameters).append(")\n");
                            script.append(";;\n");
                        }

                        scriptTemplate = script.toString();
                    } else {
                        throw new UnsupportedOperationException();
                    }

                    MapFormat format = new MapFormat(arguments);

                    format.setLeftBrace("${");
                    format.setRightBrace("}$");

                    ruleCode[0] = format.format(scriptTemplate);
                }
            });

            List<ModificationResult> results = new ArrayList<ModificationResult>();

            results.add(mod);

            results.addAll(TransformationSupport.create(ruleCode[0]).setCancel(cancel).processAllProjects());
            ReplaceConstructorWithBuilderPlugin.createAndAddElements(invertBooleanRefactoring, refactoringElements, results);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }

        return null/*XXX*/;
    }
    

    @Override
    public void cancelRequest() {
        cancel.set(true);
    }

}
