/**
 * 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.model.dataformat;

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import org.apache.camel.CamelContext;
import org.apache.camel.model.DataFormatDefinition;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.RouteContext;
import org.apache.camel.util.ObjectHelper;

/**
 * YAML data format
 *
 * @version 
 */
@Metadata(label = "dataformat,transformation,yaml", title = "YAML")
@XmlRootElement(name = "yaml")
@XmlAccessorType(XmlAccessType.FIELD)
public class YAMLDataFormat extends DataFormatDefinition {
    @XmlAttribute @Metadata(defaultValue = "SnakeYAML")
    private YAMLLibrary library = YAMLLibrary.SnakeYAML;
    @XmlTransient
    private ClassLoader classLoader;
    @XmlTransient
    private Class<?> unmarshalType;
    @XmlAttribute
    private String unmarshalTypeName;
    @XmlAttribute
    private String constructor;
    @XmlAttribute
    private String representer;
    @XmlAttribute
    private String dumperOptions;
    @XmlAttribute
    private String resolver;
    @XmlAttribute @Metadata(defaultValue = "true")
    private Boolean useApplicationContextClassLoader = true;
    @XmlAttribute @Metadata(defaultValue = "false")
    private Boolean prettyFlow = false;
    @XmlAttribute @Metadata(defaultValue = "false")
    private Boolean allowAnyType = false;
    @XmlElement(name = "typeFilter")
    private List<YAMLTypeFilterDefinition> typeFilters;

    public YAMLDataFormat() {
        this(YAMLLibrary.SnakeYAML);
    }

    public YAMLDataFormat(YAMLLibrary library) {
        super("yaml-" + library.name().toLowerCase());
        this.library = library;
    }

    public YAMLDataFormat(YAMLLibrary library, Class<?> unmarshalType) {
        super("yaml-" + library.name().toLowerCase());
        this.library = library;
        this.unmarshalType = unmarshalType;
    }

    public YAMLLibrary getLibrary() {
        return library;
    }

    /**
     * Which yaml library to use such.
     * <p/>
     * Is by default SnakeYAML
     */
    public void setLibrary(YAMLLibrary library) {
        this.library = library;
        setDataFormatName("yaml-" + library.name().toLowerCase());
    }

    public Class<?> getUnmarshalType() {
        return unmarshalType;
    }

    /**
     * Class of the object to be created
     */
    public void setUnmarshalType(Class<?> type) {
        this.unmarshalType = type;
    }

    public String getUnmarshalTypeName() {
        return unmarshalTypeName;
    }

    /**
     * Class name of the java type to use when unarmshalling
     */
    public void setUnmarshalTypeName(String unmarshalTypeName) {
        this.unmarshalTypeName = unmarshalTypeName;
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    /**
     * Set a custom classloader
     */
    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public String getConstructor() {
        return constructor;
    }

    /**
     * BaseConstructor to construct incoming documents.
     */
    public void setConstructor(String constructor) {
        this.constructor = constructor;
    }

    public String getRepresenter() {
        return representer;
    }

    /**
     * Representer to emit outgoing objects.
     */
    public void setRepresenter(String representer) {
        this.representer = representer;
    }

    public String getDumperOptions() {
        return dumperOptions;
    }

    /**
     * DumperOptions to configure outgoing objects.
     */
    public void setDumperOptions(String dumperOptions) {
        this.dumperOptions = dumperOptions;
    }

    public String getResolver() {
        return resolver;
    }

    /**
     * Resolver to detect implicit type
     */
    public void setResolver(String resolver) {
        this.resolver = resolver;
    }

    public boolean isUseApplicationContextClassLoader() {
        return useApplicationContextClassLoader;
    }

    /**
     * Use ApplicationContextClassLoader as custom ClassLoader
     */
    public void setUseApplicationContextClassLoader(boolean useApplicationContextClassLoader) {
        this.useApplicationContextClassLoader = useApplicationContextClassLoader;
    }

    public boolean isPrettyFlow() {
        return prettyFlow;
    }

    /**
     * Force the emitter to produce a pretty YAML document when using the flow
     * style.
     */
    public void setPrettyFlow(boolean prettyFlow) {
        this.prettyFlow = prettyFlow;
    }

    public boolean isAllowAnyType() {
        return allowAnyType;
    }

    /**
     * Allow any class to be un-marshaled
     */
    public void setAllowAnyType(boolean allowAnyType) {
        this.allowAnyType = allowAnyType;
    }

    public List<YAMLTypeFilterDefinition> getTypeFilters() {
        return typeFilters;
    }

    /**
     * Set the types SnakeYAML is allowed to un-marshall
     */
    public void setTypeFilters(List<YAMLTypeFilterDefinition> typeFilters) {
        this.typeFilters = typeFilters;
    }

    @Override
    protected DataFormat createDataFormat(RouteContext routeContext) {
        if (library == YAMLLibrary.SnakeYAML) {
            setProperty(routeContext.getCamelContext(), this, "dataFormatName", "yaml-snakeyaml");
        }

        return super.createDataFormat(routeContext);
    }

    @Override
    protected void configureDataFormat(DataFormat dataFormat, CamelContext camelContext) {
        if (library == YAMLLibrary.SnakeYAML) {
            configureSnakeDataFormat(dataFormat, camelContext);
        }
    }

    protected void configureSnakeDataFormat(DataFormat dataFormat, CamelContext camelContext) {
        Class<?> yamlUnmarshalType =  unmarshalType;
        if (yamlUnmarshalType == null && unmarshalTypeName != null) {
            try {
                yamlUnmarshalType = camelContext.getClassResolver().resolveMandatoryClass(unmarshalTypeName);
            } catch (ClassNotFoundException e) {
                throw ObjectHelper.wrapRuntimeCamelException(e);
            }
        }

        setProperty(dataFormat, camelContext, "unmarshalType", yamlUnmarshalType);
        setProperty(dataFormat, camelContext, "classLoader", classLoader);
        setProperty(dataFormat, camelContext, "useApplicationContextClassLoader", useApplicationContextClassLoader);
        setProperty(dataFormat, camelContext, "prettyFlow", prettyFlow);
        setProperty(dataFormat, camelContext, "allowAnyType", allowAnyType);

        if (typeFilters != null && !typeFilters.isEmpty()) {
            List<String> typeFilterDefinitions = new ArrayList<>(typeFilters.size());
            for (YAMLTypeFilterDefinition definition : typeFilters) {
                String value = definition.getValue();

                if (!value.startsWith("type") && !value.startsWith("regexp")) {
                    YAMLTypeFilterType type = definition.getType();
                    if (type == null) {
                        type = YAMLTypeFilterType.type;
                    }

                    value = type.name() + ":" + value;
                }

                typeFilterDefinitions.add(value);
            }

            setProperty(dataFormat, camelContext, "typeFilterDefinitions", typeFilterDefinitions);
        }

        setPropertyRef(dataFormat, camelContext, "constructor", constructor);
        setPropertyRef(dataFormat, camelContext, "representer", representer);
        setPropertyRef(dataFormat, camelContext, "dumperOptions", dumperOptions);
        setPropertyRef(dataFormat, camelContext, "resolver", resolver);
    }

    protected void setProperty(DataFormat dataFormat, CamelContext camelContext, String propertyName, Object propertyValue) {
        if (ObjectHelper.isNotEmpty(propertyValue)) {
            setProperty(camelContext, dataFormat, propertyName, propertyValue);
        }
    }

    protected void setPropertyRef(DataFormat dataFormat, CamelContext camelContext, String propertyName, String propertyValue) {
        if (ObjectHelper.isNotEmpty(propertyValue)) {
            // must be a reference value
            String ref = propertyValue.startsWith("#") ? propertyValue : "#" + propertyValue;
            setProperty(camelContext, dataFormat, propertyName, ref);
        }
    }

}
