/**
 * Copyright (c) 2010-2020 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.persistence.rrd4j.internal.charts;

import java.awt.Color;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;

import javax.imageio.ImageIO;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.ui.chart.ChartProvider;
import org.openhab.core.ui.items.ItemUIRegistry;
import org.openhab.persistence.rrd4j.internal.RRD4jPersistenceService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.rrd4j.ConsolFun;
import org.rrd4j.core.RrdDb;
import org.rrd4j.graph.RrdGraph;
import org.rrd4j.graph.RrdGraphDef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This servlet generates time-series charts for a given set of items.
 * It accepts the following HTTP parameters:
 * <ul>
 * <li>w: width in pixels of image to generate</li>
 * <li>h: height in pixels of image to generate</li>
 * <li>period: the time span for the x-axis. Value can be h,4h,8h,12h,D,3D,W,2W,M,2M,4M,Y</li>
 * <li>items: A comma separated list of item names to display
 * <li>groups: A comma separated list of group names, whose members should be displayed
 * </ul>
 *
 * @author Kai Kreuzer - Initial contribution
 * @author Chris Jackson - a few improvements
 * @author Jan N. Klug - a few improvements
 *
 */
@Component(service = ChartProvider.class)
public class RRD4jChartServlet implements Servlet, ChartProvider {

    private final Logger logger = LoggerFactory.getLogger(RRD4jChartServlet.class);

    /** the URI of this servlet */
    public static final String SERVLET_NAME = "/rrdchart.png";

    protected static final Color[] LINECOLORS = new Color[] { Color.RED, Color.GREEN, Color.BLUE, Color.MAGENTA,
            Color.ORANGE, Color.CYAN, Color.PINK, Color.DARK_GRAY, Color.YELLOW };
    protected static final Color[] AREACOLORS = new Color[] { new Color(255, 0, 0, 30), new Color(0, 255, 0, 30),
            new Color(0, 0, 255, 30), new Color(255, 0, 255, 30), new Color(255, 128, 0, 30),
            new Color(0, 255, 255, 30), new Color(255, 0, 128, 30), new Color(255, 128, 128, 30),
            new Color(255, 255, 0, 30) };

    protected static final Map<String, Long> PERIODS = new HashMap<>();

    static {
        PERIODS.put("h", -3600000L);
        PERIODS.put("4h", -14400000L);
        PERIODS.put("8h", -28800000L);
        PERIODS.put("12h", -43200000L);
        PERIODS.put("D", -86400000L);
        PERIODS.put("3D", -259200000L);
        PERIODS.put("W", -604800000L);
        PERIODS.put("2W", -1209600000L);
        PERIODS.put("M", -2592000000L);
        PERIODS.put("2M", -5184000000L);
        PERIODS.put("4M", -10368000000L);
        PERIODS.put("Y", -31536000000L);
    }

    @Reference
    protected HttpService httpService;

    @Reference
    protected ItemUIRegistry itemUIRegistry;

    @Activate
    protected void activate() {
        try {
            logger.debug("Starting up rrd chart servlet at {}", SERVLET_NAME);
            httpService.registerServlet(SERVLET_NAME, this, new Hashtable<>(), httpService.createDefaultHttpContext());
        } catch (NamespaceException e) {
            logger.error("Error during servlet startup", e);
        } catch (ServletException e) {
            logger.error("Error during servlet startup", e);
        }
    }

    @Deactivate
    protected void deactivate() {
        httpService.unregister(SERVLET_NAME);
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        logger.debug("RRD4J received incoming chart request: {}", req);

        int width = 480;
        try {
            width = Integer.parseInt(Objects.requireNonNull(req.getParameter("w")));
        } catch (Exception e) {
        }
        int height = 240;
        try {
            height = Integer.parseInt(Objects.requireNonNull(req.getParameter("h")));
        } catch (Exception e) {
        }
        Long period = PERIODS.get(req.getParameter("period"));
        if (period == null) {
            // use a day as the default period
            period = PERIODS.get("D");
        }
        // Create the start and stop time
        Date timeEnd = new Date();
        Date timeBegin = new Date(timeEnd.getTime() + period);

        // Set the content type to that provided by the chart provider
        res.setContentType("image/" + getChartType());
        try {
            BufferedImage chart = createChart(null, null, timeBegin, timeEnd, height, width, req.getParameter("items"),
                    req.getParameter("groups"), null, null);
            ImageIO.write(chart, getChartType().toString(), res.getOutputStream());
        } catch (ItemNotFoundException e) {
            logger.debug("Item not found error while generating chart.");
        } catch (IllegalArgumentException e) {
            logger.debug("Illegal argument in chart", e);
        }
    }

    /**
     * Adds a line for the item to the graph definition.
     * The color of the line is determined by the counter, it simply picks the according index from LINECOLORS (and
     * rolls over if necessary).
     *
     * @param graphDef the graph definition to fill
     * @param item the item to add a line for
     * @param counter defines the number of the datasource and is used to determine the line color
     */
    protected void addLine(RrdGraphDef graphDef, Item item, int counter) {
        Color color = LINECOLORS[counter % LINECOLORS.length];
        String label = itemUIRegistry.getLabel(item.getName());
        String rrdName = RRD4jPersistenceService.DB_FOLDER + File.separator + item.getName() + ".rrd";
        ConsolFun consolFun;
        if (label != null && label.contains("[") && label.contains("]")) {
            label = label.substring(0, label.indexOf('['));
        }
        try {
            RrdDb db = new RrdDb(rrdName);
            consolFun = db.getRrdDef().getArcDefs()[0].getConsolFun();
            db.close();
        } catch (IOException e) {
            consolFun = ConsolFun.MAX;
        }
        if (item instanceof NumberItem) {
            // we only draw a line
            graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
            graphDef.line(Integer.toString(counter), color, label, 2);
        } else {
            // we draw a line and fill the area beneath it with a transparent color
            graphDef.datasource(Integer.toString(counter), rrdName, "state", consolFun); // RRD4jService.getConsolidationFunction(item));
            Color areaColor = AREACOLORS[counter % LINECOLORS.length];

            graphDef.area(Integer.toString(counter), areaColor);
            graphDef.line(Integer.toString(counter), color, label, 2);
        }
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
    }

    // ----------------------------------------------------------
    // The following methods implement the ChartServlet interface

    @Override
    public String getName() {
        return "rrd4j";
    }

    @Override
    public BufferedImage createChart(String service, String theme, Date startTime, Date endTime, int height, int width,
            String items, String groups, Integer dpi, Boolean legend) throws ItemNotFoundException {
        RrdGraphDef graphDef = new RrdGraphDef();

        long period = (startTime.getTime() - endTime.getTime()) / 1000;

        graphDef.setWidth(width);
        graphDef.setHeight(height);
        graphDef.setAntiAliasing(true);
        graphDef.setImageFormat("PNG");
        graphDef.setStartTime(period);
        graphDef.setTextAntiAliasing(true);
        graphDef.setLargeFont(new Font("SansSerif", Font.PLAIN, 15));
        graphDef.setSmallFont(new Font("SansSerif", Font.PLAIN, 11));

        int seriesCounter = 0;

        // Loop through all the items
        if (items != null) {
            String[] itemNames = items.split(",");
            for (String itemName : itemNames) {
                Item item = itemUIRegistry.getItem(itemName);
                addLine(graphDef, item, seriesCounter++);
            }
        }

        // Loop through all the groups and add each item from each group
        if (groups != null) {
            String[] groupNames = groups.split(",");
            for (String groupName : groupNames) {
                Item item = itemUIRegistry.getItem(groupName);
                if (item instanceof GroupItem) {
                    GroupItem groupItem = (GroupItem) item;
                    for (Item member : groupItem.getMembers()) {
                        addLine(graphDef, member, seriesCounter++);
                    }
                } else {
                    throw new ItemNotFoundException("Item '" + item.getName() + "' defined in groups is not a group.");
                }
            }
        }

        // Write the chart as a PNG image
        RrdGraph graph;
        try {
            graph = new RrdGraph(graphDef);
            BufferedImage bi = new BufferedImage(graph.getRrdGraphInfo().getWidth(),
                    graph.getRrdGraphInfo().getHeight(), BufferedImage.TYPE_INT_RGB);
            graph.render(bi.getGraphics());

            return bi;
        } catch (IOException e) {
            logger.error("Error generating graph.", e);
        }

        return null;
    }

    @Override
    public ImageType getChartType() {
        return ImageType.png;
    }
}
