/*
 * Copyright (c) 2016 Google Inc. All Rights Reserved.
 *
 * 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.vts.servlet;

import com.android.vts.entity.DeviceInfoEntity;
import com.android.vts.entity.ProfilingPointRunEntity;
import com.android.vts.entity.TestEntity;
import com.android.vts.entity.TestRunEntity;
import com.android.vts.util.DatastoreHelper;
import com.android.vts.util.FilterUtil;
import com.android.vts.util.Graph;
import com.android.vts.util.GraphSerializer;
import com.android.vts.util.Histogram;
import com.android.vts.util.LineGraph;
import com.android.vts.util.PerformanceUtil;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.Filter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;

/** Servlet for handling requests to load graphs. */
public class ShowGraphServlet extends BaseServlet {
    private static final String GRAPH_JSP = "WEB-INF/jsp/show_graph.jsp";
    private static final long DEFAULT_FILTER_OPTION = -1;

    private static final String HIDL_HAL_OPTION = "hidl_hal_mode";
    private static final String[] splitKeysArray = new String[] {HIDL_HAL_OPTION};
    private static final Set<String> splitKeySet = new HashSet<>(Arrays.asList(splitKeysArray));
    private static final String PROFILING_DATA_ALERT = "No profiling data was found.";

    @Override
    public PageType getNavParentType() {
        return PageType.TOT;
    }

    @Override
    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
        List<Page> links = new ArrayList<>();
        String testName = request.getParameter("testName");
        links.add(new Page(PageType.TABLE, testName, "?testName=" + testName));

        String profilingPointName = request.getParameter("profilingPoint");
        links.add(
                new Page(
                        PageType.GRAPH,
                        "?testName=" + testName + "&profilingPoint=" + profilingPointName));
        return links;
    }

    /**
     * Process a profiling report message and add it to the map of graphs.
     *
     * @param profilingRun The Entity of a profiling point run to process.
     * @param idString The ID derived from the test run to identify the profiling report.
     * @param graphMap A map from graph name to Graph object.
     */
    private static void processProfilingRun(
            Entity profilingRun, String idString, Map<String, Graph> graphMap) {
        ProfilingPointRunEntity pt = ProfilingPointRunEntity.fromEntity(profilingRun);
        if (pt == null) return;
        String name = PerformanceUtil.getOptionAlias(pt, splitKeySet);
        Graph g = null;
        if (pt.getLabels() != null && pt.getLabels().size() == pt.getValues().size()) {
            g = new LineGraph(name);
        } else if (pt.getLabels() == null && pt.getValues().size() > 0) {
            g = new Histogram(name);
        } else {
            return;
        }
        if (!graphMap.containsKey(name)) {
            graphMap.put(name, g);
        }
        graphMap.get(name).addData(idString, pt);
    }

    /**
     * Get a summary string describing the devices in the test run.
     *
     * @param devices The list of device descriptors for a particular test run.
     * @return A string describing the devices in the test run.
     */
    private static String getDeviceSummary(List<DeviceInfoEntity> devices) {
        if (devices == null) return null;
        List<String> buildInfos = new ArrayList<>();
        for (DeviceInfoEntity device : devices) {
            buildInfos.add(device.getProduct() + " (" + device.getBuildId() + ")");
        }
        return StringUtils.join(buildInfos, ", ");
    }

    @Override
    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        RequestDispatcher dispatcher = null;
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        String testName = request.getParameter("testName");
        String profilingPointName = request.getParameter("profilingPoint");
        String selectedDevice = request.getParameter("device");
        Long endTime = null;
        if (request.getParameter("endTime") != null) {
            String time = request.getParameter("endTime");
            try {
                endTime = Long.parseLong(time);
            } catch (NumberFormatException e) {
            }
        }
        if (endTime == null) {
            endTime = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
        }
        Long startTime = endTime - TimeUnit.DAYS.toMicros(1);

        // Set of device names
        List<String> devices = DatastoreHelper.getAllBuildFlavors();
        if (!devices.contains(selectedDevice)) selectedDevice = null;

        Map<String, Graph> graphMap = new HashMap<>();

        // Create a query for test runs matching the time window filter
        Key parentKey = KeyFactory.createKey(TestEntity.KIND, testName);
        Filter timeFilter =
                FilterUtil.getTimeFilter(parentKey, TestRunEntity.KIND, startTime, endTime);
        Query testRunQuery =
                new Query(TestRunEntity.KIND)
                        .setAncestor(parentKey)
                        .setFilter(timeFilter)
                        .setKeysOnly();

        // Process the test runs in the query
        List<Key> gets = new ArrayList<>();
        for (Entity testRun :
                datastore
                        .prepare(testRunQuery)
                        .asIterable(DatastoreHelper.getLargeBatchOptions())) {
            gets.add(
                    KeyFactory.createKey(
                            testRun.getKey(), ProfilingPointRunEntity.KIND, profilingPointName));
        }
        Map<Key, Entity> profilingPoints = datastore.get(gets);
        Map<Key, Entity> testRunProfiling = new HashMap<>();
        for (Key key : profilingPoints.keySet()) {
            testRunProfiling.put(key.getParent(), profilingPoints.get(key));
        }

        Filter deviceFilter =
                FilterUtil.getDeviceTimeFilter(parentKey, TestRunEntity.KIND, startTime, endTime);
        if (selectedDevice != null) {
            deviceFilter =
                    Query.CompositeFilterOperator.and(
                            deviceFilter,
                            new Query.FilterPredicate(
                                    DeviceInfoEntity.BUILD_FLAVOR,
                                    Query.FilterOperator.EQUAL,
                                    selectedDevice));
        }
        Query deviceQuery =
                new Query(DeviceInfoEntity.KIND)
                        .setAncestor(parentKey)
                        .setFilter(deviceFilter)
                        .setKeysOnly();
        gets = new ArrayList<>();
        for (Entity device :
                datastore.prepare(deviceQuery).asIterable(DatastoreHelper.getLargeBatchOptions())) {
            if (testRunProfiling.containsKey(device.getParent())) {
                gets.add(device.getKey());
            }
        }

        Map<Key, Entity> deviceInfos = datastore.get(gets);
        Map<Key, List<DeviceInfoEntity>> testRunDevices = new HashMap<>();
        for (Key deviceKey : deviceInfos.keySet()) {
            if (!testRunDevices.containsKey(deviceKey.getParent())) {
                testRunDevices.put(deviceKey.getParent(), new ArrayList<DeviceInfoEntity>());
            }
            DeviceInfoEntity device = DeviceInfoEntity.fromEntity(deviceInfos.get(deviceKey));
            if (device == null) continue;
            testRunDevices.get(deviceKey.getParent()).add(device);
        }

        for (Key runKey : testRunProfiling.keySet()) {
            String idString = getDeviceSummary(testRunDevices.get(runKey));
            if (idString != null) {
                processProfilingRun(testRunProfiling.get(runKey), idString, graphMap);
            }
        }

        // Get the names of the graphs to render
        String[] names = graphMap.keySet().toArray(new String[graphMap.size()]);
        Arrays.sort(names);

        List<Graph> graphList = new ArrayList<>();
        boolean hasHistogram = false;
        for (String name : names) {
            Graph g = graphMap.get(name);
            if (g.size() > 0) {
                graphList.add(g);
                if (g instanceof Histogram) hasHistogram = true;
            }
        }

        String filterVal = request.getParameter("filterVal");
        try {
            Long.parseLong(filterVal);
        } catch (NumberFormatException e) {
            filterVal = Long.toString(DEFAULT_FILTER_OPTION);
        }
        request.setAttribute("testName", request.getParameter("testName"));
        request.setAttribute("filterVal", filterVal);
        request.setAttribute("endTime", new Gson().toJson(endTime));
        request.setAttribute("devices", devices);
        request.setAttribute("selectedDevice", selectedDevice);
        request.setAttribute("showFilterDropdown", hasHistogram);
        if (graphList.size() == 0) request.setAttribute("error", PROFILING_DATA_ALERT);

        Gson gson =
                new GsonBuilder()
                        .registerTypeHierarchyAdapter(Graph.class, new GraphSerializer())
                        .create();
        request.setAttribute("graphs", gson.toJson(graphList));

        request.setAttribute("profilingPointName", profilingPointName);
        dispatcher = request.getRequestDispatcher(GRAPH_JSP);
        try {
            dispatcher.forward(request, response);
        } catch (ServletException e) {
            logger.log(Level.SEVERE, "Servlet Excpetion caught : ", e);
        }
    }
}
