/*
 * Copyright (c) 2005-2010 KOM – Multimedia Communications Lab
 *
 * This file is part of PeerfactSim.KOM.
 *
 * PeerfactSim.KOM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * PeerfactSim.KOM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PeerfactSim.KOM.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package de.tud.kom.p2psim.impl.analyzer.metric.output;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;

import de.tud.kom.p2psim.api.common.SimHost;
import de.tud.kom.p2psim.impl.util.db.dao.DAO;
import de.tud.kom.p2psim.impl.util.db.dao.metric.MeasurementDAO;
import de.tud.kom.p2psim.impl.util.db.metric.MetricDescription;
import de.tud.kom.p2psim.impl.util.oracle.GlobalOracle;
import de.tudarmstadt.maki.simonstrator.api.Time;
import de.tudarmstadt.maki.simonstrator.api.common.metric.ActiveMetric;
import de.tudarmstadt.maki.simonstrator.api.common.metric.ActiveSpatialMetric;
import de.tudarmstadt.maki.simonstrator.api.common.metric.ActiveSpatialMetric.ActiveSpatialMetricListener;
import de.tudarmstadt.maki.simonstrator.api.common.metric.ActiveSpatialMetric.SpatialMetricValue;
import de.tudarmstadt.maki.simonstrator.api.common.metric.ActiveMetric.ActiveMetricListener;
import de.tudarmstadt.maki.simonstrator.api.common.metric.Metric;
import de.tudarmstadt.maki.simonstrator.api.common.metric.Metric.MetricValue;
import de.tudarmstadt.maki.simonstrator.api.util.XMLConfigurableConstructor;

/**
 * This class maps {@link Metric}s to calls to the DAO on regular intervals or
 * special actions.
 *
 * @author Bjoern Richerzhagen
 * @version 1.0, 13.08.2012
 */
public class MetricOutputDAO extends AbstractOutput {


    protected long timeEnableDao = 0;

    protected long timeStopDao = Long.MAX_VALUE;

    protected Set<String> metricsToAggregate = new LinkedHashSet<>();

    protected List<DaoAdapter> daoAdapters = new LinkedList<>();

    /**
     *
     * @param table
     */
    @XMLConfigurableConstructor({ "table" })
    public MetricOutputDAO(String table) {
        DAO.setDatabase(table);
    }

    public void setAddress(String address) {
        DAO.address = address;
    }

    public void setPort(String port) {
        DAO.port = port;
    }

    public void setUser(String user) {
        DAO.setUsername(user);
    }

    public void setPassword(String password) {
        DAO.setPassword(password);
    }

    public void setTimeEnableDao(long timeEnableDao) {
        this.timeEnableDao = timeEnableDao;
    }

    public void setTimeStopDao(long timeStopDao) {
        this.timeStopDao = timeStopDao;
    }

    /**
     * A list of PER-HOST metrics that are NOT written individually but instead
     * statistical figures for a group of hosts is written. The group of hosts
     * is defined by the hostGroup from the XML-config.
     *
     * @param metricsToAggregate
     */
    public void setToAggregate(String[] metricsToAggregate) {
        for (String metric : metricsToAggregate) {
            this.metricsToAggregate.add(metric);
        }
    }

    public boolean isToAggregate(Metric metric) {
        for (String string : metricsToAggregate) {
            if (metric.getName().equals(string)) {
                return true;
            }
            if (string.endsWith("*")) {
                // prefix matching
                String mName = metric.getName();
                if (mName.startsWith(string.substring(0, string.length()-1))) {
                    return true;
                }
            }
            if (string.startsWith("*")) {
                // postfix matching
                String mName = metric.getName();
                if (mName.endsWith(string.substring(1, string.length()))) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void onInitialize(List<Metric> metrics) {
        for (Metric metric : metrics) {
            /*
             * Only active metrics are allowed. We register as a listener and
             * wait for our call.
             */
            if (metric instanceof ActiveMetric) {
                ActiveMetric am = (ActiveMetric) metric;
                MetricDaoAdapter adapter = new MetricDaoAdapter(am);
                am.addActiveMetricListener(adapter);
                daoAdapters.add(adapter);
            }
            if (metric instanceof ActiveSpatialMetric) {
                ActiveSpatialMetric asm = (ActiveSpatialMetric) metric;
                SpatialMetricDaoAdapter adapter = new SpatialMetricDaoAdapter(asm);
                asm.addActiveSpatialMetricListener(adapter);
                daoAdapters.add(adapter);
        }
    }
    }

    @Override
    public void onStop() {
        for (DaoAdapter adapter : daoAdapters) {
            adapter.onStop();
        }
        /*
         * Commit missing values
         */
        DAO.commitQueue();
    }

    private interface DaoAdapter {
        void onStop();
    }

    /**
     * This class helps in persisting a metric using the {@link MeasurementDAO}
     *
     * @author Bjoern Richerzhagen
     * @version 1.0, 13.08.2012
     */
    private class MetricDaoAdapter implements ActiveMetricListener, DaoAdapter {

        private final ActiveMetric metric;

        private final MetricDescription md;

        private final MeasurementDAO dao = new MeasurementDAO();

        private final List<SimHost> hosts;

        private final boolean writeAggregates;

        private final Map<String, List<SimHost>> hostsByGroup;

        private final Map<String, DescriptiveStatistics> globalStatsByGroup;

        private long timestampLastEvent = -1;

        public MetricDaoAdapter(ActiveMetric metric) {
            this.metric = metric;
            this.md = new MetricDescription(MetricOutputDAO.class.getName(),
                    metric.getName(), metric.getDescription(),
                    metric.getUnit().toString());
            this.hosts = GlobalOracle.getHosts();
            this.writeAggregates = isToAggregate(metric);
            this.hostsByGroup = new LinkedHashMap<>();
            this.globalStatsByGroup = new LinkedHashMap<>();
            for (SimHost simHost : hosts) {
                String groupId = simHost.getProperties().getGroupID();
                if (!this.hostsByGroup.containsKey(groupId)) {
                    this.hostsByGroup.put(groupId, new LinkedList<>());
                    this.globalStatsByGroup.put(groupId,
                            new DescriptiveStatistics());
                }
                this.hostsByGroup.get(groupId).add(simHost);
            }
        }

        public void onStop() {
            if (writeAggregates) {
                for (Entry<String, DescriptiveStatistics> groupData : globalStatsByGroup
                        .entrySet()) {
                    MeasurementDAO.storeGroupStatisticsMeasurement(md,
                            groupData.getKey(), Time.getCurrentTime(),
                            groupData.getValue(),
                            Time.getCurrentTime() - timeEnableDao, true);
                }
                globalStatsByGroup.clear();
            }
        }

        @Override
        public void onMetricUpdate(ActiveMetric metric) {
            long time = Time.getCurrentTime();

            if (time < timeEnableDao || time > timeStopDao) {
                return;
            }

            if (metric.isOverallMetric()) {
                // global
                MetricValue mv = metric.getOverallMetric();
                Object val = mv.getValue();
                if (mv.isValid()) {
                    if (val instanceof Number) {
                        double vd = ((Number) val).doubleValue();
                        dao.storeGlobalSingleMeasurement(md, time, vd);
                    }
                }
            } else {
                // per-host metric
                if (writeAggregates) {
                    /*
                     * Write aggregates instead of individual metric values.
                     * This can be improved w.r.t. performance, but currently we
                     * do not really care.
                     */
                    // Iterate over groups
                    for (String group : hostsByGroup.keySet()) {
                        DescriptiveStatistics stats = new DescriptiveStatistics();
                        DescriptiveStatistics globalStats = globalStatsByGroup
                                .get(group);
                        for (SimHost host : hostsByGroup.get(group)) {
                            MetricValue mv = metric
                                    .getPerHostMetric(host.getId());
                            if (mv != null) {
                                Object val = mv.getValue();
                                if (mv.isValid()) {
                                    if (val instanceof Number) {
                                        double vd = ((Number) val)
                                                .doubleValue();
                                        if (Double.isNaN(vd)) {
                                            continue;
                                        }
                                        // Add valuer
                                        if(stats != null)
                                            stats.addValue(vd);
                                        if(globalStats != null)
                                            globalStats.addValue(vd);
                                    }
                                }
                            }
                        }
                        // Write Group stats
                        long observationDuration = Time.getCurrentTime()
                                - timestampLastEvent;
                        if (timestampLastEvent == -1) {
                            observationDuration = Time.getCurrentTime()
                                    - timeEnableDao;
                        }
                        MeasurementDAO.storeGroupStatisticsMeasurement(md,
                                group, time, stats, observationDuration, false);
                    }
                    timestampLastEvent = Time.getCurrentTime();
                } else {
                    for (SimHost host : hosts) {
                        MetricValue mv = metric.getPerHostMetric(host.getId());
                        if (mv != null) {
                            Object val = mv.getValue();
                            if (mv.isValid()) {
                                if (val instanceof Number) {
                                    double vd = ((Number) val).doubleValue();
                                    if (Double.isNaN(vd)) {
                                        continue;
                                    }
                                    dao.storeSingleMeasurement(md,
                                            host.getHostId(), time, vd);
                                }
                            }
                        }
                    }
                }
            }
        }

    }

    /**
     * This class helps in persisting a spatial metric using the
     * {@link MeasurementDAO}
     *
     * @author Tobias Meuser
     * @version 1.0, 10.10.2018
     */
    private class SpatialMetricDaoAdapter implements ActiveSpatialMetricListener, DaoAdapter {

        private final MetricDescription md;

        private final MeasurementDAO dao = new MeasurementDAO();

        private final List<SimHost> hosts;

        private final Map<String, List<SimHost>> hostsByGroup;

        private final Map<String, DescriptiveStatistics> globalStatsByGroup;

        private Map<GridKey, Double> values = new HashMap<>();
        private long lastUpdate = -1;

        private final double spatialResolution;

        private ActiveSpatialMetric<?> metric;

        public SpatialMetricDaoAdapter(ActiveSpatialMetric metric) {
            this.metric = metric;
            this.md = new MetricDescription(MetricOutputDAO.class.getName(), metric.getName(), metric.getDescription(),
                    metric.getUnit().toString());
            this.hosts = GlobalOracle.getHosts();
            this.hostsByGroup = new LinkedHashMap<>();
            this.globalStatsByGroup = new LinkedHashMap<>();
            for (SimHost simHost : hosts) {
                String groupId = simHost.getProperties().getGroupID();
                if (!this.hostsByGroup.containsKey(groupId)) {
                    this.hostsByGroup.put(groupId, new LinkedList<>());
                    this.globalStatsByGroup.put(groupId, new DescriptiveStatistics());
                }
                this.hostsByGroup.get(groupId).add(simHost);
            }

            spatialResolution = metric.getResolution();
        }

        @Override
        public void onStop() {
            store();
        }

        private void store() {
            for (Entry<GridKey, Double> entry : values.entrySet()) {
                dao.storeGlobalSpatialMeasurement(md, lastUpdate, entry.getValue(), entry.getKey().getX(),
                        entry.getKey().getY());
            }
            values.clear();
            lastUpdate = Time.getCurrentTime();
        }

        @Override
        public void onMetricUpdate(ActiveSpatialMetric<?> metric) {
            long time = Time.getCurrentTime();

            if (time < timeEnableDao || time > timeStopDao) {
                return;
            }


            if (metric.isOverallMetric()) {
                if (!metric.isAggregated() || Time.getCurrentTime() != lastUpdate) {
                    store();
                }

                // global
                SpatialMetricValue mv = metric.getOverallMetric();
                Object val = mv.getValue();
                if (mv.isValid()) {
                    if (val instanceof Number) {
                        double vd = ((Number) val).doubleValue();

                        GridKey key = new GridKey((int) (mv.getLocation().getLongitudeOrX() / spatialResolution),
                                (int) (mv.getLocation().getLatitudeOrY() / spatialResolution));
                        if (!values.containsKey(key)) {
                            values.put(key, vd);
                        } else {
                            double storedValue = values.get(key);
                            if (storedValue < vd) {
                                values.put(key, vd);
                            }
                        }
                    }
                }
            } else {
                for (SimHost host : hosts) {
                    SpatialMetricValue mv = metric.getPerHostMetric(host.getId());
                    if (mv != null) {
                        Object val = mv.getValue();
                        if (mv.isValid()) {
                            if (val instanceof Number) {
                                double vd = ((Number) val).doubleValue();
                                if (Double.isNaN(vd)) {
                                    continue;
                                }
                                dao.storeSpatialMeasurement(md, host.getId().value(), time, vd,
                                        (int) (mv.getLocation().getLongitudeOrX() / spatialResolution),
                                        (int) (mv.getLocation().getLatitudeOrY() / spatialResolution));
                            }
                        }
                    }
                }
            }
        }
    }

    private class GridKey {
        private final int x;
        private final int y;

        public GridKey(int pX, int pY) {
            x = pX;
            y = pY;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        @Override
        public int hashCode() {
            return Integer.hashCode(x) << 16 + Integer.hashCode(y);
        }

        @Override
        public boolean equals(Object pObj) {
            if (pObj instanceof GridKey) {
                return x == ((GridKey) pObj).x && y == ((GridKey) pObj).y;
            }
            return false;
        }
    }
}
