/*
 * Copyright (c) 2017 NIBIO <http://www.nibio.no/>. 
 * 
 * This file is part of VIPSLogic.
 * VIPSLogic is free software: you can redistribute it and/or modify
 * it under the terms of the NIBIO Open Source License as published by 
 * NIBIO, either version 1 of the License, or (at your option) any
 * later version.
 * 
 * VIPSLogic 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
 * NIBIO Open Source License for more details.
 * 
 * You should have received a copy of the NIBIO Open Source License
 * along with VIPSLogic.  If not, see <http://www.nibio.no/licenses/>.
 * 
 */

package no.nibio.vips.util.weather;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import no.nibio.vips.entity.WeatherObservation;
import no.nibio.vips.logic.entity.PointOfInterestWeatherStation;
import no.nibio.vips.logic.scheduling.model.PreprocessorException;
import no.nibio.vips.logic.util.SystemTime;
import org.apache.commons.io.IOUtils;

/**
 * @copyright 2017 <a href="http://www.nibio.no/">NIBIO</a>
 * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
 */
public class WeatherDataSourceUtil {

    /**
     * Fetches measured data from the stations weather data source, and optionally
     * a weather forecast provider (if so specified in the weather station configuration).
     * Regarding weather forecast parameters: All requested parameters need be present in the
     * forecast in order for any parameters to be fetched. So if you request e.g. TJM5 and TM,
     * you won't get forecast values for any of them, because TJM5 is not present. Solve this
     * by calling this method twice: Once for the parameters with forecasts, and one for the
     * remaining.
     *
     * @param station The WeatherStation to fetch data from
     * @param logIntervalId hourly/daily etc.
     * @param elementMeasurementTypes Which parameters should be fetched
     * @param startTime When to start
     * @param endTime When to stop
     * @param ignoreErrors if true, accepts errors and missing data from source
     * @return
     * @throws PreprocessorException
     */
    public List<WeatherObservation> getWeatherObservations(PointOfInterestWeatherStation station, Integer logIntervalId, String[] elementMeasurementTypes, Date startTime, Date endTime, Boolean ignoreErrors) throws WeatherDataSourceException {
        List<WeatherObservation> observations = this.getWeatherObservations(station.getDataFetchUri(), logIntervalId, elementMeasurementTypes, startTime, endTime, TimeZone.getTimeZone(station.getTimeZone()), ignoreErrors);
        Collections.sort(observations);
        Date latestTimeOfMeasuredObservations = observations.isEmpty() ? null : observations.get(observations.size() - 1).getTimeMeasured();
        //System.out.println("latestTimeOfMeasuredObservations = " + latestTimeOfMeasuredObservations);
        // Todo: We don't collect forecast data when the endTime is before the actual, current date (not systemdate)
        if (station.getWeatherForecastProviderId() != null && ! SystemTime.isSystemTimeOffsetFromNow()) {
            try {
                WeatherForecastProvider forecastProvider = WeatherStationProviderFactory.getWeatherForecastProvider(station.getWeatherForecastProviderId().getWeatherForecastProviderId());
                List<WeatherObservation> forecasts = forecastProvider.getWeatherForecasts(station);
                Map<String, List<WeatherObservation>> obsMap = new HashMap<>();
                for (String elementMeasurementType : elementMeasurementTypes) {
                    obsMap.put(elementMeasurementType, new ArrayList<>());
                }
                forecasts.stream().filter((obs) -> (
                        obs.getLogIntervalId().equals(logIntervalId) && obsMap.get(obs.getElementMeasurementTypeId()) != null)
                    ).forEachOrdered((obs) -> {
                        obsMap.get(obs.getElementMeasurementTypeId()).add(obs);
                    });
                Date latestCommonDate = null;
                for (String elementMeasurementType : elementMeasurementTypes) {
                    List<WeatherObservation> paramObs = obsMap.get(elementMeasurementType);
                    if (paramObs.isEmpty()) {
                        continue;
                    }
                    Collections.sort(paramObs);
                    //System.out.println("elementMeasurementType " + elementMeasurementType + " first date = " + paramObs.get(0).getTimeMeasured());
                    latestCommonDate = latestCommonDate == null ? paramObs.get(paramObs.size() - 1).getTimeMeasured() : new Date(Math.min(latestCommonDate.getTime(), paramObs.get(paramObs.size() - 1).getTimeMeasured().getTime()));
                }
                Integer previousObsSize = null;
                for (String elementMeasurementType : elementMeasurementTypes) {
                    List<WeatherObservation> paramForecastObs = obsMap.get(elementMeasurementType);
                    List<WeatherObservation> forecastsOutsideScope = new ArrayList<>();
                    for (WeatherObservation obs : paramForecastObs) {
                        if (obs.getTimeMeasured().compareTo(latestCommonDate) > 0 || (latestTimeOfMeasuredObservations != null && obs.getTimeMeasured().compareTo(latestTimeOfMeasuredObservations) <= 0)) {
                            forecastsOutsideScope.add(obs);
                        }
                    }
                    paramForecastObs.removeAll(forecastsOutsideScope);
                    if (previousObsSize != null && previousObsSize != paramForecastObs.size()) {
                        throw new WeatherDataSourceException("Weather forecast from " + station.getWeatherForecastProviderId().getName() + " does not provide consistent data. Array length differ " + "between parameters for same time span. First deviation " + "was found for " + elementMeasurementType + ", it has " + paramForecastObs.size() + " elements, compared to " + previousObsSize + " for the previous parameter. End of the period is " + latestCommonDate);
                    }
                }
                for (String elementMeasurementType : elementMeasurementTypes) {
                    observations.addAll(obsMap.get(elementMeasurementType));
                }
            } catch (ParseWeatherDataException ex) {
                throw new WeatherDataSourceException(ex.getMessage());
            }
        }
        return observations;
    }

    /**
     * Fetches measured data from the stations weather data source, and optionally
     * a weather forecast provider (if so specified in the weather station configuration).
     * Regarding weather forecast parameters: All requested parameters need be present in the
     * forecast in order for any parameters to be fetched. So if you request e.g. TJM5 and TM,
     * you won't get forecast values for any of them, because TJM5 is not present. Solve this
     * by calling this method twice: Once for the parameters with forecasts, and one for the
     * remaining.
     * 
     * Requires the source to provide a complete data set (still, no guarantee)
     *
     * @param station The WeatherStation to fetch data from
     * @param logIntervalId hourly/daily etc.
     * @param elementMeasurementTypes Which parameters should be fetched
     * @param startTime When to start
     * @param endTime When to stop
     * @return
     * @throws PreprocessorException
     */
    public List<WeatherObservation> getWeatherObservations(PointOfInterestWeatherStation station, Integer logIntervalId, String[] elementMeasurementTypes, Date startTime, Date endTime) throws WeatherDataSourceException
    {
        return this.getWeatherObservations(station, logIntervalId, elementMeasurementTypes, startTime, endTime, Boolean.FALSE);
    }
    
    public List<WeatherObservation> getWeatherObservations(String JSONtext) throws IOException {
        return new ObjectMapper().readValue(JSONtext, new TypeReference<List<WeatherObservation>>() {
        });
    }

    /**
     * Collects weather observations from a data source
     * @param fetchURI Base URI. E.g. http://lmt.nibio.no/agrometbase/export/getVIPS3JSONWeatherData.php?weatherStationId=13
     * @param logIntervalId
     * @param elementMeasurementTypes
     * @param startTime
     * @param endTime
     * @param timeZone
     * @return
     */
    public List<WeatherObservation> getWeatherObservations(String fetchURI, Integer logIntervalId, String[] elementMeasurementTypes, Date startTime, Date endTime, TimeZone timeZone, Boolean ignoreErrors) throws WeatherDataSourceException {
        SimpleDateFormat dateOutput = new SimpleDateFormat("yyyy-MM-dd");
        dateOutput.setTimeZone(timeZone);
        SimpleDateFormat hourOutput = new SimpleDateFormat("H");
        hourOutput.setTimeZone(timeZone);
        StringBuilder URL = new StringBuilder(fetchURI)
                .append(fetchURI.contains("?") ? "&" : "?")
                .append("logIntervalId=").append(logIntervalId)
                .append("&timeZone=").append(timeZone.getID())
                .append("&startDate=").append(dateOutput.format(startTime))
                .append("&startTime=").append(hourOutput.format(startTime))
                .append("&endDate=").append(dateOutput.format(endTime))
                .append("&endTime=").append(hourOutput.format(endTime));
        for (String type : elementMeasurementTypes) {
            URL.append("&elementMeasurementTypes[]=").append(type);
        }
        if(ignoreErrors)
        {
            URL.append("&ignoreErrors=true");
        }
        URLConnection URLConn = null;
        InputStream URLStream = null;
        InputStream error = null;
        String URLOutput;
        try {
            URL weatherURL = new URL(URL.toString());
            //System.out.println("URL=" + weatherURL);
            URLConn = weatherURL.openConnection();
            URLStream = URLConn.getInputStream();
            URLOutput = IOUtils.toString(URLStream);
            List<WeatherObservation> preliminaryResult = this.getWeatherObservations(URLOutput);
            List<WeatherObservation> filteredObservations = new ArrayList<>();
            preliminaryResult.stream().filter((candidateObs) -> (
                    candidateObs.getLogIntervalId().equals(logIntervalId)
                    && Arrays.asList(elementMeasurementTypes).contains(candidateObs.getElementMeasurementTypeId())
                    )
                ).forEachOrdered((candidateObs) -> {
                    filteredObservations.add(candidateObs);
                });
            return filteredObservations;
        } catch (IOException ex) {
            StringBuilder errorMessage = new StringBuilder().append("Could not fetch weather observations from URI ").append(URL.toString()).append(".\n");
            String errorOutput = "";
            try {
                error = ((HttpURLConnection) URLConn).getErrorStream();
                errorOutput = IOUtils.toString(error);
            } catch (IOException | NullPointerException ex2) {
            }
            if (errorOutput.isEmpty()) {
                errorMessage.append("There was no output from weather data server to explain this.");
            } else {
                errorMessage.append("Output from weather data server was: \n").append(errorOutput);
            }
            errorMessage.append("\nFirst error output from code was: \n").append(ex.getMessage());
            throw new WeatherDataSourceException(errorMessage.toString());
        } finally {
            if (URLStream != null) {
                IOUtils.closeQuietly(URLStream);
            }
            if (error != null) {
                IOUtils.closeQuietly(error);
            }
        }
    }

}