/*
 * Copyright (C) 2012-2018 52°North Initiative for Geospatial Open Source
 * Software GmbH
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 as published
 * by the Free Software Foundation.
 *
 * If the program is linked with libraries which are licensed under one of
 * the following licenses, the combination of the program with the linked
 * library is not considered a "derivative work" of the program:
 *
 *     - Apache License, version 2.0
 *     - Apache Software License, version 1.0
 *     - GNU Lesser General Public License, version 3
 *     - Mozilla Public License, versions 1.0, 1.1 and 2.0
 *     - Common Development and Distribution License (CDDL), version 1.0
 *
 * Therefore the distribution of the program linked with libraries licensed
 * under the aforementioned licenses, is permitted by the copyright holders
 * if the distribution is compliant with both the GNU General Public
 * License version 2 and the aforementioned licenses.
 *
 * This program 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.
 */
package org.n52.sos.ds;

import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.xml.namespace.QName;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.locationtech.jts.geom.Geometry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.n52.io.request.IoParameters;
import org.n52.series.db.DataAccessException;
import org.n52.series.db.HibernateSessionStore;
import org.n52.series.db.beans.FormatEntity;
import org.n52.series.db.beans.OfferingEntity;
import org.n52.series.db.beans.PhenomenonEntity;
import org.n52.series.db.beans.ProcedureEntity;
import org.n52.series.db.dao.DbQuery;
import org.n52.series.db.dao.OfferingDao;
import org.n52.series.db.dao.PhenomenonDao;
import org.n52.series.db.dao.ProcedureDao;
import org.n52.shetland.ogc.gml.ReferenceType;
import org.n52.shetland.ogc.gml.time.TimePeriod;
import org.n52.shetland.ogc.ows.exception.CodedException;
import org.n52.shetland.ogc.ows.exception.NoApplicableCodeException;
import org.n52.shetland.ogc.ows.exception.OwsExceptionReport;
import org.n52.shetland.ogc.ows.extension.CapabilitiesExtension;
import org.n52.shetland.ogc.ows.service.GetCapabilitiesRequest;
import org.n52.shetland.ogc.sos.Sos1Constants;
import org.n52.shetland.ogc.sos.Sos2Constants;
import org.n52.shetland.ogc.sos.SosConstants;
import org.n52.shetland.ogc.sos.SosObservationOffering;
import org.n52.shetland.ogc.sos.SosOffering;
import org.n52.shetland.ogc.sos.extension.SosObservationOfferingExtension;
import org.n52.shetland.ogc.sos.ro.RelatedOfferingConstants;
import org.n52.shetland.ogc.sos.ro.RelatedOfferings;
import org.n52.shetland.util.CollectionHelper;
import org.n52.shetland.util.OMHelper;
import org.n52.shetland.util.ReferencedEnvelope;
import org.n52.sos.config.CapabilitiesExtensionService;
import org.n52.sos.ogc.sos.SosObservationOfferingExtensionRepository;
import org.n52.sos.util.I18NHelper;
import org.n52.sos.util.JTSConverter;


/**
 * Implementation of the interface AbstractGetCapabilitiesHandler
 *
 * @since 4.0.0
 */
public class GetCapabilitiesHandler extends AbstractSosGetCapabilitiesHandler implements ApiQueryHelper {

    private static final Logger LOGGER = LoggerFactory.getLogger(GetCapabilitiesHandler.class);
    @Inject
    private HibernateSessionStore sessionStore;
    @Inject
    private CapabilitiesExtensionService capabilitiesExtensionService;
    @Inject
    private SosObservationOfferingExtensionRepository offeringExtensionRepository;

    /**
     * Get the contents for SOS 1.0.0 capabilities
     *
     * @return sectionSpecificContentObject metadata holder to get contents
     *
     * @throws OwsExceptionReport If an error occurs
     */
    @Override
    protected List<SosObservationOffering> getContentsForSosV1(SectionSpecificContentObject sectionSpecificContentObject)
            throws OwsExceptionReport {
        Session session = null;
        try {
            session = sessionStore.getSession();
            String version = sectionSpecificContentObject.getGetCapabilitiesResponse().getVersion();
            Collection<OfferingEntity> offerings = new OfferingDao(session)
                    .getAllInstances(new DbQuery(IoParameters.createDefaults()));
            List<SosObservationOffering> sosOfferings = new ArrayList<>(offerings.size());
            for (OfferingEntity offering : offerings) {
                Collection<ProcedureEntity> procedures = GetCapabilitiesHandler.this
                        .getProceduresForOfferingEntity(offering, session);
                ReferencedEnvelope envelopeForOffering = getCache().getEnvelopeForOffering(offering
                        .getIdentifier());
                Set<String> featuresForoffering = getFOI4offering(offering.getIdentifier());
                Collection<String> responseFormats = getResponseFormatRepository()
                        .getSupportedResponseFormats(SosConstants.SOS, Sos1Constants.SERVICEVERSION);
                if (checkOfferingValues(envelopeForOffering, featuresForoffering, responseFormats)) {
                    SosObservationOffering sosObservationOffering = new SosObservationOffering();

                    // insert observationTypes
                    sosObservationOffering.setObservationTypes(getObservationTypes(offering.getIdentifier()));

                    // only if fois are contained for the offering set the values of
                    // the envelope
                    if (offering.isSetGeometry()) {
                        sosObservationOffering.setObservedArea(processObservedArea(JTSConverter.convert(offering
                                .getGeometry())));
                    } else if (getCache().hasEnvelopeForOffering(offering.getIdentifier())) {
                        sosObservationOffering.setObservedArea(getCache().getEnvelopeForOffering(offering
                                .getIdentifier()));
                    }

                    // TODO: add intended application
                    // xb_oo.addIntendedApplication("");
                    // add offering name
                    addSosOfferingToObservationOffering(offering, sosObservationOffering,
                                                        sectionSpecificContentObject.getGetCapabilitiesRequest());

                    // set up phenomena
                    sosObservationOffering.setObservableProperties(getCache()
                            .getObservablePropertiesForOffering(offering.getIdentifier()));
                    Set<String> compositePhenomenonsForOffering = getCache().getCompositePhenomenonsForOffering(offering
                            .getIdentifier());
                    sosObservationOffering.setCompositePhenomena(compositePhenomenonsForOffering);

                    Map<String, Collection<String>> phens4CompPhens = Optional
                            .ofNullable(compositePhenomenonsForOffering)
                            .map(Set::stream).orElseGet(Stream::empty)
                            .collect(toMap(Function.identity(),
                                           getCache()::getObservablePropertiesForCompositePhenomenon));

                    sosObservationOffering.setPhens4CompPhens(phens4CompPhens);

                    // set up time
                    setUpTimeForOffering(offering, sosObservationOffering);

                    // add feature of interests
                    if (getProfileHandler().getActiveProfile().isListFeatureOfInterestsInOfferings()) {
                        sosObservationOffering.setFeatureOfInterest(getFOI4offering(offering.getIdentifier()));
                    }

                    // set procedures
                    sosObservationOffering.setProcedures(
                            procedures.stream().map(p -> p.getIdentifier()).collect(Collectors.toSet()));

                    // insert result models
                    Collection<QName> resultModels = OMHelper.getQNamesForResultModel(getCache()
                            .getObservationTypesForOffering(offering.getIdentifier()));
                    sosObservationOffering.setResultModels(resultModels);

                    // set response format
                    sosObservationOffering.setResponseFormats(responseFormats);

                    // set response Mode
                    sosObservationOffering.setResponseModes(SosConstants.RESPONSE_MODES);

                    sosOfferings.add(sosObservationOffering);
                }
            }

            return sosOfferings;
        } catch (HibernateException | DataAccessException e) {
            throw new NoApplicableCodeException().causedBy(e).withMessage(
                    "Error while querying data for GetCapabilities document!");
        } finally {
            sessionStore.returnSession(session);
        }
    }

    private ReferencedEnvelope processObservedArea(Geometry geometry) throws CodedException {
        return new ReferencedEnvelope(geometry.getEnvelopeInternal(), geometry.getSRID());
    }

    /**
     * Get the contents for SOS 2.0 capabilities
     *
     * @return sectionSpecificContentObject metadata holder to get contents
     *
     * @throws OwsExceptionReport * If an error occurs
     */
    @Override
    protected List<SosObservationOffering> getContentsForSosV2(SectionSpecificContentObject sectionSpecificContentObject)
            throws OwsExceptionReport {
        Session session = null;
        try {
            session = sessionStore.getSession();
            Collection<OfferingEntity> offerings = new OfferingDao(session).getAllInstances(new DbQuery(IoParameters
                    .createDefaults()));
            List<SosObservationOffering> sosOfferings = new ArrayList<>(offerings.size());
            Map<String, List<SosObservationOfferingExtension>> extensions = this.capabilitiesExtensionService
                    .getActiveOfferingExtensions();

            if (CollectionHelper.isEmpty(offerings)) {
                // Set empty offering to add empty Contents section to Capabilities
                sosOfferings.add(new SosObservationOffering());
            } else {
                if (checkListOnlyParentOfferings()) {
                    sosOfferings
                            .addAll(createAndGetParentOfferings(offerings, sectionSpecificContentObject, extensions, session));
                } else {
                    for (OfferingEntity offering : offerings) {
                        Collection<ProcedureEntity> procedures = getProceduresForOfferingEntity(offering, session);
                        if (!procedures.isEmpty()) {
                            Collection<FormatEntity> observationTypes = offering.getObservationTypes();
                            if (observationTypes != null && !observationTypes.isEmpty()) {
                                // FIXME why a loop? We are in SOS 2.0 context -> offering 1
                                // <-> 1 procedure!
                                for (ProcedureEntity procedure : procedures) {

                                    SosObservationOffering sosObservationOffering = new SosObservationOffering();

                                    // insert observationTypes
                                    sosObservationOffering.setObservationTypes(toStringSet(observationTypes));

                                    if (offering.isSetGeometry()) {
                                        sosObservationOffering.setObservedArea(processObservedArea(JTSConverter
                                                .convert(offering.getGeometry())));
                                    } else if (getCache().hasEnvelopeForOffering(offering.getIdentifier())) {
                                        sosObservationOffering.setObservedArea(getCache()
                                                .getEnvelopeForOffering(offering.getIdentifier()));
                                    }

                                    sosObservationOffering.setProcedures(Collections.singletonList(procedure
                                            .getIdentifier()));
                                    GetCapabilitiesRequest request = sectionSpecificContentObject
                                            .getGetCapabilitiesRequest();

                                    // TODO: add intended application
                                    // add offering to observation offering
                                    addSosOfferingToObservationOffering(offering, sosObservationOffering, request);
                                    // add offering extension
                                    if (offeringExtensionRepository.hasOfferingExtensionProviderFor(request)) {
                                        offeringExtensionRepository.getOfferingExtensionProvider(request)
                                                .stream().filter(Objects::nonNull)
                                                .filter(provider -> provider.hasExtendedOfferingFor(offering
                                                .getIdentifier()))
                                                .map(provider -> provider
                                                .getOfferingExtensions(offering.getIdentifier()))
                                                .forEach(sosObservationOffering::addExtensions);
                                    }

                                    if (extensions.containsKey(sosObservationOffering.getOffering().getIdentifier())) {
                                        extensions.get(sosObservationOffering.getIdentifier())
                                                .stream().map(CapabilitiesExtension::new)
                                                .forEach(sosObservationOffering::addExtension);
                                    }

                                    setUpPhenomenaForOffering(offering, procedure, sosObservationOffering, session);
                                    setUpTimeForOffering(offering, sosObservationOffering);
                                    setUpRelatedFeaturesForOffering(offering, sosObservationOffering);
                                    setUpFeatureOfInterestTypesForOffering(offering, sosObservationOffering);
                                    setUpProcedureDescriptionFormatForOffering(sosObservationOffering, Sos2Constants.SERVICEVERSION);
                                    setUpResponseFormatForOffering(sosObservationOffering, Sos2Constants.SERVICEVERSION);

                                    sosOfferings.add(sosObservationOffering);
                                }
                            }
                        } else {
                            LOGGER
                                    .error("No procedures are contained in the database for the offering {}! Please contact the admin of this SOS.",
                                           offering.getIdentifier());
                        }
                    }
                }
            }

            return sosOfferings;
        } catch (HibernateException | DataAccessException e) {
            throw new NoApplicableCodeException().causedBy(e).withMessage(
                    "Error while querying data for GetCapabilities document!");
        } finally {
            sessionStore.returnSession(session);
        }
    }

    private Collection<? extends SosObservationOffering> createAndGetParentOfferings(
            Collection<OfferingEntity> offerings,
            SectionSpecificContentObject sectionSpecificContentObject,
            Map<String, List<SosObservationOfferingExtension>> extensions, Session session) throws OwsExceptionReport,
                                                                                                   DataAccessException {
        Map<OfferingEntity, Set<OfferingEntity>> parentChilds = getParentOfferings(offerings);

        List<SosObservationOffering> sosOfferings = new ArrayList<>(parentChilds.size());
        for (Entry<OfferingEntity, Set<OfferingEntity>> entry : parentChilds.entrySet()) {
            Collection<String> observationTypes = getObservationTypes(entry);
            if (CollectionHelper.isNotEmpty(observationTypes)) {
                Collection<ProcedureEntity> procedures = getProceduresForOfferingEntity(entry, session);
                if (CollectionHelper.isNotEmpty(procedures)) {
                    Set<OfferingEntity> allOfferings = new HashSet<>();
                    allOfferings.addAll(entry.getValue());
                    allOfferings.add(entry.getKey());
                    SosObservationOffering sosObservationOffering = new SosObservationOffering();
                    sosObservationOffering.setObservationTypes(observationTypes);
                    sosObservationOffering.setObservedArea(getObservedArea(entry));

                    sosObservationOffering.setProcedures(
                            procedures.stream().map(p -> p.getIdentifier()).collect(Collectors.toSet()));
                    //
//                    // TODO: add intended application
//
//                    // add offering to observation offering
                    addSosOfferingToObservationOffering(entry.getKey(), sosObservationOffering, sectionSpecificContentObject
                                                        .getGetCapabilitiesRequest());
                    // add offering extension
                    if (offeringExtensionRepository.hasOfferingExtensionProviderFor(sectionSpecificContentObject
                            .getGetCapabilitiesRequest())) {
                        offeringExtensionRepository.getOfferingExtensionProvider(sectionSpecificContentObject
                                .getGetCapabilitiesRequest())
                                .stream().filter(Objects::nonNull)
                                .filter(provider -> provider.hasExtendedOfferingFor(entry.getKey().getIdentifier()))
                                .map(provider -> provider.getOfferingExtensions(entry.getKey().getIdentifier()))
                                .forEach(sosObservationOffering::addExtensions);
                    }
                    if (extensions.containsKey(sosObservationOffering.getOffering().getIdentifier())) {
                        extensions.get(sosObservationOffering.getOffering().getIdentifier())
                                .forEach(sosObservationOffering::addExtension);
                    }
                    // add sub-level offerings
                    if (!entry.getValue().isEmpty()) {
                        RelatedOfferings relatedOfferings = new RelatedOfferings();
                        String gdaURL = getGetDataAvailabilityUrl();
                        gdaURL = addParameter(gdaURL, "responseFormat", "http://www.opengis.net/sosgda/2.0");
                        for (OfferingEntity offering : entry.getValue()) {
                            relatedOfferings.addValue(new ReferenceType(RelatedOfferingConstants.ROLE),
                                                      new ReferenceType(
                                                              addParameter(gdaURL, "offering", offering.getIdentifier()),
                                                              offering.getIdentifier()));
                        }
                        sosObservationOffering.addExtension(relatedOfferings);
                    }

                    setUpPhenomenaForOffering(allOfferings, procedures.iterator().next(), sosObservationOffering, session);
                    setUpTimeForOffering(allOfferings.stream().map(OfferingEntity::getIdentifier), sosObservationOffering);
                    setUpRelatedFeaturesForOffering(allOfferings, sosObservationOffering);
                    setUpFeatureOfInterestTypesForOffering(allOfferings, sosObservationOffering);
                    setUpProcedureDescriptionFormatForOffering(sosObservationOffering, Sos2Constants.SERVICEVERSION);
                    setUpResponseFormatForOffering(sosObservationOffering, Sos2Constants.SERVICEVERSION);

                    sosOfferings.add(sosObservationOffering);
                }
            }
        }
        return sosOfferings;
    }

    private Map<OfferingEntity, Set<OfferingEntity>> getParentOfferings(Collection<OfferingEntity> offerings) {
        return offerings.stream().filter(OfferingEntity::hasParents)
                .collect(toMap(Function.identity(), this::getAllParents));
    }

    private Set<OfferingEntity> getAllParents(OfferingEntity entity) {
        return Stream.concat(Stream.of(entity), entity.getParents().stream()
                             .map(this::getAllParents).flatMap(Set::stream)).collect(toSet());
    }

    private void addSosOfferingToObservationOffering(OfferingEntity offering,
                                                     SosObservationOffering sosObservationOffering,
                                                     GetCapabilitiesRequest request) throws CodedException {
        SosOffering sosOffering = new SosOffering(offering.getIdentifier(), false);
        sosObservationOffering.setOffering(sosOffering);
        // add offering name
        I18NHelper.addOfferingNames(getCache(), sosOffering, getRequestedLocale(request), Locale.ROOT, false);
        // add offering description
        I18NHelper.addOfferingDescription(sosOffering, getRequestedLocale(request), Locale.ROOT, getCache());
    }

    private Collection<String> getObservationTypes(Entry<OfferingEntity, Set<OfferingEntity>> entry) {
        Set<String> observationTypes = new HashSet<>();
        entry.getValue().stream()
                .map(OfferingEntity::getIdentifier)
                .map(this::getObservationTypes)
                .forEach(observationTypes::addAll);
        observationTypes.addAll(getObservationTypes(entry.getKey().getIdentifier()));
        return observationTypes;
    }

    protected void setUpPhenomenaForOffering(Set<OfferingEntity> allOfferings, ProcedureEntity procedure,
                                             SosObservationOffering sosObservationOffering, Session session) throws
            DataAccessException {
        for (OfferingEntity offering : allOfferings) {
            setUpPhenomenaForOffering(offering, procedure, sosObservationOffering, session);
        }
    }

    protected void setUpPhenomenaForOffering(OfferingEntity offering, ProcedureEntity procedure,
                                             SosObservationOffering sosOffering, Session session) throws
            DataAccessException {
        Map<String, String> map = new HashMap<>();
        map.put(IoParameters.OFFERINGS, Long.toString(offering.getId()));
        map.put(IoParameters.PROCEDURES, Long.toString(procedure.getId()));

        List<PhenomenonEntity> observableProperties = new PhenomenonDao(session)
                .getAllInstances(new DbQuery(IoParameters.createFromSingleValueMap(map)));

        Collection<String> phenomenons = new LinkedList<>();
        Map<String, Collection<String>> phens4CompPhens = new HashMap<>(observableProperties.size());
        observableProperties.forEach(observableProperty -> {
            if (!observableProperty.hasChildren() && !observableProperty.hasParents()) {
                phenomenons.add(observableProperty.getIdentifier());
            } else if (observableProperty.hasChildren() && !observableProperty.hasParents()) {
                phens4CompPhens.put(observableProperty.getIdentifier(),
                                    observableProperty.getChildren().stream().map(PhenomenonEntity::getIdentifier)
                                            .collect(toCollection(TreeSet::new)));
            }

        });
        sosOffering.setObservableProperties(phenomenons);
        sosOffering.setPhens4CompPhens(phens4CompPhens);
    }

    protected void setUpTimeForOffering(OfferingEntity offering, SosObservationOffering sosOffering) {
        sosOffering.setPhenomenonTime(new TimePeriod(offering.getSamplingTimeStart(), offering.getSamplingTimeEnd()));
        sosOffering.setResultTime(new TimePeriod(offering.getResultTimeStart(), offering.getResultTimeEnd()));
    }

    protected void setUpFeatureOfInterestTypesForOffering(Collection<OfferingEntity> offerings,
                                                          SosObservationOffering sosOffering) {
        sosOffering.setFeatureOfInterestTypes(offerings.stream().map(OfferingEntity::getFeatureTypes)
                .flatMap(Set::stream).map(FormatEntity::getFormat).collect(toSet()));
    }

    protected void setUpFeatureOfInterestTypesForOffering(OfferingEntity offering,
                                                          SosObservationOffering sosOffering) {
        sosOffering.setFeatureOfInterestTypes(toStringSet(offering.getFeatureTypes()));
    }

    private void setUpRelatedFeaturesForOffering(OfferingEntity offering, SosObservationOffering sosObservationOffering)
            throws OwsExceptionReport {
        setUpRelatedFeaturesForOffering(Collections.singleton(offering), sosObservationOffering);
    }

    private void setUpRelatedFeaturesForOffering(Collection<OfferingEntity> offerings,
                                                 SosObservationOffering sosObservationOffering)
            throws OwsExceptionReport {
        setUpRelatedFeaturesForOffering(offerings.stream().map(OfferingEntity::getIdentifier), sosObservationOffering);
    }

    private ReferencedEnvelope getObservedArea(Entry<OfferingEntity, Set<OfferingEntity>> entry)
            throws OwsExceptionReport {
        ReferencedEnvelope envelope = new ReferencedEnvelope();
        if (!entry.getValue().isEmpty()) {
            for (OfferingEntity offering : entry.getValue()) {
                envelope.expandToInclude(getObservedArea(offering.getIdentifier()));
            }
        } else {
            envelope.expandToInclude(getObservedArea(entry.getKey().getIdentifier()));
        }
        return envelope;
    }

    private Collection<ProcedureEntity> getProceduresForOfferingEntity(
            Entry<OfferingEntity, Set<OfferingEntity>> entry, Session session)
            throws OwsExceptionReport, DataAccessException {
        Collection<ProcedureEntity> procedures = new HashSet<>();
        for (OfferingEntity offering : entry.getValue()) {
            procedures.addAll(getProceduresForOfferingEntity(offering, session));
        }
        procedures.addAll(getProceduresForOfferingEntity(entry.getKey(), session));
        return procedures;
    }

    private Collection<ProcedureEntity> getProceduresForOfferingEntity(OfferingEntity offering, Session session)
            throws OwsExceptionReport, DataAccessException {
        Map<String, String> map = new HashMap<>(1);
        map.put(IoParameters.OFFERINGS, Long.toString(offering.getId()));
        return new ProcedureDao(session).getAllInstances(new DbQuery(IoParameters.createFromSingleValueMap(map)));
    }
}
