/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.camel.language.datasonnet;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.datasonnet.Mapper;
import com.datasonnet.MapperBuilder;
import com.datasonnet.document.DefaultDocument;
import com.datasonnet.document.Document;
import com.datasonnet.document.MediaType;
import com.datasonnet.document.MediaTypes;
import com.datasonnet.header.Header;
import com.datasonnet.spi.Library;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.RuntimeExpressionException;
import org.apache.camel.spi.ExpressionResultTypeAware;
import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.ExpressionAdapter;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DatasonnetExpression extends ExpressionAdapter implements ExpressionResultTypeAware {
    private static final Logger LOG = LoggerFactory.getLogger(DatasonnetExpression.class);

    private final String expression;
    private Expression source;
    private MediaType bodyMediaType;
    private MediaType outputMediaType;
    private Class<?> resultType;
    private Collection<String> libraryPaths;
    private transient DatasonnetLanguage language;

    public DatasonnetExpression(String expression) {
        this.expression = expression;
    }

    @Deprecated
    public static DatasonnetExpression builder(String expression) {
        return new DatasonnetExpression(expression);
    }

    @Deprecated
    public static DatasonnetExpression builder(String expression, Class<?> resultType) {
        DatasonnetExpression answer = new DatasonnetExpression(expression);
        answer.setResultType(resultType);
        return answer;
    }

    @Override
    public boolean matches(Exchange exchange) {
        this.outputMediaType = MediaTypes.APPLICATION_JAVA;
        return evaluate(exchange, Boolean.class);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T evaluate(Exchange exchange, Class<T> type) {
        try {
            // pass exchange to CML lib using thread as context
            CML.getInstance().getExchange().set(exchange);

            Document<?> result = doEvaluate(exchange);
            if (!type.equals(Object.class)) {
                return ExchangeHelper.convertToType(exchange, type, result.getContent());
            } else if (resultType == null || resultType.equals(Document.class)) {
                return (T) result;
            } else {
                return (T) result.getContent();
            }
        } catch (Exception e) {
            throw new RuntimeExpressionException("Unable to evaluate DataSonnet expression: " + expression, e);
        } finally {
            CML.getInstance().getExchange().remove();
        }
    }

    private Document<?> doEvaluate(Exchange exchange) {
        MediaType bodyMT = bodyMediaType;
        if (bodyMT == null && !expression.startsWith(Header.DATASONNET_HEADER)) {
            //Try to auto-detect input mime type if it was not explicitly set
            String typeHeader = exchange.getProperty(DatasonnetConstants.BODY_MEDIATYPE,
                    exchange.getIn().getHeader(Exchange.CONTENT_TYPE), String.class);
            if (typeHeader != null) {
                bodyMT = MediaType.valueOf(typeHeader);
            }
        }

        Object payload = source != null ? source.evaluate(exchange, Object.class) : exchange.getMessage().getBody();
        Document<?> doc = null;
        if (payload != null) {
            doc = exchange.getContext().getTypeConverter().tryConvertTo(Document.class, exchange, payload);
        }
        if (doc == null) {
            String text = exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, payload);
            if (exchange.getMessage().getBody() == null || "".equals(text)) {
                //Empty body, force type to be application/java
                doc = new DefaultDocument<>("", MediaTypes.APPLICATION_JAVA);
            } else if (MediaTypes.APPLICATION_JAVA.equalsTypeAndSubtype(bodyMT) || bodyMT == null) {
                doc = new DefaultDocument<>(payload);
            } else {
                // force using string value
                doc = new DefaultDocument<>(text, bodyMT);
            }
        }

        // the mapper is pre initialized but can become empty
        Mapper mapper = language.computeIfMiss(expression, () -> {
            MapperBuilder builder = new MapperBuilder(expression)
                    .withInputNames("body")
                    .withImports(resolveImports(language))
                    .withLibrary(CML.getInstance())
                    .withDefaultOutput(MediaTypes.APPLICATION_JAVA);

            Set<Library> additionalLibraries = exchange.getContext().getRegistry().findByType(com.datasonnet.spi.Library.class);
            for (Library lib : additionalLibraries) {
                builder = builder.withLibrary(lib);
            }
            return builder.build();
        });

        MediaType outMT = outputMediaType;
        if (outMT == null) {
            //Try to auto-detect output mime type if it was not explicitly set
            String typeHeader = exchange.getProperty(DatasonnetConstants.OUTPUT_MEDIATYPE,
                    exchange.getIn().getHeader(DatasonnetConstants.OUTPUT_MEDIATYPE), String.class);
            if (typeHeader != null) {
                outMT = MediaType.valueOf(typeHeader);
            } else {
                outMT = MediaTypes.ANY;
            }
        }

        Map<String, Document<?>> inputs = Collections.singletonMap("body", doc);
        if (resultType == null || resultType.equals(Document.class)) {
            return mapper.transform(doc, inputs, outMT, Object.class);
        } else {
            return mapper.transform(doc, inputs, outMT, resultType);
        }
    }

    private Map<String, String> resolveImports(DatasonnetLanguage language) {
        if (libraryPaths == null) {
            return language.getClasspathImports();
        }

        Map<String, String> answer = new HashMap<>();
        LOG.debug("Explicit library path is: {}", libraryPaths);
        for (String nextPath : libraryPaths) {
            final File nextLibDir = new File(nextPath);
            if (nextLibDir.isDirectory()) {
                try {
                    Files.walkFileTree(nextLibDir.toPath(), new SimpleFileVisitor<Path>() {
                        @Override
                        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                            File f = file.toFile();
                            if (!f.isDirectory() && f.getName().toLowerCase().endsWith(".libsonnet")) {
                                String content = IOUtils.toString(file.toUri(), Charset.defaultCharset());
                                Path relative = nextLibDir.toPath().relativize(file);
                                LOG.debug("Loading DataSonnet library: {}", relative);
                                answer.put(relative.toString(), content);
                            }
                            return FileVisitResult.CONTINUE;
                        }
                    });
                } catch (IOException e) {
                    LOG.warn("Unable to load DataSonnet library from: {}", nextPath, e);
                }
            }
        }

        return answer;
    }

    @Override
    public void init(CamelContext context) {
        super.init(context);

        language = (DatasonnetLanguage) context.resolveLanguage("datasonnet");
        // initialize mapper eager
        language.computeIfMiss(expression, () -> {
            MapperBuilder builder = new MapperBuilder(expression)
                    .withInputNames("body")
                    .withImports(resolveImports(language))
                    .withLibrary(CML.getInstance())
                    .withDefaultOutput(MediaTypes.APPLICATION_JAVA);

            Set<Library> additionalLibraries = context.getRegistry().findByType(com.datasonnet.spi.Library.class);
            for (Library lib : additionalLibraries) {
                builder = builder.withLibrary(lib);
            }
            return builder.build();
        });
    }

    // Getter/Setter methods
    // -------------------------------------------------------------------------

    public Expression getSource() {
        return source;
    }

    public void setSource(Expression source) {
        this.source = source;
    }

    public MediaType getBodyMediaType() {
        return bodyMediaType;
    }

    /**
     * The message's body MediaType
     */
    public void setBodyMediaType(MediaType inputMimeType) {
        this.bodyMediaType = inputMimeType;
    }

    public MediaType getOutputMediaType() {
        return outputMediaType;
    }

    /**
     * The MediaType to output
     */
    public void setOutputMediaType(MediaType outputMimeType) {
        this.outputMediaType = outputMimeType;
    }

    public Collection<String> getLibraryPaths() {
        return libraryPaths;
    }

    /**
     * The paths to search for .libsonnet files
     */
    public void setLibraryPaths(Collection<String> libraryPaths) {
        this.libraryPaths = libraryPaths;
    }

    @Override
    public String getExpressionText() {
        return this.expression;
    }

    @Override
    public Class<?> getResultType() {
        return this.resultType;
    }

    /**
     * Sets the class of the result type (type from output).
     * <p/>
     * The default result type is com.datasonnet.document.Document
     */
    public void setResultType(Class<?> targetType) {
        this.resultType = targetType;
    }

    // Fluent builder methods
    // -------------------------------------------------------------------------

    @Deprecated
    public DatasonnetExpression bodyMediaType(MediaType bodyMediaType) {
        setBodyMediaType(bodyMediaType);
        return this;
    }

    @Deprecated
    public DatasonnetExpression outputMediaType(MediaType outputMediaType) {
        setOutputMediaType(outputMediaType);
        return this;
    }

    @Override
    public String toString() {
        return "datasonnet: " + expression;
    }

}
