package org.xbib.elasticsearch.rest.xml;

import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.http.HttpChannel;
import org.elasticsearch.http.HttpRequest;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestFilter;
import org.elasticsearch.rest.RestFilterChain;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;

import org.xbib.elasticsearch.common.xcontent.XmlXContentBuilder;
import org.xbib.elasticsearch.common.xcontent.XmlXContentFactory;
import org.xbib.elasticsearch.common.xcontent.XmlXContentType;
import org.xbib.elasticsearch.common.xcontent.xml.XmlXParams;

import java.util.Map;

/**
 * XML filter for Elasticsearch REST requests and responses.
 *
 * To receive XML responses, the request header Accept: must contain "application/xml"
 */
public class XmlFilter extends RestFilter {

    private final ESLogger logger = ESLoggerFactory.getLogger(XmlFilter.class.getName());

    private final XmlXParams params;

    public XmlFilter() {
        this.params = new XmlXParams();
    }

    @Override
    public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) {
        filterChain.continueProcessing(new XmlRequest(request), new XmlChannel(request, channel));
    }

    private boolean isXml(RestRequest request) {
        return "application/xml".equals(request.header("Accept"))
                || request.hasParam("xml");
    }

    /**
     * Unwraps an XML REST request to JSON if Content-type: header declares application/xml.
     * We must extend HttpRequest because this will get used in a casting in the HTTP controller.
     */
    class XmlRequest extends HttpRequest {

        private RestRequest request;

        XmlRequest(RestRequest request) {
            this.request = request;
        }

        @Override
        public Method method() {
            return request.method();
        }

        @Override
        public String uri() {
            return request.uri();
        }

        @Override
        public String rawPath() {
            return request.rawPath();
        }

        @Override
        public boolean hasContent() {
            return request.hasContent();
        }

        @Override
        public BytesReference content() {
            if (isXml(request)) {
                XContentParser parser = null;
                try {
                    BytesReference b = request.content();
                    parser = XmlXContentFactory.xContent(XmlXContentType.XML).createParser(b);
                    parser.nextToken();
                    XContentBuilder builder = XContentFactory.jsonBuilder();
                    builder.copyCurrentStructure(parser);
                    return builder.bytes();
                } catch (Throwable e) {
                    logger.error(e.getMessage(), e);
                } finally {
                    if (parser != null) {
                        parser.close();
                    }
                }
            }
            return request.content();
        }

        @Override
        public String header(String name) {
            return request.header(name);
        }

        @Override
        public Iterable<Map.Entry<String, String>> headers() {
            return request.headers();
        }

        @Override
        public boolean hasParam(String key) {
            return false;
        }

        @Override
        public String param(String key) {
            return request.param(key);
        }

        @Override
        public String param(String key, String defaultValue) {
            return request.param(key, defaultValue);
        }

        @Override
        public Map<String, String> params() {
            return request.params();
        }
    }

    /**
     * Wraps a REST channel response into XML if Accept: header declares application/xml.
     * This must extend HttpChannel because this will get used in a casting in the HTTP controller.
     */
    class XmlChannel extends HttpChannel {

        private final RestChannel channel;

        XmlChannel(RestRequest request, RestChannel channel) {
            super(request, true);
            this.channel = channel;
        }

        @Override
        public void sendResponse(RestResponse response) {
            if (!response.status().equals(RestStatus.OK)) {
                channel.sendResponse(response);
            }
            if (isXml(request)) {
                XContentParser parser = null;
                try {
                    String string = response.content().toUtf8(); // takes some space ... :(
                    XContentType xContentType = XContentFactory.xContentType(string);
                    parser = XContentFactory.xContent(xContentType).createParser(string);
                    parser.nextToken();
                    XmlXContentBuilder builder = XmlXContentFactory.xmlBuilder(params);
                    if (request.paramAsBoolean("pretty", false)) {
                        builder.prettyPrint();
                    }
                    builder.copyCurrentStructure(parser);
                    BytesRestResponse restResponse = new BytesRestResponse(RestStatus.OK, "text/xml; charset=UTF-8", builder.bytes());
                    channel.sendResponse(restResponse);
                    return;
                } catch (Throwable e) {
                    logger.error(e.getMessage(), e);
                    channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, e.getMessage()));
                    return;
                } finally {
                    if (parser != null) {
                        parser.close();
                    }
                }
            }
            channel.sendResponse(response);
        }
    }

}