/*
 * Copyright 2015-2020 52┬░North Initiative for Geospatial Open Source
 * Software GmbH
 *
 * 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 org.n52.svalbard.decode.json.wps;

import com.fasterxml.jackson.databind.JsonNode;
import org.n52.shetland.ogc.ows.OwsAllowedValues;
import org.n52.shetland.ogc.ows.OwsAnyValue;
import org.n52.shetland.ogc.ows.OwsCRS;
import org.n52.shetland.ogc.ows.OwsDomainMetadata;
import org.n52.shetland.ogc.ows.OwsMetadata;
import org.n52.shetland.ogc.ows.OwsPossibleValues;
import org.n52.shetland.ogc.ows.OwsRange;
import org.n52.shetland.ogc.ows.OwsValue;
import org.n52.shetland.ogc.ows.OwsValuesReference;
import org.n52.shetland.ogc.wps.Format;
import org.n52.shetland.ogc.wps.description.BoundingBoxDescription;
import org.n52.shetland.ogc.wps.description.BoundingBoxInputDescription;
import org.n52.shetland.ogc.wps.description.BoundingBoxOutputDescription;
import org.n52.shetland.ogc.wps.description.ComplexDescription;
import org.n52.shetland.ogc.wps.description.ComplexInputDescription;
import org.n52.shetland.ogc.wps.description.ComplexOutputDescription;
import org.n52.shetland.ogc.wps.description.Description;
import org.n52.shetland.ogc.wps.description.LiteralDataDomain;
import org.n52.shetland.ogc.wps.description.LiteralDescription;
import org.n52.shetland.ogc.wps.description.LiteralInputDescription;
import org.n52.shetland.ogc.wps.description.LiteralOutputDescription;
import org.n52.shetland.ogc.wps.description.ProcessDescription;
import org.n52.shetland.ogc.wps.description.ProcessDescriptionBuilderFactory;
import org.n52.shetland.ogc.wps.description.ProcessInputDescription;
import org.n52.shetland.ogc.wps.description.ProcessOutputDescription;
import org.n52.shetland.ogc.wps.description.impl.ProcessDescriptionFactory;
import org.n52.svalbard.coding.json.JSONConstants;
import org.n52.svalbard.decode.exception.DecodingException;
import org.n52.svalbard.decode.json.JSONDecoder;

import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.StreamSupport;

public class ProcessDescriptionDecoder extends JSONDecoder<ProcessDescription> {
    private final ProcessDescriptionBuilderFactory<?, ?, ?, ?, ?, ?, ?, ?, ?, ?> factory;

    public ProcessDescriptionDecoder() {
        this(null);
    }

    public ProcessDescriptionDecoder(ProcessDescriptionBuilderFactory factory) {
        super(ProcessDescription.class);
        this.factory = Optional.ofNullable(factory).orElseGet(ProcessDescriptionFactory::instance);
    }

    @Override
    public ProcessDescription decodeJSON(JsonNode node, boolean validate) throws DecodingException {
        if (node == null || node.isNull()) {
            return null;
        }
        return decodeProcess(node);
    }

    private ProcessDescription decodeProcess(JsonNode node) throws DecodingException {
        if (!node.isObject()) {
            throw new DecodingException("expected object");
        }
        ProcessDescription.Builder<?, ?> builder = factory.process();
        decodeDescription(builder, node);
        for (JsonNode input : node.path(JSONConstants.INPUTS)) {
            builder.withInput(decodeInput(input));
        }
        for (JsonNode output : node.path(JSONConstants.OUTPUTS)) {
            builder.withOutput(decodeOutput(output));
        }
        if (node.path(JSONConstants.VERSION).isValueNode()) {
            builder.withVersion(node.path(JSONConstants.VERSION).textValue());
        }
        return builder.build();
    }

    private ProcessInputDescription decodeInput(JsonNode node) throws DecodingException {
        if (node.path(JSONConstants.INPUT).has(JSONConstants.LITERAL_DATA_DOMAINS)) {
            return decodeLiteralInput(node);
        } else if (node.path(JSONConstants.INPUT).has(JSONConstants.FORMATS)) {
            return decodeComplexInput(node);
        } else if (node.path(JSONConstants.INPUT).has(JSONConstants.SUPPORTED_CRS)) {
            return decodeBoundingBoxInput(node);
        }
        throw new DecodingException("unsupported input" + node);
    }

    private LiteralInputDescription decodeLiteralInput(JsonNode node) throws DecodingException {
        LiteralInputDescription.Builder<?, ?> builder = factory.literalInput();
        decodeInputDescription(builder, node);
        decodeSupportedLiteralDataDomains(builder, node.path(JSONConstants.INPUT));
        return builder.build();
    }

    private void decodeSupportedLiteralDataDomains(LiteralDescription.Builder<?, ?> builder, JsonNode node)
            throws DecodingException {
        JsonNode domains = node.path(JSONConstants.LITERAL_DATA_DOMAINS);
        LiteralDataDomain defaultLiteralDataDomain = null;
        List<LiteralDataDomain> literalDataDomains = new ArrayList<>(domains.size());
        for (JsonNode domainNode : domains) {
            LiteralDataDomain literalDataDomain = decodeLiteralDataDomain(domainNode);
            literalDataDomains.add(literalDataDomain);
            if (domainNode.path(JSONConstants.DEFAULT).asBoolean(false)) {
                defaultLiteralDataDomain = literalDataDomain;
            }
        }
        if (defaultLiteralDataDomain == null && !literalDataDomains.isEmpty()) {
            defaultLiteralDataDomain = literalDataDomains.iterator().next();
        }
        if (defaultLiteralDataDomain == null) {
            throw new DecodingException("missing literal data domain");
        }
        builder.withDefaultLiteralDataDomain(defaultLiteralDataDomain);
        builder.withSupportedLiteralDataDomain(literalDataDomains);

    }

    private LiteralDataDomain decodeLiteralDataDomain(JsonNode node) {
        LiteralDataDomain.Builder<?, ?> builder = factory.literalDataDomain();
        if (!node.path(JSONConstants.DEFAULT_VALUE).isMissingNode()) {
            builder.withDefaultValue(node.path(JSONConstants.DEFAULT_VALUE).textValue());
        }
        JsonNode dataTypeNode = node.path(JSONConstants.DATA_TYPE);
        if (!dataTypeNode.isMissingNode()) {
            builder.withDataType(decodeDomainMetadata(dataTypeNode));
        }
        JsonNode uomNode = node.path(JSONConstants.UOM);
        if (!uomNode.isMissingNode()) {
            builder.withUOM(decodeDomainMetadata(uomNode));
        }
        builder.withValueDescription(decodeValueDefinition(node.path(JSONConstants.VALUE_DEFINITION)));
        return builder.build();
    }

    private OwsPossibleValues decodeValueDefinition(JsonNode node) {
        if (node.isObject()) {
            return OwsAnyValue.instance();
        } else if (node.isArray()) {
            return new OwsAllowedValues(StreamSupport.stream(node.spliterator(), false)
                                                     .map(n -> n.isObject() ? decodeRange(n) : decodeValue(n))
                                                     .filter(Objects::nonNull));
        } else {
            return new OwsValuesReference(URI.create(node.asText()));
        }
    }

    private OwsValue decodeValue(JsonNode node) {
        if (!node.isValueNode()) {
            return null;
        }
        String text = node.asText();
        return new OwsValue(text);
    }

    private OwsRange decodeRange(JsonNode node) {
        OwsValue min = decodeValue(node.path(JSONConstants.MINIMUM_VALUE));
        OwsValue max = decodeValue(node.path(JSONConstants.MAXIMUM_VALUE));
        OwsValue spacing = decodeValue(node.path(JSONConstants.SPACING));
        String rangeClosure = node.path(JSONConstants.RANGE_CLOSURE).textValue();
        return new OwsRange(min, max, rangeClosure, spacing);
    }

    private OwsDomainMetadata decodeDomainMetadata(JsonNode dataTypeNode) {
        String name = dataTypeNode.path(JSONConstants.NAME).textValue();
        URI reference = Optional.ofNullable(dataTypeNode.path(JSONConstants.REFERENCE).textValue())
                                .map(URI::create).orElse(null);
        return new OwsDomainMetadata(reference, name);
    }

    private ComplexInputDescription decodeComplexInput(JsonNode node) throws DecodingException {
        ComplexInputDescription.Builder<?, ?> builder = factory.complexInput();
        decodeInputDescription(builder, node);
        BigInteger maximumMegabytes = decodeSupportedFormats(builder, node.path(JSONConstants.INPUT));
        return builder.withMaximumMegabytes(maximumMegabytes).build();
    }

    private BigInteger getMaximumMegabytes(BigInteger currentMaximum, JsonNode formatNode) {
        if (formatNode.path(JSONConstants.MAXIMUM_MEGABYTES).isNumber()) {
            BigInteger value = formatNode.path(JSONConstants.MAXIMUM_MEGABYTES).bigIntegerValue();
            if (currentMaximum == null || currentMaximum.compareTo(value) > 0) {
                return value;
            }
        }
        return currentMaximum;
    }

    private Format decodeFormat(JsonNode node) {
        return new Format(node.path(JSONConstants.MIME_TYPE).textValue(),
                          node.path(JSONConstants.ENCODING).textValue(),
                          node.path(JSONConstants.SCHEMA).textValue());
    }

    private BoundingBoxInputDescription decodeBoundingBoxInput(JsonNode node) throws DecodingException {
        BoundingBoxInputDescription.Builder<?, ?> builder = factory.boundingBoxInput();
        decodeInputDescription(builder, node);
        decodeSupportedCRS(builder, node.path(JSONConstants.INPUT));
        return builder.build();
    }

    private ProcessOutputDescription decodeOutput(JsonNode node) throws DecodingException {
        if (node.path(JSONConstants.OUTPUT).has(JSONConstants.LITERAL_DATA_DOMAINS)) {
            return decodeLiteralOutput(node);
        } else if (node.path(JSONConstants.OUTPUT).has(JSONConstants.FORMATS)) {
            return decodeComplexOutput(node);
        } else if (node.path(JSONConstants.OUTPUT).has(JSONConstants.SUPPORTED_CRS)) {
            return decodeBoundingBoxOutput(node);
        }
        throw new DecodingException("unsupported output" + node);
    }

    private BoundingBoxOutputDescription decodeBoundingBoxOutput(JsonNode node) throws DecodingException {
        BoundingBoxOutputDescription.Builder<?, ?> builder = factory.boundingBoxOutput();
        decodeDescription(builder, node);
        decodeSupportedCRS(builder, node.path(JSONConstants.OUTPUT));
        return builder.build();
    }

    private ComplexOutputDescription decodeComplexOutput(JsonNode node) throws DecodingException {
        ComplexOutputDescription.Builder<?, ?> builder = factory.complexOutput();
        decodeDescription(builder, node);
        BigInteger maximumMegabytes = decodeSupportedFormats(builder, node.path(JSONConstants.OUTPUT));
        return builder.withMaximumMegabytes(maximumMegabytes).build();
    }

    private BigInteger decodeSupportedFormats(ComplexDescription.Builder<?, ?> builder, JsonNode node)
            throws DecodingException {
        JsonNode formatsNode = node.path(JSONConstants.FORMATS);
        BigInteger maximumMegabytes = null;
        Format defaultFormat = null;
        List<Format> formats = new ArrayList<>(formatsNode.size());
        for (JsonNode formatNode : formatsNode) {
            Format format = decodeFormat(formatNode);
            formats.add(format);
            if (formatNode.path(JSONConstants.DEFAULT).asBoolean(false)) {
                defaultFormat = format;
            }
            maximumMegabytes = getMaximumMegabytes(maximumMegabytes, formatNode);
        }

        if (defaultFormat == null && !formats.isEmpty()) {
            defaultFormat = formats.iterator().next();
        }
        if (defaultFormat == null) {
            throw new DecodingException("missing default format");
        }
        builder.withDefaultFormat(defaultFormat);
        builder.withSupportedFormat(formats);
        return maximumMegabytes;
    }

    private LiteralOutputDescription decodeLiteralOutput(JsonNode node) throws DecodingException {
        LiteralOutputDescription.Builder<?, ?> builder = factory.literalOutput();
        decodeDescription(builder, node);
        decodeSupportedLiteralDataDomains(builder, node.path(JSONConstants.OUTPUT));
        return builder.build();
    }

    private void decodeInputDescription(ProcessInputDescription.Builder<?, ?> builder, JsonNode node) {
        decodeDescription(builder, node);
        if (node.path(JSONConstants.MIN_OCCURS).isNumber()) {
            builder.withMinimalOccurence(node.path(JSONConstants.MIN_OCCURS).bigIntegerValue());
        }
        if (node.path(JSONConstants.MAX_OCCURS).isNumber()) {
            builder.withMaximalOccurence(node.path(JSONConstants.MAX_OCCURS).bigIntegerValue());
        }
    }

    private void decodeSupportedCRS(BoundingBoxDescription.Builder<?, ?> builder, JsonNode node)
            throws DecodingException {
        JsonNode crsNodes = node.path(JSONConstants.SUPPORTED_CRS);
        OwsCRS defaultCRS = null;
        List<OwsCRS> supportedCRS = new ArrayList<>(crsNodes.size());
        for (JsonNode crsNode : crsNodes) {
            OwsCRS crs = new OwsCRS(URI.create(crsNode.path(JSONConstants.CRS).textValue()));
            supportedCRS.add(crs);
            if (crsNode.path(JSONConstants.DEFAULT).asBoolean(false)) {
                defaultCRS = crs;
            }
        }
        if (defaultCRS == null && !supportedCRS.isEmpty()) {
            defaultCRS = supportedCRS.iterator().next();
        }
        if (defaultCRS == null) {
            throw new DecodingException("missing default crs");
        }
        builder.withDefaultCRS(defaultCRS);
        builder.withSupportedCRS(supportedCRS);
    }

    private void decodeDescription(Description.Builder<?, ?> builder, JsonNode node) {
        builder.withIdentifier(node.path(JSONConstants.ID).textValue());
        builder.withTitle(node.path(JSONConstants.TITLE).textValue());
        builder.withAbstract(node.path(JSONConstants.DESCRIPTION).textValue());

        for (JsonNode metadata : node.path(JSONConstants.METADATA)) {
            URI role = Optional.ofNullable(metadata.path(JSONConstants.ROLE).textValue()).map(URI::create).orElse(null);
            URI href = Optional.ofNullable(metadata.path(JSONConstants.HREF).textValue()).map(URI::create).orElse(null);
            builder.withMetadata(new OwsMetadata(href, role, null, null, null, null, null));
        }

        for (JsonNode keyword : node.path(JSONConstants.KEYWORDS)) {
            builder.withKeyword(keyword.textValue());
        }
    }
}