package com.github.piasy.safelyandroid.lint;

import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import java.util.Arrays;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Return;
import lombok.ast.This;

/**
 * Created by Piasy{github.com/Piasy} on 16/4/16.
 */
public class UnsafeAndroidDetector extends Detector implements Detector.JavaScanner {
    public static final String DISMISS = "dismiss";
    public static final String COMMIT = "commit";
    public static final String DIALOG_FRAGMENT = "android.app.DialogFragment";
    public static final String V4_DIALOG_FRAGMENT = "android.support.v4.app.DialogFragment";

    public static final String DESCRIPTION_UNSAFE_DISMISS = "Dismiss dialog fragment unsafely";
    public static final String DESCRIPTION_UNSAFE_COMMIT = "Commit fragment transaction unsafely";

    public static final Issue ISSUE_UNSAFE_DISMISS =
            Issue.create("UnsafeDismiss", DESCRIPTION_UNSAFE_DISMISS,
                    "You should use safely way to dismiss dialog fragment to avoid activity state" +
                            " loss.", Category.CORRECTNESS, 8, Severity.ERROR,
                    new Implementation(UnsafeAndroidDetector.class, Scope.JAVA_FILE_SCOPE));
    public static final Issue ISSUE_UNSAFE_COMMIT =
            Issue.create("UnsafeCommit", DESCRIPTION_UNSAFE_COMMIT,
                    "You should use safely way to commit fragment transaction to avoid activity " +
                            "state loss.", Category.CORRECTNESS, 8, Severity.ERROR,
                    new Implementation(UnsafeAndroidDetector.class, Scope.JAVA_FILE_SCOPE));

    @Override
    public List<String> getApplicableMethodNames() {
        return Arrays.asList(DISMISS, COMMIT);
    }

    @Override
    public void visitMethod(JavaContext context, AstVisitor visitor, MethodInvocation node) {
        String name = node.astName().astValue();
        if (DISMISS.equals(name)) {
            checkUnsafeDismiss(context, node);
        } else {
            checkUnsafeCommit(context, node);
        }
    }

    private void checkUnsafeCommit(JavaContext context, MethodInvocation node) {
        /*System.out.println(context.getLocation(node).getFile().getAbsolutePath() + ":" +
                context.getLocation(node).getStart().getLine() + ", " + node);

        System.out.println(node.rawOperand());
        System.out.println(node.rawOperand().getClass());
        for (Node child : node.rawOperand().getChildren()) {
            System.out.println("child: " + child);
        }
        System.out.println(node.rawOperand().getGeneratedBy());
        System.out.println(node.rawOperand().getNativeNode());
        System.out.println(node.rawOperand().getNativeNode().getClass());
        System.out.println(context.resolve(node.rawOperand()));
        System.out.println(context.getType(node.rawOperand()));
        System.out.println(JavaContext.getMethodName(node.rawOperand()));

        // TODO: 16/4/17
         can't get the return type of node.rawOperand(), otherwise we can check whether
         it's FragmentTransaction. after the ast is built, we should could know the return type
         of a method call.

        do {
            if (node.astOperand() == null) {
                break;
            }
            Object resolvedNode = context.resolve(node.astOperand());
            System.out.println(resolvedNode);
            if (!(resolvedNode instanceof JavaParser.ResolvedVariable)) {
                break;
            }
            JavaParser.ResolvedVariable variable = (JavaParser.ResolvedVariable) resolvedNode;
            if (isDialogFragment(variable.getType().getTypeClass())) {
                context.report(ISSUE_UNSAFE_DISMISS, node, context.getLocation(node),
                        DESCRIPTION_UNSAFE_DISMISS);
                return;
            }
        } while (false);

        Object resolvedNode = context.resolve(node);
        System.out.println(resolvedNode);
        if (!(resolvedNode instanceof JavaParser.ResolvedMethod)) {
            return;
        }
        JavaParser.ResolvedMethod resolvedMethod = (JavaParser.ResolvedMethod) resolvedNode;
        if (isDialogFragment(resolvedMethod.getContainingClass())) {
            context.report(ISSUE_UNSAFE_DISMISS, node, context.getLocation(node),
                    DESCRIPTION_UNSAFE_DISMISS);
        }*/
    }


    private static class ReceiverFinder extends ForwardingAstVisitor {
        private final MethodInvocation mTarget;
        private boolean mFound;
        private boolean mSeenTarget;

        private ReceiverFinder(MethodInvocation target) {
            this.mTarget = target;
        }

        @Override
        public boolean visitMethodInvocation(MethodInvocation node) {
            System.out.println("ReceiverFinder::visitMethodInvocation " + node);
            if(node == this.mTarget) {
                this.mSeenTarget = true;
            } else if((this.mSeenTarget || node.astOperand() == this.mTarget) && "show".equals(node.astName().astValue())) {
                this.mFound = true;
            }

            return true;
        }

        @Override
        public boolean visitReturn(Return node) {
            if(node.astValue() == this.mTarget) {
                this.mFound = true;
            }

            return super.visitReturn(node);
        }
    }

    private void checkUnsafeDismiss(JavaContext context, MethodInvocation node) {
        if (node.rawOperand() == null && isInsideDialogFragment(context, node)) {
            context.report(ISSUE_UNSAFE_DISMISS, node, context.getLocation(node),
                    DESCRIPTION_UNSAFE_DISMISS);
            return;
        }

        do {
            if (node.astOperand() == null) {
                break;
            }
            if (node.astOperand() instanceof This) {
                This thiz = (This) node.astOperand();
                Object resolvedNode = context.resolve(thiz.astQualifier());
                if (resolvedNode instanceof JavaParser.ResolvedClass &&
                        isDialogFragment((JavaParser.ResolvedClass) resolvedNode)) {
                    context.report(ISSUE_UNSAFE_DISMISS, node, context.getLocation(node),
                            DESCRIPTION_UNSAFE_DISMISS);
                    return;
                }
            }
            Object resolvedNode = context.resolve(node.astOperand());
            if (!(resolvedNode instanceof JavaParser.ResolvedVariable)) {
                break;
            }
            JavaParser.ResolvedVariable variable = (JavaParser.ResolvedVariable) resolvedNode;
            if (isDialogFragment(variable.getType().getTypeClass())) {
                context.report(ISSUE_UNSAFE_DISMISS, node, context.getLocation(node),
                        DESCRIPTION_UNSAFE_DISMISS);
                return;
            }
        } while (false);

        Object resolvedNode = context.resolve(node);
        if (!(resolvedNode instanceof JavaParser.ResolvedMethod)) {
            return;
        }
        JavaParser.ResolvedMethod resolvedMethod = (JavaParser.ResolvedMethod) resolvedNode;
        if (isDialogFragment(resolvedMethod.getContainingClass())) {
            context.report(ISSUE_UNSAFE_DISMISS, node, context.getLocation(node),
                    DESCRIPTION_UNSAFE_DISMISS);
        }
    }

    private boolean isInsideDialogFragment(JavaContext context, MethodInvocation node) {
        Node parent = node.getParent();
        while (parent != null) {
            Object resolvedNode = context.resolve(parent);
            if (resolvedNode instanceof JavaParser.ResolvedMethod) {
                JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolvedNode;
                if (isDialogFragment(method.getContainingClass())) {
                    return true;
                }
            }
            parent = parent.getParent();
        }
        return false;
    }

    private boolean isDialogFragment(JavaParser.ResolvedClass clazz) {
        JavaParser.ResolvedClass superClazz = clazz;
        while (superClazz != null) {
            String name = superClazz.getName();
            if (DIALOG_FRAGMENT.equals(name) || V4_DIALOG_FRAGMENT.equals(name)) {
                return true;
            }
            superClazz = superClazz.getSuperClass();
        }
        return false;
    }
}