/*
 * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
 * Copyright (C) 2012  Mickael Guessant
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package davmail.exchange.dav;

import davmail.exchange.XMLStreamUtil;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpConnection;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.xml.Namespace;
import org.apache.log4j.Logger;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * New stax based implementation to replace DOM based jackrabbit version an support Exchange only extensions.
 */
public abstract class ExchangeDavMethod extends PostMethod {
    protected static final Logger LOGGER = Logger.getLogger(ExchangeDavMethod.class);
    List<MultiStatusResponse> responses;

    /**
     * Create PROPPATCH method.
     *
     * @param path           path
     */
    public ExchangeDavMethod(String path) {
        super(path);
        setRequestEntity(new RequestEntity() {
            byte[] content;

            public boolean isRepeatable() {
                return true;
            }

            public void writeRequest(OutputStream outputStream) throws IOException {
                if (content == null) {
                    content = generateRequestContent();
                }
                outputStream.write(content);
            }

            public long getContentLength() {
                if (content == null) {
                    content = generateRequestContent();
                }
                return content.length;
            }

            public String getContentType() {
                return "text/xml;charset=UTF-8";
            }
        });
    }

    /**
     * Generate request content from property values.
     *
     * @return request content as byte array
     */
    protected abstract byte[] generateRequestContent();

    @Override
    protected void processResponseBody(HttpState httpState, HttpConnection httpConnection) {
        Header contentTypeHeader = getResponseHeader("Content-Type");
        if (contentTypeHeader != null && "text/xml".equals(contentTypeHeader.getValue())) {
            responses = new ArrayList<MultiStatusResponse>();
            XMLStreamReader reader;
            try {
                reader = XMLStreamUtil.createXMLStreamReader(new FilterInputStream(getResponseBodyAsStream()) {
                    final byte[] lastbytes = new byte[3];

                    @Override
                    public int read(byte[] bytes, int off, int len) throws IOException {
                        int count = in.read(bytes, off, len);
                        // patch invalid element name
                        for (int i = 0; i < count; i++) {
                            byte currentByte = bytes[off + i];
                            if ((lastbytes[0] == '<') && (currentByte >= '0' && currentByte <= '9')) {
                                // move invalid first tag char to valid range
                                bytes[off + i] = (byte) (currentByte + 49);
                            }
                            lastbytes[0] = lastbytes[1];
                            lastbytes[1] = lastbytes[2];
                            lastbytes[2] = currentByte;
                        }
                        return count;
                    }

                });
                while (reader.hasNext()) {
                    reader.next();
                    if (XMLStreamUtil.isStartTag(reader, "response")) {
                        handleResponse(reader);
                    }
                }

            } catch (IOException e) {
                LOGGER.error("Error while parsing soap response: " + e, e);
            } catch (XMLStreamException e) {
                LOGGER.error("Error while parsing soap response: " + e, e);
            }
        }
    }

    protected void handleResponse(XMLStreamReader reader) throws XMLStreamException {
        MultiStatusResponse multiStatusResponse = null;
        String href = null;
        String responseStatus = "";
        while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "response")) {
            reader.next();
            if (XMLStreamUtil.isStartTag(reader)) {
                String tagLocalName = reader.getLocalName();
                if ("href".equals(tagLocalName)) {
                    href = reader.getElementText();
                } else if ("status".equals(tagLocalName)) {
                    responseStatus = reader.getElementText();
                } else if ("propstat".equals(tagLocalName)) {
                     if (multiStatusResponse == null) {
                         multiStatusResponse = new MultiStatusResponse(href, responseStatus);
                     }
                     handlePropstat(reader, multiStatusResponse);
                }
            }
        }
        if (multiStatusResponse != null) {
            responses.add(multiStatusResponse);
        }
    }

    protected void handlePropstat(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
        int propstatStatus = 0;
        while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "propstat")) {
            reader.next();
            if (XMLStreamUtil.isStartTag(reader)) {
                String tagLocalName = reader.getLocalName();
                if ("status".equals(tagLocalName)) {
                    if ("HTTP/1.1 200 OK".equals(reader.getElementText())) {
                        propstatStatus = HttpStatus.SC_OK;
                    } else {
                        propstatStatus = 0;
                    }
                } else if ("prop".equals(tagLocalName) && propstatStatus == HttpStatus.SC_OK) {
                    handleProperty(reader, multiStatusResponse);
                }
            }
        }

    }

    protected void handleProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
        while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, "prop")) {
            reader.next();
            if (XMLStreamUtil.isStartTag(reader)) {
                Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI());
                String tagLocalName = reader.getLocalName();
                if (reader.getAttributeCount() > 0 && "mv.string".equals(reader.getAttributeValue(0))) {
                     handleMultiValuedProperty(reader, multiStatusResponse);
                } else {
                    String tagContent = getTagContent(reader);
                    if (tagContent != null) {
                        multiStatusResponse.add(new DefaultDavProperty<String>(tagLocalName, tagContent, namespace));
                    }
                }
            }
        }
    }

    protected void handleMultiValuedProperty(XMLStreamReader reader, MultiStatusResponse multiStatusResponse) throws XMLStreamException {
        String tagLocalName = reader.getLocalName();
        Namespace namespace = Namespace.getNamespace(reader.getNamespaceURI());
        ArrayList<String> values = new ArrayList<String>();
        while (reader.hasNext() && !XMLStreamUtil.isEndTag(reader, tagLocalName)) {
            reader.next();
            if (XMLStreamUtil.isStartTag(reader)) {
                String tagContent = getTagContent(reader);
                if (tagContent != null) {
                    values.add(tagContent);
                }
            }
        }
        multiStatusResponse.add(new DefaultDavProperty<ArrayList<String>>(tagLocalName, values, namespace));
    }

    protected String getTagContent(XMLStreamReader reader) throws XMLStreamException {
        String value = null;
        String tagLocalName = reader.getLocalName();
        while (reader.hasNext() &&
                !((reader.getEventType() == XMLStreamConstants.END_ELEMENT) && tagLocalName.equals(reader.getLocalName()))) {
            reader.next();
            if (reader.getEventType() == XMLStreamConstants.CHARACTERS) {
                value = reader.getText();
            }
        }
        // empty tag
        if (!reader.hasNext()) {
            throw new XMLStreamException("End element for " + tagLocalName + " not found");
        }
        return value;
    }

    /**
     * Get Multistatus responses.
     *
     * @return responses
     * @throws HttpException on error
     */
    public MultiStatusResponse[] getResponses() throws HttpException {
        if (responses == null) {
            throw new HttpException(getStatusLine().toString());
        }
        return responses.toArray(new MultiStatusResponse[0]);
    }

    /**
     * Get single Multistatus response.
     *
     * @return response
     * @throws HttpException on error
     */
    public MultiStatusResponse getResponse() throws HttpException {
        if (responses == null || responses.size() != 1) {
            throw new HttpException(getStatusLine().toString());
        }
        return responses.get(0);
    }

    /**
     * Return method http status code.
     *
     * @return http status code
     * @throws HttpException on error
     */
    public int getResponseStatusCode() throws HttpException {
        String responseDescription = getResponse().getResponseDescription();
        if ("HTTP/1.1 201 Created".equals(responseDescription)) {
            return HttpStatus.SC_CREATED;
        } else {
            return HttpStatus.SC_OK;
        }
    }
}
