/*
 * Copyright (C) 2013 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.perflib.vmtrace;

import com.android.utils.SparseArray;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * The {@link VmTraceData} class stores all the information from a Dalvik method trace file.
 * Specifically, it provides:
 *  <ul>
 *      <li>A mapping from thread ids to thread names.</li>
 *      <li>A mapping from method ids to {@link MethodInfo}</li>
 *      <li>A mapping from each thread to the top level call on that thread.</li>
 *  </ul>
 */
public class VmTraceData {
    public enum VmClockType { THREAD_CPU, WALL, DUAL }

    private final int mVersion;
    private final boolean mDataFileOverflow;
    private final VmClockType mVmClockType;
    private final String mVm;
    private final Map<String, String> mTraceProperties;

    /** Map from method id to method info. */
    private final Map<Long,MethodInfo> mMethods;

    /** Map from thread name to thread info. */
    private final Map<String, ThreadInfo> mThreadInfo;

    private VmTraceData(Builder b) {
        mVersion = b.mVersion;
        mDataFileOverflow = b.mDataFileOverflow;
        mVmClockType = b.mVmClockType;
        mVm = b.mVm;
        mTraceProperties = b.mProperties;
        mMethods = b.mMethods;

        mThreadInfo = Maps.newHashMapWithExpectedSize(b.mThreads.size());
        for (int i = 0; i < b.mThreads.size(); i++) {
            int id = b.mThreads.keyAt(i);
            String name = b.mThreads.valueAt(i);

            ThreadInfo info = mThreadInfo.get(name);
            if (info != null) {
                // there is alread a thread with the same name
                name = String.format("%1$s-%2$d", name, id);
            }

            info = new ThreadInfo(id, name, b.mTopLevelCalls.get(id));
            mThreadInfo.put(name, info);
        }
    }

    public int getVersion() {
        return mVersion;
    }

    public boolean isDataFileOverflow() {
        return mDataFileOverflow;
    }

    public VmClockType getVmClockType() {
        return mVmClockType;
    }

    public String getVm() {
        return mVm;
    }

    public Map<String, String> getTraceProperties() {
        return mTraceProperties;
    }

    public static TimeUnit getDefaultTimeUnits() {
        // The traces from the VM currently use microseconds.
        // TODO: figure out if this can be obtained/inferred from the trace itself
        return TimeUnit.MICROSECONDS;
    }

    public Collection<ThreadInfo> getThreads() {
        return mThreadInfo.values();
    }

    public List<ThreadInfo> getThreads(boolean excludeThreadsWithNoActivity) {
        Collection<ThreadInfo> allThreads = getThreads();
        if (!excludeThreadsWithNoActivity) {
            return ImmutableList.copyOf(allThreads);
        }

        return Lists.newArrayList(Iterables.filter(allThreads, new Predicate<ThreadInfo>() {
            @Override
            public boolean apply(
                    com.android.tools.perflib.vmtrace.ThreadInfo input) {
                return input.getTopLevelCall() != null;
            }
        }));
    }

    public ThreadInfo getThread(String name) {
        return mThreadInfo.get(name);
    }

    public Map<Long,MethodInfo> getMethods() {
        return mMethods;
    }

    public MethodInfo getMethod(long methodId) {
        return mMethods.get(methodId);
    }

    /** Returns the duration of this call as a percentage of the duration of the top level call. */
    public double getDurationPercentage(Call call, ThreadInfo thread, ClockType clockType,
            boolean inclusiveTime) {
        MethodInfo methodInfo = getMethod(call.getMethodId());
        TimeSelector selector = TimeSelector.create(clockType, inclusiveTime);
        long methodTime = selector.get(methodInfo, thread, TimeUnit.NANOSECONDS);
        return getDurationPercentage(methodTime, thread, clockType);
    }

    /**
     * Returns the given duration as a percentage of the duration of the top level call
     * in given thread.
     */
    public double getDurationPercentage(long methodTime, ThreadInfo thread, ClockType clockType) {
        Call topCall = getThread(thread.getName()).getTopLevelCall();
        if (topCall == null) {
            return 100.;
        }

        MethodInfo topInfo = getMethod(topCall.getMethodId());

        // always use inclusive time to obtain the top level's time when computing percentages
        TimeSelector selector = TimeSelector.create(clockType, true);
        long topLevelTime = selector.get(topInfo, thread, TimeUnit.NANOSECONDS);

        return (double) methodTime/topLevelTime * 100;
    }

    public SearchResult searchFor(String pattern, ThreadInfo thread) {
        pattern = pattern.toLowerCase(Locale.US);

        Set<MethodInfo> methods = new HashSet<MethodInfo>();
        Set<Call> calls = new HashSet<Call>();

        Call topLevelCall = getThread(thread.getName()).getTopLevelCall();
        if (topLevelCall == null) {
            // no matches
            return new SearchResult(methods, calls);
        }

        // Find all methods matching given pattern called on given thread
        for (MethodInfo method: getMethods().values()) {
            String fullName = method.getFullName().toLowerCase(Locale.US);
            if (fullName.contains(pattern)) { // method name matches
                long inclusiveTime = method.getProfileData()
                        .getInclusiveTime(thread, ClockType.GLOBAL, TimeUnit.NANOSECONDS);
                if (inclusiveTime > 0) {
                    // method was called in this thread
                    methods.add(method);
                }
            }
        }

        // Find all invocations of the matched methods
        Iterator<Call> iterator = topLevelCall.getCallHierarchyIterator();
        while (iterator.hasNext()) {
            Call c = iterator.next();
            MethodInfo method = getMethod(c.getMethodId());
            if (methods.contains(method)) {
                calls.add(c);
            }
        }

        return new SearchResult(methods, calls);
    }

    public static class Builder {
        private static final boolean DEBUG = false;

        private int mVersion;
        private boolean mDataFileOverflow;
        private VmClockType mVmClockType = VmClockType.THREAD_CPU;
        private String mVm = "";
        private final Map<String, String> mProperties = new HashMap<String, String>(10);

        /** Map from thread ids to thread names. */
        private final SparseArray<String> mThreads = new SparseArray<String>(10);

        /** Map from method id to method info. */
        private final Map<Long,MethodInfo> mMethods = new HashMap<Long, MethodInfo>(100);

        /** Map from thread id to per thread stack call reconstructor. */
        private final SparseArray<CallStackReconstructor> mStackReconstructors
                = new SparseArray<CallStackReconstructor>(10);

        /** Map from thread id to the top level call for that thread. */
        private final SparseArray<Call> mTopLevelCalls = new SparseArray<Call>(10);

        public void setVersion(int version) {
            mVersion = version;
        }

        public int getVersion() {
            return mVersion;
        }

        public void setDataFileOverflow(boolean dataFileOverflow) {
            mDataFileOverflow = dataFileOverflow;
        }

        public void setVmClockType(VmClockType vmClockType) {
            mVmClockType = vmClockType;
        }

        public VmClockType getVmClockType() {
            return mVmClockType;
        }

        public void setProperty(String key, String value) {
            mProperties.put(key, value);
        }

        public void setVm(String vm) {
            mVm = vm;
        }

        public void addThread(int id, String name) {
            mThreads.put(id, name);
        }

        public void addMethod(long id, MethodInfo info) {
            mMethods.put(id, info);
        }

        public void addMethodAction(int threadId, long methodId, TraceAction methodAction,
                int threadTime, int globalTime) {
            // create thread info if it doesn't exist
            if (mThreads.get(threadId) == null) {
                mThreads.put(threadId, String.format("Thread id: %1$d", threadId));
            }

            // create method info if it doesn't exist
            if (mMethods.get(methodId) == null) {
                MethodInfo info = new MethodInfo(methodId, "unknown", "unknown", "unknown",
                        "unknown", -1);
                mMethods.put(methodId, info);
            }

            if (DEBUG) {
                MethodInfo methodInfo = mMethods.get(methodId);
                System.out.printf("Thread %1$30s: (%2$8x) %3$-40s %4$20s\n",
                        mThreads.get(threadId), methodId, methodInfo.getShortName(), methodAction);
            }

            CallStackReconstructor reconstructor = mStackReconstructors.get(threadId);
            if (reconstructor == null) {
                long topLevelCallId = createUniqueMethodIdForThread(threadId);
                reconstructor = new CallStackReconstructor(topLevelCallId);
                mStackReconstructors.put(threadId, reconstructor);
            }

            reconstructor.addTraceAction(methodId, methodAction, threadTime, globalTime);
        }

        private long createUniqueMethodIdForThread(int threadId) {
            long id = Long.MAX_VALUE - mThreads.indexOfKey(threadId);
            assert mMethods.get(id) == null :
                    "Unexpected error while attempting to create a unique key - key already exists";
            MethodInfo info = new MethodInfo(id, mThreads.get(threadId), "", "", "", 0);
            mMethods.put(id, info);
            return id;
        }

        public VmTraceData build() {
            for (int i = 0; i < mStackReconstructors.size(); i++) {
                int threadId = mStackReconstructors.keyAt(i);
                CallStackReconstructor reconstructor = mStackReconstructors.valueAt(i);
                mTopLevelCalls.put(threadId, reconstructor.getTopLevel());
            }

            return new VmTraceData(this);
        }
    }
}
