/**
 * 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.component.rest;

import java.io.InputStream;
import java.util.Locale;

import org.apache.camel.AsyncCallback;
import org.apache.camel.AsyncProcessor;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Exchange;
import org.apache.camel.processor.DelegateAsyncProcessor;
import org.apache.camel.processor.MarshalProcessor;
import org.apache.camel.processor.UnmarshalProcessor;
import org.apache.camel.processor.binding.BindingException;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.util.ExchangeHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ServiceHelper;

/**
 * A {@link org.apache.camel.Processor} that binds the REST producer request and reply messages
 * from sources of json or xml to Java Objects.
 * <p/>
 * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform
 * from xml/json to Java Objects and reverse again.
 * <p/>
 * The rest-dsl consumer side is implemented in {@link org.apache.camel.processor.RestBindingAdvice}
 */
public class RestProducerBindingProcessor extends DelegateAsyncProcessor {

    private final CamelContext camelContext;
    private final AsyncProcessor jsonUnmarshal;
    private final AsyncProcessor xmlUnmarshal;
    private final AsyncProcessor jsonMarshal;
    private final AsyncProcessor xmlMarshal;
    private final String bindingMode;
    private final boolean skipBindingOnErrorCode;
    private final String outType;

    public RestProducerBindingProcessor(AsyncProcessor processor, CamelContext camelContext,
                                        DataFormat jsonDataFormat, DataFormat xmlDataFormat,
                                        DataFormat outJsonDataFormat, DataFormat outXmlDataFormat,
                                        String bindingMode, boolean skipBindingOnErrorCode,
                                        String outType) {

        super(processor);

        this.camelContext = camelContext;

        if (outJsonDataFormat != null) {
            this.jsonUnmarshal = new UnmarshalProcessor(outJsonDataFormat);
        } else {
            this.jsonUnmarshal = null;
        }
        if (jsonDataFormat != null) {
            this.jsonMarshal = new MarshalProcessor(jsonDataFormat);
        } else {
            this.jsonMarshal = null;
        }

        if (outXmlDataFormat != null) {
            this.xmlUnmarshal = new UnmarshalProcessor(outXmlDataFormat);
        } else {
            this.xmlUnmarshal = null;
        }
        if (xmlDataFormat != null) {
            this.xmlMarshal = new MarshalProcessor(xmlDataFormat);
        } else {
            this.xmlMarshal = null;
        }

        this.bindingMode = bindingMode;
        this.skipBindingOnErrorCode = skipBindingOnErrorCode;
        this.outType = outType;
    }

    @Override
    public String toString() {
        return "RestProducerBindingProcessor";
    }

    @Override
    protected void doStart() throws Exception {
        // inject CamelContext before starting
        if (jsonMarshal instanceof CamelContextAware) {
            ((CamelContextAware) jsonMarshal).setCamelContext(camelContext);
        }
        if (jsonUnmarshal instanceof CamelContextAware) {
            ((CamelContextAware) jsonUnmarshal).setCamelContext(camelContext);
        }
        if (xmlMarshal instanceof CamelContextAware) {
            ((CamelContextAware) xmlMarshal).setCamelContext(camelContext);
        }
        if (xmlUnmarshal instanceof CamelContextAware) {
            ((CamelContextAware) xmlUnmarshal).setCamelContext(camelContext);
        }
        ServiceHelper.startServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
    }

    @Override
    protected void doStop() throws Exception {
        ServiceHelper.stopServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
    }

    @Override
    public boolean process(Exchange exchange, AsyncCallback callback) {
        boolean isXml = false;
        boolean isJson = false;

        // skip before binding for empty/null body
        Object body = exchange.getIn().getBody();
        if (ObjectHelper.isEmpty(body)) {
            if (outType != null) {
                // wrap callback to add reverse operation if we know the output type from the REST service
                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
            }
            // okay now we can continue routing to the producer
            return getProcessor().process(exchange, callback);
        }

        // we only need to perform before binding if the message body is POJO based
        if (body instanceof String || body instanceof byte[]) {
            // the body is text based and thus not POJO so no binding needed
            if (outType != null) {
                // wrap callback to add reverse operation if we know the output type from the REST service
                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
            }
            // okay now we can continue routing to the producer
            return getProcessor().process(exchange, callback);
        } else {
            // if its convertable to stream based then its not POJO based
            InputStream is = camelContext.getTypeConverter().tryConvertTo(InputStream.class, exchange, body);
            if (is != null) {
                exchange.getIn().setBody(is);
                if (outType != null) {
                    // wrap callback to add reverse operation if we know the output type from the REST service
                    callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
                }
                // okay now we can continue routing to the producer
                return getProcessor().process(exchange, callback);
            }
        }

        // assume body is POJO based and binding needed

        String contentType = ExchangeHelper.getContentType(exchange);
        if (contentType != null) {
            isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
            isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
        }

        // only allow xml/json if the binding mode allows that
        isXml &= bindingMode.equals("auto") || bindingMode.contains("xml");
        isJson &= bindingMode.equals("auto") || bindingMode.contains("json");

        // if we do not yet know if its xml or json, then use the binding mode to know the mode
        if (!isJson && !isXml) {
            isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
            isJson = bindingMode.equals("auto") || bindingMode.contains("json");
        }

        // favor json over xml
        if (isJson && jsonMarshal != null) {
            try {
                jsonMarshal.process(exchange);
            } catch (Exception e) {
                // we failed so cannot call producer
                exchange.setException(e);
                callback.done(true);
                return true;
            }
            // need to prepare exchange first
            ExchangeHelper.prepareOutToIn(exchange);
            if (outType != null) {
                // wrap callback to add reverse operation if we know the output type from the REST service
                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
            }
            // okay now we can continue routing to the producer
            return getProcessor().process(exchange, callback);
        } else if (isXml && xmlMarshal != null) {
            try {
                xmlMarshal.process(exchange);
            } catch (Exception e) {
                // we failed so cannot call producer
                exchange.setException(e);
                callback.done(true);
                return true;
            }
            // need to prepare exchange first
            ExchangeHelper.prepareOutToIn(exchange);
            if (outType != null) {
                // wrap callback to add reverse operation if we know the output type from the REST service
                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, true);
            }
            // okay now we can continue routing to the producer
            return getProcessor().process(exchange, callback);
        }

        // we could not bind
        if ("off".equals(bindingMode) || bindingMode.equals("auto")) {
            if (outType != null) {
                // wrap callback to add reverse operation if we know the output type from the REST service
                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
            }
            // okay now we can continue routing to the producer
            return getProcessor().process(exchange, callback);
        } else {
            if (bindingMode.contains("xml")) {
                exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange));
            } else {
                exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange));
            }
            // we failed so cannot call producer
            callback.done(true);
            return true;
        }
    }

    private final class RestProducerBindingUnmarshalCallback implements AsyncCallback {

        private final Exchange exchange;
        private final AsyncCallback callback;
        private final AsyncProcessor jsonMarshal;
        private final AsyncProcessor xmlMarshal;
        private boolean wasXml;

        private RestProducerBindingUnmarshalCallback(Exchange exchange, AsyncCallback callback,
                                                     AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml) {
            this.exchange = exchange;
            this.callback = callback;
            this.jsonMarshal = jsonMarshal;
            this.xmlMarshal = xmlMarshal;
            this.wasXml = wasXml;
        }

        @Override
        public void done(boolean doneSync) {
            try {
                doDone();
            } catch (Throwable e) {
                exchange.setException(e);
            } finally {
                // ensure callback is called
                callback.done(doneSync);
            }
        }

        private void doDone() {
            // only unmarshal if there was no exception
            if (exchange.getException() != null) {
                return;
            }

            if (skipBindingOnErrorCode) {
                Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class);
                // if there is a custom http error code then skip binding
                if (code != null && code >= 300) {
                    return;
                }
            }

            boolean isXml = false;
            boolean isJson = false;

            // check the content-type if its json or xml
            String contentType = ExchangeHelper.getContentType(exchange);
            if (contentType != null) {
                isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
                isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
            }

            // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json)
            if (bindingMode != null) {
                isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml");
                isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json");

                // if we do not yet know if its xml or json, then use the binding mode to know the mode
                if (!isJson && !isXml) {
                    isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
                    isJson = bindingMode.equals("auto") || bindingMode.contains("json");
                }
            }

            // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller
            if (isXml && isJson) {
                isXml = wasXml;
                isJson = !wasXml;
            }

            // need to prepare exchange first
            ExchangeHelper.prepareOutToIn(exchange);

            // ensure there is a content type header (even if binding is off)
            ensureHeaderContentType(isXml, isJson, exchange);

            if (bindingMode == null || "off".equals(bindingMode)) {
                // binding is off, so no message body binding
                return;
            }

            // is there any unmarshaller at all
            if (jsonUnmarshal == null && xmlUnmarshal == null) {
                return;
            }

            // is the body empty
            if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) {
                return;
            }

            contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class);
            // need to lower-case so the contains check below can match if using upper case
            contentType = contentType.toLowerCase(Locale.US);
            try {
                // favor json over xml
                if (isJson && jsonUnmarshal != null) {
                    // only marshal if its json content type
                    if (contentType.contains("json")) {
                        jsonUnmarshal.process(exchange);
                    }
                } else if (isXml && xmlUnmarshal != null) {
                    // only marshal if its xml content type
                    if (contentType.contains("xml")) {
                        xmlUnmarshal.process(exchange);
                    }
                } else {
                    // we could not bind
                    if (bindingMode.equals("auto")) {
                        // okay for auto we do not mind if we could not bind
                    } else {
                        if (bindingMode.contains("xml")) {
                            exchange.setException(new BindingException("Cannot bind from xml as message body is not xml compatible", exchange));
                        } else {
                            exchange.setException(new BindingException("Cannot bind from json as message body is not json compatible", exchange));
                        }
                    }
                }
            } catch (Throwable e) {
                exchange.setException(e);
            }
        }

        private void ensureHeaderContentType(boolean isXml, boolean isJson, Exchange exchange) {
            // favor json over xml
            if (isJson) {
                // make sure there is a content-type with json
                String type = ExchangeHelper.getContentType(exchange);
                if (type == null) {
                    exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json");
                }
            } else if (isXml) {
                // make sure there is a content-type with xml
                String type = ExchangeHelper.getContentType(exchange);
                if (type == null) {
                    exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml");
                }
            }
        }

        @Override
        public String toString() {
            return "RestProducerBindingUnmarshalCallback";
        }
    }

}
