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

import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.google.common.annotations.Beta;

import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Specialized visitor for running detectors on a class object model.
 * <p>
 * It operates in two phases:
 * <ol>
 *   <li> First, it computes a set of maps where it generates a map from each
 *        significant method name to a list of detectors to consult for that method
 *        name. The set of method names that a detector is interested in is provided
 *        by the detectors themselves.
 *   <li> Second, it iterates over the DOM a single time. For each method call it finds,
 *        it dispatches to any check that has registered interest in that method name.
 *   <li> Finally, it runs a full check on those class scanners that do not register
 *        specific method names to be checked. This is intended for those detectors
 *        that do custom work, not related specifically to method calls.
 * </ol>
 * It also notifies all the detectors before and after the document is processed
 * such that they can do pre- and post-processing.
 * <p>
 * <b>NOTE: This is not a public or final API; if you rely on this be prepared
 * to adjust your code for the next tools release.</b>
 */
@Beta
class AsmVisitor {
    /**
     * Number of distinct node types specified in {@link AbstractInsnNode}. Sadly
     * there isn't a max-constant there, so update this along with ASM library
     * updates.
     */
    private static final int TYPE_COUNT = AbstractInsnNode.LINE + 1;
    private final Map<String, List<ClassScanner>> mMethodNameToChecks =
            new HashMap<String, List<ClassScanner>>();
    private final Map<String, List<ClassScanner>> mMethodOwnerToChecks =
            new HashMap<String, List<ClassScanner>>();
    private final List<Detector> mFullClassChecks = new ArrayList<Detector>();

    private final List<? extends Detector> mAllDetectors;
    private List<ClassScanner>[] mNodeTypeDetectors;

    // Really want this:
    //<T extends List<Detector> & Detector.ClassScanner> ClassVisitor(T xmlDetectors) {
    // but it makes client code tricky and ugly.
    @SuppressWarnings("unchecked")
    AsmVisitor(@NonNull LintClient client, @NonNull List<? extends Detector> classDetectors) {
        mAllDetectors = classDetectors;

        // TODO: Check appliesTo() for files, and find a quick way to enable/disable
        // rules when running through a full project!
        for (Detector detector : classDetectors) {
            Detector.ClassScanner scanner = (Detector.ClassScanner) detector;

            boolean checkFullClass = true;

            Collection<String> names = scanner.getApplicableCallNames();
            if (names != null) {
                checkFullClass = false;
                for (String element : names) {
                    List<Detector.ClassScanner> list = mMethodNameToChecks.get(element);
                    if (list == null) {
                        list = new ArrayList<Detector.ClassScanner>();
                        mMethodNameToChecks.put(element, list);
                    }
                    list.add(scanner);
                }
            }

            Collection<String> owners = scanner.getApplicableCallOwners();
            if (owners != null) {
                checkFullClass = false;
                for (String element : owners) {
                    List<Detector.ClassScanner> list = mMethodOwnerToChecks.get(element);
                    if (list == null) {
                        list = new ArrayList<Detector.ClassScanner>();
                        mMethodOwnerToChecks.put(element, list);
                    }
                    list.add(scanner);
                }
            }

            int[] types = scanner.getApplicableAsmNodeTypes();
            if (types != null) {
                checkFullClass = false;
                for (int type : types) {
                    if (type < 0 || type >= TYPE_COUNT) {
                        // Can't support this node type: looks like ASM wasn't updated correctly.
                        client.log(null, "Out of range node type %1$d from detector %2$s",
                                type, scanner);
                        continue;
                    }
                    if (mNodeTypeDetectors == null) {
                        mNodeTypeDetectors = new List[TYPE_COUNT];
                    }
                    List<ClassScanner> checks = mNodeTypeDetectors[type];
                    if (checks == null) {
                        checks = new ArrayList<ClassScanner>();
                        mNodeTypeDetectors[type] = checks;
                    }
                    checks.add(scanner);
                }
            }

            if (checkFullClass) {
                mFullClassChecks.add(detector);
            }
        }
    }

    @SuppressWarnings("rawtypes") // ASM API uses raw types
    void runClassDetectors(ClassContext context) {
        ClassNode classNode = context.getClassNode();

        for (Detector detector : mAllDetectors) {
            detector.beforeCheckFile(context);
        }

        for (Detector detector : mFullClassChecks) {
            Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
            scanner.checkClass(context, classNode);
            detector.afterCheckFile(context);
        }

        if (!mMethodNameToChecks.isEmpty() || !mMethodOwnerToChecks.isEmpty() ||
                mNodeTypeDetectors != null && mNodeTypeDetectors.length > 0) {
            List methodList = classNode.methods;
            for (Object m : methodList) {
                MethodNode method = (MethodNode) m;
                InsnList nodes = method.instructions;
                for (int i = 0, n = nodes.size(); i < n; i++) {
                    AbstractInsnNode instruction = nodes.get(i);
                    int type = instruction.getType();
                    if (type == AbstractInsnNode.METHOD_INSN) {
                        MethodInsnNode call = (MethodInsnNode) instruction;

                        String owner = call.owner;
                        List<ClassScanner> scanners = mMethodOwnerToChecks.get(owner);
                        if (scanners != null) {
                            for (ClassScanner scanner : scanners) {
                                scanner.checkCall(context, classNode, method, call);
                            }
                        }

                        String name = call.name;
                        scanners = mMethodNameToChecks.get(name);
                        if (scanners != null) {
                            for (ClassScanner scanner : scanners) {
                                scanner.checkCall(context, classNode, method, call);
                            }
                        }
                    }

                    if (mNodeTypeDetectors != null && type < mNodeTypeDetectors.length) {
                        List<ClassScanner> scanners = mNodeTypeDetectors[type];
                        if (scanners != null) {
                            for (ClassScanner scanner : scanners) {
                                scanner.checkInstruction(context, classNode, method, instruction);
                            }
                        }
                    }
                }
            }
        }

        for (Detector detector : mAllDetectors) {
            detector.afterCheckFile(context);
        }
    }
}
