/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed 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 com.android.tools.lint.checks;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.checks.ControlFlowGraph.Node;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;

import java.util.Arrays;
import java.util.List;

/**
 * Checks for problems with wakelocks (such as failing to release them)
 * which can lead to unnecessary battery usage.
 */
public class WakelockDetector extends Detector implements ClassScanner {
    public static final String ANDROID_APP_ACTIVITY = "android/app/Activity";        //$NON-NLS-1$

    /** Problems using wakelocks */
    public static final Issue ISSUE = Issue.create(
            "Wakelock", //$NON-NLS-1$
            "Incorrect `WakeLock` usage",

            "Failing to release a wakelock properly can keep the Android device in " +
            "a high power mode, which reduces battery life. There are several causes " +
            "of this, such as releasing the wake lock in `onDestroy()` instead of in " +
            "`onPause()`, failing to call `release()` in all possible code paths after " +
            "an `acquire()`, and so on.\n" +
            "\n" +
            "NOTE: If you are using the lock just to keep the screen on, you should " +
            "strongly consider using `FLAG_KEEP_SCREEN_ON` instead. This window flag " +
            "will be correctly managed by the platform as the user moves between " +
            "applications and doesn't require a special permission. See " +
            "http://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_KEEP_SCREEN_ON.",

            Category.PERFORMANCE,
            9,
            Severity.WARNING,
            new Implementation(
                    WakelockDetector.class,
                    Scope.CLASS_FILE_SCOPE));

    private static final String WAKELOCK_OWNER = "android/os/PowerManager$WakeLock"; //$NON-NLS-1$
    private static final String RELEASE_METHOD = "release"; //$NON-NLS-1$
    private static final String ACQUIRE_METHOD = "acquire"; //$NON-NLS-1$
    private static final String IS_HELD_METHOD = "isHeld"; //$NON-NLS-1$
    private static final String POWER_MANAGER = "android/os/PowerManager"; //$NON-NLS-1$
    private static final String NEW_WAKE_LOCK_METHOD = "newWakeLock"; //$NON-NLS-1$

    /** Print diagnostics during analysis (display flow control graph etc).
     * Make sure you add the asm-debug or asm-util jars to the runtime classpath
     * as well since the opcode integer to string mapping display routine looks for
     * it via reflection. */
    private static final boolean DEBUG = false;

    /** Constructs a new {@link WakelockDetector} */
    public WakelockDetector() {
    }

    @Override
    public void afterCheckProject(@NonNull Context context) {
        if (mHasAcquire && !mHasRelease && context.getDriver().getPhase() == 1) {
            // Gather positions of the acquire calls
            context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE);
        }
    }

    // ---- Implements ClassScanner ----

    /** Whether any {@code acquire()} calls have been encountered */
    private boolean mHasAcquire;

    /** Whether any {@code release()} calls have been encountered */
    private boolean mHasRelease;

    @Override
    @Nullable
    public List<String> getApplicableCallNames() {
        return Arrays.asList(ACQUIRE_METHOD, RELEASE_METHOD, NEW_WAKE_LOCK_METHOD);
    }

    @Override
    public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
            @NonNull MethodNode method, @NonNull MethodInsnNode call) {
        if (!context.getProject().getReportIssues()) {
            // If this is a library project not being analyzed, ignore it
            return;
        }

        if (call.owner.equals(WAKELOCK_OWNER)) {
            String name = call.name;
            if (name.equals(ACQUIRE_METHOD)) {
                if (call.desc.equals("(J)V")) { // acquire(long timeout) does not require a corresponding release
                    return;
                }
                mHasAcquire = true;

                if (context.getDriver().getPhase() == 2) {
                    assert !mHasRelease;
                    context.report(ISSUE, method, call, context.getLocation(call),
                        "Found a wakelock `acquire()` but no `release()` calls anywhere");
                } else {
                    assert context.getDriver().getPhase() == 1;
                    // Perform flow analysis in this method to see if we're
                    // performing an acquire/release block, where there are code paths
                    // between the acquire and release which can result in the
                    // release call not getting reached.
                    checkFlow(context, classNode, method, call);
                }
            } else if (name.equals(RELEASE_METHOD)) {
                mHasRelease = true;

                // See if the release is happening in an onDestroy method, in an
                // activity.
                if ("onDestroy".equals(method.name) //$NON-NLS-1$
                        && context.getDriver().isSubclassOf(
                                classNode, ANDROID_APP_ACTIVITY)) {
                    context.report(ISSUE, method, call, context.getLocation(call),
                        "Wakelocks should be released in `onPause`, not `onDestroy`");
                }
            }
        } else if (call.owner.equals(POWER_MANAGER)) {
            if (call.name.equals(NEW_WAKE_LOCK_METHOD)) {
                AbstractInsnNode prev = LintUtils.getPrevInstruction(call);
                if (prev == null) {
                    return;
                }
                prev = LintUtils.getPrevInstruction(prev);
                if (prev == null || prev.getOpcode() != Opcodes.LDC) {
                    return;
                }
                LdcInsnNode ldc = (LdcInsnNode) prev;
                Object constant = ldc.cst;
                if (constant instanceof Integer) {
                    int flag = ((Integer) constant).intValue();
                    // Constant values are copied into the bytecode so we have to compare
                    // values; however, that means the values are part of the API
                    final int PARTIAL_WAKE_LOCK = 0x00000001;
                    final int ACQUIRE_CAUSES_WAKEUP = 0x10000000;
                    final int both = PARTIAL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP;
                    if ((flag & both) == both) {
                        context.report(ISSUE, method, call, context.getLocation(call),
                                "Should not set both `PARTIAL_WAKE_LOCK` and `ACQUIRE_CAUSES_WAKEUP`. "
                                        + "If you do not want the screen to turn on, get rid of "
                                        + "`ACQUIRE_CAUSES_WAKEUP`");
                    }
                }

            }
        }
    }

    private static void checkFlow(@NonNull ClassContext context, @NonNull ClassNode classNode,
            @NonNull MethodNode method, @NonNull MethodInsnNode acquire) {
        final InsnList instructions = method.instructions;
        MethodInsnNode release = null;

        // Find release call
        for (int i = 0, n = instructions.size(); i < n; i++) {
            AbstractInsnNode instruction = instructions.get(i);
            int type = instruction.getType();
            if (type == AbstractInsnNode.METHOD_INSN) {
                MethodInsnNode call = (MethodInsnNode) instruction;
                if (call.name.equals(RELEASE_METHOD) &&
                        call.owner.equals(WAKELOCK_OWNER)) {
                    release = call;
                    break;
                }
            }
        }

        if (release == null) {
            // Didn't find both acquire and release in this method; no point in doing
            // local flow analysis
            return;
        }

        try {
            MyGraph graph = new MyGraph();
            ControlFlowGraph.create(graph, classNode, method);

            if (DEBUG) {
                // Requires util package
                //ClassNode clazz = classNode;
                //clazz.accept(new TraceClassVisitor(new PrintWriter(System.out)));
                System.out.println(graph.toString(graph.getNode(acquire)));
            }

            int status = dfs(graph.getNode(acquire));
            if ((status & SEEN_RETURN) != 0) {
                String message;
                if ((status & SEEN_EXCEPTION) != 0) {
                    message = "The `release()` call is not always reached (via exceptional flow)";
                } else {
                    message = "The `release()` call is not always reached";
                }

                context.report(ISSUE, method, acquire,
                        context.getLocation(release), message);
            }
        } catch (AnalyzerException e) {
            context.log(e, null);
        }
    }

    private static final int SEEN_TARGET = 1;
    private static final int SEEN_BRANCH = 2;
    private static final int SEEN_EXCEPTION = 4;
    private static final int SEEN_RETURN = 8;

    /** TODO RENAME */
    private static class MyGraph extends ControlFlowGraph {
        @Override
        protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
            if (from.getOpcode() == Opcodes.IFNULL) {
                JumpInsnNode jump = (JumpInsnNode) from;
                if (jump.label == to) {
                    // Skip jump targets on null if it's surrounding the release call
                    //
                    //  if (lock != null) {
                    //      lock.release();
                    //  }
                    //
                    // The above shouldn't be considered a scenario where release() may not
                    // be called.
                    AbstractInsnNode next = LintUtils.getNextInstruction(from);
                    if (next != null && next.getType() == AbstractInsnNode.VAR_INSN) {
                        next = LintUtils.getNextInstruction(next);
                        if (next != null && next.getType() == AbstractInsnNode.METHOD_INSN) {
                            MethodInsnNode method = (MethodInsnNode) next;
                            if (method.name.equals(RELEASE_METHOD) &&
                                    method.owner.equals(WAKELOCK_OWNER)) {
                                // This isn't entirely correct; this will also trigger
                                // for "if (lock == null) { lock.release(); }" but that's
                                // not likely (and caught by other null checking in tools)
                                return;
                            }
                        }
                    }
                }
            } else if (from.getOpcode() == Opcodes.IFEQ) {
                JumpInsnNode jump = (JumpInsnNode) from;
                if (jump.label == to) {
                    AbstractInsnNode prev = LintUtils.getPrevInstruction(from);
                    if (prev != null && prev.getType() == AbstractInsnNode.METHOD_INSN) {
                        MethodInsnNode method = (MethodInsnNode) prev;
                        if (method.name.equals(IS_HELD_METHOD) &&
                                method.owner.equals(WAKELOCK_OWNER)) {
                            AbstractInsnNode next = LintUtils.getNextInstruction(from);
                            if (next != null) {
                                super.add(from, next);
                                return;
                            }
                        }
                    }
                }
            }

            super.add(from, to);
        }
    }

    /** Search from the given node towards the target; return false if we reach
     * an exit point such as a return or a call on the way there that is not within
     * a try/catch clause.
     *
     * @param node the current node
     * @return true if the target was reached
     *    XXX RETURN VALUES ARE WRONG AS OF RIGHT NOW
     */
    protected static int dfs(ControlFlowGraph.Node node) {
        AbstractInsnNode instruction = node.instruction;
        if (instruction.getType() == AbstractInsnNode.JUMP_INSN) {
            int opcode = instruction.getOpcode();
            if (opcode == Opcodes.RETURN || opcode == Opcodes.ARETURN
                    || opcode == Opcodes.LRETURN || opcode == Opcodes.IRETURN
                    || opcode == Opcodes.DRETURN || opcode == Opcodes.FRETURN
                    || opcode == Opcodes.ATHROW) {
                if (DEBUG) {
                    System.out.println("Found exit via explicit return: " //$NON-NLS-1$
                            + node.toString(false));
                }
                return SEEN_RETURN;
            }
        }

        if (!DEBUG) {
            // There are no cycles, so no *NEED* for this, though it does avoid
            // researching shared labels. However, it makes debugging harder (no re-entry)
            // so this is only done when debugging is off
            if (node.visit != 0) {
                return 0;
            }
            node.visit = 1;
        }

        // Look for the target. This is any method call node which is a release on the
        // lock (later also check it's the same instance, though that's harder).
        // This is because finally blocks tend to be inlined so from a single try/catch/finally
        // with a release() in the finally, the bytecode can contain multiple repeated
        // (inlined) release() calls.
        if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
            MethodInsnNode method = (MethodInsnNode) instruction;
            if (method.name.equals(RELEASE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
                return SEEN_TARGET;
            } else if (method.name.equals(ACQUIRE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
                // OK
            } else if (method.name.equals(IS_HELD_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
                // OK
            } else {
                // Some non acquire/release method call: if this is not associated with a
                // try-catch block, it would mean the exception would exit the method,
                // which would be an error
                if (node.exceptions == null || node.exceptions.isEmpty()) {
                    // Look up the corresponding frame, if any
                    AbstractInsnNode curr = method.getPrevious();
                    boolean foundFrame = false;
                    while (curr != null) {
                        if (curr.getType() == AbstractInsnNode.FRAME) {
                            foundFrame = true;
                            break;
                        }
                        curr = curr.getPrevious();
                    }

                    if (!foundFrame) {
                        if (DEBUG) {
                            System.out.println("Found exit via unguarded method call: " //$NON-NLS-1$
                                    + node.toString(false));
                        }
                        return SEEN_RETURN;
                    }
                }
            }
        }

        // if (node.instruction is a call, and the call is not caught by
        // a try/catch block (provided the release is not inside the try/catch block)
        // then return false
        int status = 0;

        boolean implicitReturn = true;
        List<Node> successors = node.successors;
        List<Node> exceptions = node.exceptions;
        if (exceptions != null) {
            if (!exceptions.isEmpty()) {
                implicitReturn = false;
            }
            for (Node successor : exceptions) {
                status = dfs(successor) | status;
                if ((status & SEEN_RETURN) != 0) {
                    if (DEBUG) {
                        System.out.println("Found exit via exception: " //$NON-NLS-1$
                                + node.toString(false));
                    }
                    return status;
                }
            }

            if (status != 0) {
                status |= SEEN_EXCEPTION;
            }
        }

        if (successors != null) {
            if (!successors.isEmpty()) {
                implicitReturn = false;
                if (successors.size() > 1) {
                    status |= SEEN_BRANCH;
                }
            }
            for (Node successor : successors) {
                status = dfs(successor) | status;
                if ((status & SEEN_RETURN) != 0) {
                    if (DEBUG) {
                        System.out.println("Found exit via branches: " //$NON-NLS-1$
                                + node.toString(false));
                    }
                    return status;
                }
            }
        }

        if (implicitReturn) {
            status |= SEEN_RETURN;
            if (DEBUG) {
                System.out.println("Found exit: via implicit return: " //$NON-NLS-1$
                        + node.toString(false));
            }
        }

        return status;
    }
}
