/*
 * Copyright (C) 2015 Jian Chen <[email protected]>.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.mobmead.easympermission.javac.handlers;

import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import lombok.core.AnnotationValues;
import lombok.javac.Javac;
import lombok.javac.JavacAnnotationHandler;
import lombok.javac.JavacNode;
import lombok.javac.JavacTreeMaker;
import lombok.javac.handlers.JavacHandlerUtil;
import org.mangosdk.spi.ProviderFor;

import com.sun.tools.javac.tree.JCTree.*;

import com.mobmead.easympermission.Permission;
import com.mobmead.easympermission.RuntimePermission;

import static com.mobmead.easympermission.javac.fluent.Builder.*;

import static lombok.javac.Javac.*;
import static lombok.javac.handlers.JavacHandlerUtil.*;

/**
 * Handles the {@code easympermission.RuntimePermission} annotation for javac.
 */
@ProviderFor(JavacAnnotationHandler.class)
public class HandleRuntimePermission extends JavacAnnotationHandler<RuntimePermission> {

    private final String MESSAGE_PERMISSIONS_WERE_NOT_GRANTED = "Permissions were not granted.";
    private final String MESSAGE_PERMISSIONS_HAVE_BEEN_GRANTED = "Permissions have been granted.";
    private final int FIRST_PERMISSION_REQUEST_CODE = 1000;
    private final String IS_PERMISSION_GRANTED_METHOD = "isPermissionGranted$";

    @Override
    public void handle(AnnotationValues<RuntimePermission> annotation, JCAnnotation ast, JavacNode annotationNode) {

        deleteAnnotationIfNeccessary(annotationNode, RuntimePermission.class);
        JavacNode typeNode = annotationNode.up();
        boolean notAClass = !isClass(typeNode);

        if (notAClass) {
            annotationNode.addError("@RuntimePermission is only supported on a class.");
            return;
        }

        JCMethodDecl method;

        List<PermissionAnnotatedItem> permissionList = findAllAnnotatedMethods(typeNode);
        for (PermissionAnnotatedItem item : permissionList) {
            method = createPermissionCheckedMethod(typeNode, item);
            removeMethod(typeNode, item.getMethod());
            injectMethod(typeNode, recursiveSetGeneratedBy(method, annotationNode.get(), typeNode.getContext()));
        }

        method = recursiveSetGeneratedBy(createIsPermissionGrantedMethod(typeNode), annotationNode.get(), typeNode.getContext());
        injectMethod(typeNode, method);

        method = recursiveSetGeneratedBy(createOnRequestPermissionMethod(typeNode, permissionList), annotationNode.get(), typeNode.getContext());
        injectMethod(typeNode, method);
    }

    private JCMethodDecl createOnRequestPermissionMethod(JavacNode typeNode, List<PermissionAnnotatedItem> permissionList) {
        JavacTreeMaker treeMaker = typeNode.getTreeMaker();

        ListBuffer<JCStatement> statements = new ListBuffer<JCStatement>();

        JCTree.JCExpression returnType = treeMaker.Type(Javac.createVoidType(treeMaker, CTC_VOID));

        List<JCVariableDecl> paramList = List.of(
            Variable(typeNode)
                    .modifiers(Flags.PARAMETER)
                    .name("requestCode")
                    .type(treeMaker.TypeIdent(CTC_INT))
                    .value(null)
                    .build(),
            Variable(typeNode)
                    .modifiers(Flags.PARAMETER)
                    .name("permissions")
                    .type(treeMaker.TypeArray(genJavaLangTypeRef(typeNode, "String")))
                    .value(null)
                    .build(),
            Variable(typeNode)
                    .modifiers(Flags.PARAMETER)
                    .name("grantResults")
                    .type(treeMaker.TypeArray(treeMaker.TypeIdent(CTC_INT)))
                    .value(null)
                    .build()
        );

        for (PermissionAnnotatedItem item : permissionList) {

            JCReturn jcReturn = treeMaker.Return(null);

            JCBinary cmpRequestCode = treeMaker.Binary(CTC_EQUAL,
                    treeMaker.Ident(typeNode.toName("requestCode")),
                    treeMaker.Literal(CTC_INT, item.getRequestCode()));

            JCBinary cmpGrantResult = treeMaker.Binary(CTC_NOT_EQUAL,
                    treeMaker.Indexed(treeMaker.Ident(typeNode.toName("grantResults")), treeMaker.Literal(CTC_INT, 0)),
                    JavacHandlerUtil.chainDots(typeNode, "android", "content", "pm", "PackageManager", "PERMISSION_GRANTED"));

            JCExpression toastMethod =
                    JavacHandlerUtil.chainDots(typeNode, "android", "widget", "Toast", "makeText");
            List<JCExpression> toastArgs = List.<JCTree.JCExpression>of(
                    treeMaker.Ident(typeNode.toName("this")),
                    treeMaker.Literal(MESSAGE_PERMISSIONS_WERE_NOT_GRANTED),
                    treeMaker.Literal(CTC_INT, 1));
            JCMethodInvocation printlnInvocation =
                    treeMaker.Apply(List.<JCTree.JCExpression>nil(), toastMethod, toastArgs);
            JCExpressionStatement thenPart = treeMaker.Exec(
                    treeMaker.Apply(List.<JCTree.JCExpression>nil(), treeMaker.Select(printlnInvocation, typeNode.toName("show")), List.<JCExpression>nil()));

            toastArgs = List.<JCTree.JCExpression>of(
                    treeMaker.Ident(typeNode.toName("this")),
                    treeMaker.Literal(MESSAGE_PERMISSIONS_HAVE_BEEN_GRANTED),
                    treeMaker.Literal(CTC_INT, 1));
            printlnInvocation =
                    treeMaker.Apply(List.<JCTree.JCExpression>nil(), toastMethod, toastArgs);
            JCExpressionStatement elsePart = treeMaker.Exec(
                    treeMaker.Apply(List.<JCTree.JCExpression>nil(), treeMaker.Select(printlnInvocation, typeNode.toName("show")), List.<JCExpression>nil()));

            JCIf jcIfGrantResult = If(typeNode)
                    .condition(cmpGrantResult)
                    .withThen(Block(typeNode).last(thenPart).build())
                    .withElse(Block(typeNode).last(elsePart).build())
                    .build();

            JCIf jcIfRequestCode = If(typeNode)
                    .condition(cmpRequestCode)
                    .onlyThen(Block(typeNode).add(jcIfGrantResult).last(jcReturn).build())
                    .build();

            statements.append(jcIfRequestCode);
        }

        JCBlock body = Block(typeNode)
                .last(statements.toList())
                .build();

        return Method(typeNode)
                .modifiers(Flags.PUBLIC)
                .returnType(returnType)
                .name("onRequestPermissionsResult")
                .paramType()
                .parameters(paramList)
                .thrown()
                .body(body)
                .build();
    }

    private List<PermissionAnnotatedItem> findAllAnnotatedMethods(JavacNode node) {
        int requestCode = FIRST_PERMISSION_REQUEST_CODE;
        ListBuffer<PermissionAnnotatedItem> permissionList = new ListBuffer<PermissionAnnotatedItem>();

        node = upToTypeNode(node);

        if (node != null && node.get() instanceof JCTree.JCClassDecl) {
           for (JCTree def : ((JCTree.JCClassDecl)node.get()).defs) {
                if (def instanceof JCMethodDecl) {
                    JCMethodDecl md = (JCMethodDecl) def;

                    List<JCAnnotation> annotations = md.getModifiers().getAnnotations();
                    if (annotations != null) {
                        for (JCAnnotation anno : annotations) {
                            if (anno.getAnnotationType() != null
                                    && anno.getAnnotationType().type.toString().equals(Permission.class.getName())) {
                                if (anno.getArguments().get(0) instanceof JCTree.JCAssign) {
                                    JCTree.JCAssign assign = (JCTree.JCAssign) anno.getArguments().get(0);
                                    PermissionAnnotatedItem item = new PermissionAnnotatedItem(md, requestCode,
                                            convertStringToList(assign.rhs.toString()));
                                    permissionList.append(item);
                                    requestCode++;
                                }
                            }
                        }
                    }
                }
            }
        }

        return permissionList.toList();
    }

    private List<String> convertStringToList(String input) {
        ListBuffer<String> list = new ListBuffer<String>();
        for (String a : input.replaceAll("^\\{+", "").replaceAll("\\}$", "").split(",")) {
            list.append(a.replaceAll("^\"+", "").replaceAll("\"$", ""));
        }
        return list.toList();
    }

    private JCMethodDecl createPermissionCheckedMethod(JavacNode typeNode, PermissionAnnotatedItem permissionAnnotatedItem) {
        JCBlock block = createPermissionCheckedBlock(typeNode, permissionAnnotatedItem);
        JCMethodDecl origMethod = permissionAnnotatedItem.getMethod();
        return Method(typeNode)
                .modifiers(origMethod.getModifiers())
                .returnType((JCExpression) origMethod.getReturnType())
                .name(origMethod.getName().toString())
                .paramType(origMethod.getTypeParameters())
                .parameters(origMethod.getParameters())
                .thrown(origMethod.getThrows())
                .body(block)
                .build();
    }

    private void removeMethod(JavacNode typeNode, JCMethodDecl method) {
        typeNode = upToTypeNode(typeNode);
        ListBuffer<JCTree> newList = new ListBuffer<JCTree>();
        for (JCTree def : ((JCTree.JCClassDecl)typeNode.get()).defs) {
            if (!(def instanceof JCMethodDecl && def == method)) {
                newList.append(def);
            }
        }
        ((JCTree.JCClassDecl)typeNode.get()).defs = newList.toList();
    }

    private JCMethodDecl createIsPermissionGrantedMethod(JavacNode typeNode) {
        JavacTreeMaker treeMaker = typeNode.getTreeMaker();

        ListBuffer<JCStatement> statements = new ListBuffer<JCStatement>();

        statements.append(Variable(typeNode)
                .modifiers(0)
                .name("ok")
                .type(treeMaker.TypeIdent(CTC_BOOLEAN))
                .value(treeMaker.Literal(CTC_BOOLEAN, 1))
                .build());

        statements.append(Variable(typeNode)
                .modifiers(0)
                .name("failed")
                .type(treeMaker.TypeIdent(CTC_BOOLEAN))
                .value(treeMaker.Literal(CTC_BOOLEAN, 0))
                .build());

        JCTree.JCExpression returnType = treeMaker.TypeIdent(CTC_BOOLEAN);

        List<JCTypeParameter> typleList = List.nil();

        List<JCVariableDecl> paramList = List.of(
                Variable(typeNode)
                        .modifiers(Flags.PARAMETER)
                        .name("permissions")
                        .type(treeMaker.TypeArray(genJavaLangTypeRef(typeNode, "String")))
                        .value(null)
                        .build()
        );

        JCVariableDecl v = Variable(typeNode)
                .modifiers(Flags.PARAMETER)
                .name("permission")
                .type(genJavaLangTypeRef(typeNode, "String"))
                .value(null)
                .build();

        JCExpression checkMethod = JavacHandlerUtil.chainDots(typeNode, "this", "checkSelfPermission");
        List<JCExpression> checkArgs = List.<JCExpression>of(treeMaker.Ident(typeNode.toName("permission")));
        JCMethodInvocation checkInvocation = treeMaker.Apply(List.<JCExpression>nil(), checkMethod, checkArgs);
        JCExpression cmp = treeMaker.Binary(CTC_NOT_EQUAL,
                checkInvocation,
                JavacHandlerUtil.chainDots(typeNode, "android", "content", "pm", "PackageManager", "PERMISSION_GRANTED"));

        JCBlock body = Block(typeNode)
                .last(treeMaker.Return(treeMaker.Ident(typeNode.toName("failed"))))
                .build();

        JCIf jcIf = If(typeNode)
                .condition(cmp)
                .onlyThen(body)
                .build();

        JCEnhancedForLoop block = treeMaker.ForeachLoop(v,
                treeMaker.Ident(typeNode.toName("permissions")),
                jcIf);
        statements.append(block);

        statements.append(treeMaker.Return(treeMaker.Ident(typeNode.toName("ok"))));

        body = Block(typeNode)
                .last(statements.toList())
                .build();

        return Method(typeNode)
                .modifiers(Flags.PRIVATE)
                .returnType(returnType)
                .name(IS_PERMISSION_GRANTED_METHOD)
                .paramType(typleList)
                .parameters(paramList)
                .thrown()
                .body(body)
                .build();
    }

    private JCBlock createPermissionCheckedBlock(JavacNode typeNode, PermissionAnnotatedItem permissionAnnotatedItem) {
        JavacTreeMaker treeMaker = typeNode.getTreeMaker();

        // Create permission list
        ListBuffer<JCExpression> permissionArgs = new ListBuffer<JCExpression>();
        for (String permission : permissionAnnotatedItem.getPermissions()) {
            permissionArgs.append(treeMaker.Literal(permission));
        }

        String permissionVarName = "_permissions_" + permissionAnnotatedItem.getMethod().getName();
        JCExpression v = treeMaker.NewArray(null,
                List.<JCExpression>nil(),
                permissionArgs.toList());
        JCVariableDecl permissions = Variable(typeNode)
                .modifiers(Flags.PRIVATE)
                .name(permissionVarName)
                .type(treeMaker.TypeArray(genJavaLangTypeRef(typeNode, "String")))
                .value(v)
                .build();
        injectField(typeNode, permissions);

        JCExpression checkMethod = JavacHandlerUtil.chainDots(typeNode, "this", IS_PERMISSION_GRANTED_METHOD);
        List<JCExpression> checkArgs = List.<JCExpression>of(treeMaker.Ident(typeNode.toName(permissionVarName)));
        JCMethodInvocation checkInvocation = treeMaker.Apply(List.<JCExpression>nil(), checkMethod, checkArgs);
        JCExpression permissionCmp = treeMaker.Binary(CTC_EQUAL,
                checkInvocation,
                treeMaker.Literal(CTC_BOOLEAN, 1));

        JCExpression requestMethod = JavacHandlerUtil.chainDots(typeNode, "this", "requestPermissions");
        List<JCExpression> requestArgs = List.<JCExpression>of(
                treeMaker.Ident(typeNode.toName(permissionVarName)),
                treeMaker.Literal(CTC_INT, permissionAnnotatedItem.getRequestCode()));
        JCMethodInvocation requestInvocation = treeMaker.Apply(List.<JCExpression>nil(), requestMethod, requestArgs);
        JCBlock body = Block(typeNode)
                .last(treeMaker.Exec(requestInvocation))
                .build();

        JCIf jcCheckPermissionIf = If(typeNode)
                .condition(permissionCmp)
                .withThen(permissionAnnotatedItem.getMethod().getBody())
                .withElse(body)
                .build();
        JCBlock checkPermissionBlock = Block(typeNode)
                .last(jcCheckPermissionIf)
                .build();

        JCExpression versionCmp = treeMaker.Binary(CTC_LESS_THAN,
                JavacHandlerUtil.chainDots(typeNode, "android", "os", "Build", "VERSION", "SDK_INT"),
                treeMaker.Literal(CTC_INT, 23));
        JCIf jcIf = If(typeNode)
                .condition(versionCmp)
                .withThen(permissionAnnotatedItem.getMethod().getBody())
                .withElse(checkPermissionBlock)
                .build();

        return Block(typeNode)
                .last(jcIf)
                .build();
    }

    static boolean isClass(JavacNode typeNode) {
        return isClassAndDoesNotHaveFlags(typeNode, Flags.INTERFACE | Flags.ENUM | Flags.ANNOTATION);
    }

    static boolean isClassOrEnum(JavacNode typeNode) {
        return isClassAndDoesNotHaveFlags(typeNode, Flags.INTERFACE | Flags.ANNOTATION);
    }

    private class PermissionAnnotatedItem {
        private final int requestCode;
        private final List<String> permissions;
        private final JCMethodDecl method;

        public PermissionAnnotatedItem(JCMethodDecl method, int requestCode, List<String> permissions) {
            this.method = method;
            this.requestCode = requestCode;
            this.permissions = permissions;
        }

        public JCMethodDecl getMethod() {
            return method;
        }

        public List<String> getPermissions() {
            return permissions;
        }

        public int getRequestCode() {
            return requestCode;
        }
    }
}