package qunar.tc.bistoury.instrument.client.profiler.sync.runtime;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.AtomicLongMap;
import com.taobao.middleware.logger.Logger;
import qunar.tc.bistoury.attach.common.BistouryLoggerHelper;
import qunar.tc.bistoury.attach.common.BistouryLoggger;
import qunar.tc.bistoury.common.profiler.compact.CompactClassHelper;
import qunar.tc.bistoury.common.profiler.method.MethodCache;
import qunar.tc.bistoury.common.profiler.method.MethodInfo;
import qunar.tc.bistoury.instrument.client.profiler.sync.Manager;
import qunar.tc.bistoury.instrument.client.profiler.sync.runtime.cpu.DumpData;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * @author cai.wen created on 2019/10/17 11:15
 */
public class ProfilerDataDumper {

    private static final Logger logger = BistouryLoggger.getLogger();

    private static final Splitter METHOD_ID_SPLITTER = Splitter.on("-").omitEmptyStrings();

    private final ProfilerData profilerData;

    public ProfilerDataDumper() {
        profilerData = ProfilerData.getInstance();
    }

    public void dump() {
        logger.info("start dump");
        DumpData dumpData = profilerData.getDumpData();
        long dumpTime = System.currentTimeMillis();
        if (Manager.isDebugMode()) {
            logger.info("dumpData: {}", dumpData);
        }

        doDump(dumpData.getBlockedTimes(), Manager.getBlockedDataPath(), false);
        doDump(dumpData.getBlockedTimes(), Manager.getFilterBlockedDataPath(), true);
        doDump(dumpData.getRunnableCpuTimes(), Manager.getRunnableDataPath(), false);
        doDump(dumpData.getRunnableCpuTimes(), Manager.getFilterRunnableDataPath(), true);
        doDump(dumpData.getTimedWaitingTimes(), Manager.getTimedWaitingDataPath(), false);
        doDump(dumpData.getTimedWaitingTimes(), Manager.getFilterTimedWaitingDataPath(), true);
        doDump(dumpData.getWaitingTimes(), Manager.getWaitingDataPath(), false);
        doDump(dumpData.getWaitingTimes(), Manager.getFilterWaitingDataPath(), true);

        dumpAllState(dumpData);
        Manager.renameResult(dumpTime);
    }

    private void dumpAllState(DumpData dumpData) {
        AtomicLongMap<Integer> allStateMap = AtomicLongMap.create();
        List<Map<Integer, Long>> allRecord = ImmutableList.of(dumpData.getBlockedTimes(), dumpData.getRunnableCpuTimes(),
                dumpData.getTimedWaitingTimes(), dumpData.getWaitingTimes());
        for (Map<Integer, Long> cpuTime : allRecord) {
            for (Map.Entry<Integer, Long> entry : cpuTime.entrySet()) {
                allStateMap.addAndGet(entry.getKey(), entry.getValue());
            }
        }
        doDump(allStateMap.asMap(), Manager.getAllStatePath(), false);
        doDump(allStateMap.asMap(), Manager.getFilterAllStatePath(), true);
    }

    private void doDump(Map<Integer, Long> cpuTimes, String dumpFile, boolean isFilter) {
        File realPath = new File(dumpFile);

        try {
            realPath.createNewFile();
        } catch (IOException e) {
            logger.error("", BistouryLoggerHelper.formatMessage("create dump file error. path: {}", realPath), e);
        }

        List<Map.Entry<Integer, Long>> timeList;
        if (Manager.isDebugMode()) {
            timeList = sortByValue(cpuTimes);
        } else {
            timeList = Lists.newArrayList(cpuTimes.entrySet());
        }

        try (BufferedWriter dumpStream = new BufferedWriter(new FileWriter(realPath))) {
            Map<Integer, String> callStackIds = profilerData.getCallStackMapping();
            for (Map.Entry<Integer, Long> entry : timeList) {
                String callStackTags = callStackIds.get(entry.getKey());
                String callStack = getCallStack(callStackTags, isFilter);
                Long cpuTime = entry.getValue();
                if (callStack.isEmpty()) {
                    continue;
                }
                if (cpuTime == 0) {
                    continue;
                }
                dumpStream.write(callStack);
                dumpStream.write(" ");
                dumpStream.write(cpuTime.toString());
                dumpStream.write("\n");
            }
            dumpStream.flush();
        } catch (Exception e) {
            logger.error("", BistouryLoggerHelper.formatMessage("dump cputime map error. dump file: {}. path: {}", dumpFile), e);
        }
    }

    private String getCallStack(String methodIdTag, boolean isFilter) {
        List<String> methodIdStr = METHOD_ID_SPLITTER.splitToList(methodIdTag);
        List<Integer> methodIds = Lists.newArrayListWithExpectedSize(methodIdStr.size());
        for (String idStr : methodIdStr) {
            methodIds.add(Integer.valueOf(idStr));
        }

        StringBuilder builder = new StringBuilder();
        List<MethodInfo> methodInfos = isFilter ? getFilterMethodInfos(methodIds) : getMethodInfos(methodIds);
        if (methodInfos.size() <= 1) {
            return "";
        }
        for (MethodInfo methodInfo : methodInfos) {
            builder.append(methodInfo).append(";");
        }
        if (builder.length() != 0) {
            builder.delete(builder.length() - 1, builder.length());
        }

        return builder.toString();
    }

    private List<MethodInfo> getMethodInfos(List<Integer> methodIds) {
        Map<Integer, MethodInfo> idMap = MethodCache.getIdToInfoMapping();
        List<MethodInfo> result = Lists.newArrayListWithExpectedSize(methodIds.size());
        MethodInfo preInfo = null;
        for (int methodId : Lists.reverse(methodIds)) {
            MethodInfo methodInfo = idMap.get(methodId);

            //如果当前方法和上一层是重载,抛弃
            if (methodInfo.equals(preInfo)) {
                continue;
            }
            result.add(methodInfo);
            preInfo = methodInfo;
        }
        return result;
    }

    private List<MethodInfo> getFilterMethodInfos(List<Integer> methodIds) {
        Map<Integer, MethodInfo> idMap = MethodCache.getIdToInfoMapping();
        List<MethodInfo> result = Lists.newArrayListWithExpectedSize(methodIds.size() / 2);
        List<Integer> reverseIds = Lists.reverse(methodIds);

        MethodInfo firstMethodInfo = idMap.get(reverseIds.remove(0));
        //最底层的一般是Thread的子类,直接打印
        result.add(firstMethodInfo);

        boolean isPreCompact = CompactClassHelper.isCompactClass(firstMethodInfo.getClassName());
        MethodInfo preInfo = null;
        for (int methodId : reverseIds) {
            MethodInfo methodInfo = idMap.get(methodId);

            //如果当前方法和上一层是重载,抛弃
            if (methodInfo.equals(preInfo)) {
                continue;
            }
            preInfo = methodInfo;

            boolean isCompact = CompactClassHelper.isCompactClass(methodInfo.getClassName());
            if (isPreCompact && isCompact) {
                continue;
            }
            isPreCompact = isCompact;

            result.add(methodInfo);
        }

        if (isPreCompact && result.size() == 2) {
            return ImmutableList.of();
        }

        return result;
    }

    private List<Map.Entry<Integer, Long>> sortByValue(Map<Integer, Long> countMap) {
        List<Map.Entry<Integer, Long>> countEntryList = new ArrayList<>(countMap.entrySet());
        Collections.sort(countEntryList, new Comparator<Map.Entry<Integer, Long>>() {
            @Override
            public int compare(Map.Entry<Integer, Long> l, Map.Entry<Integer, Long> r) {
                long left = l.getValue();
                long right = r.getValue();
                if (left > right) {
                    return -1;
                } else if (left == right) {
                    return 0;
                }
                return 1;
            }
        });
        return countEntryList;
    }
}