package com.buttongames.butterflyserver.http.handlers;

import com.buttongames.butterflycore.encryption.Rc4;
import com.buttongames.butterflycore.xml.XmlUtils;
import com.buttongames.butterflycore.xml.kbinxml.PublicKt;
import com.google.common.net.MediaType;
import com.jamesmurty.utils.BaseXMLBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Element;
import spark.Request;
import spark.Response;
import spark.utils.StringUtils;

import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import static com.buttongames.butterflycore.util.Constants.COMPRESSION_HEADER;
import static com.buttongames.butterflycore.util.Constants.CRYPT_KEY_HEADER;

/**
 * Base request handler that the others inherit from.
 * @author skogaby ([email protected])
 */
public abstract class BaseRequestHandler {

    private final Logger LOG = LogManager.getLogger(BaseRequestHandler.class);

    /**
     * Handles an incoming request for the given module.
     * @param requestBody The XML document of the incoming request.
     * @param request The Spark request
     * @param response The Spark response
     * @return A response object for Spark
     */
    public abstract Object handleRequest(final Element requestBody, final Request request, final Response response);

    /**
     * Sends the response to the client.
     * @param request The original request.
     * @param response The response object we can use to send the data.
     * @param respBody The XML document of the response.
     * @return A response object for Spark
     */
    protected Object sendResponse(final Request request, final Response response, final BaseXMLBuilder respBody) {
        // get the bytes of the XML document
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();

        try {
            final TransformerFactory transformerFactory = TransformerFactory.newInstance();
            final Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");

            final DOMSource source = new DOMSource(respBody.getDocument());
            final StreamResult result = new StreamResult(bos);
            transformer.transform(source, result);
        } catch (TransformerException e) {
            e.printStackTrace();
            return 500;
        }

        byte[] respBytes = bos.toByteArray();
        return this.sendBytesToClient(respBytes, request, response);
    }

    /**
     * Takes the raw, unencrypted and decompressed bytes, transforms them as necessary, and sends
     * them to the client.
     * @param respBytes The bytes to send.
     * @param request The original request.
     * @param response The response object we can use to send the data.
     * @return A response object for Spark
     */
    protected Object sendBytesToClient(byte[] respBytes, final Request request, final Response response) {
        response.header("Connection", "keep-alive");

        // convert them to binary XML
        if (!XmlUtils.isBinaryXML(respBytes)) {
            respBytes = PublicKt.kbinEncode(new String(respBytes), "UTF-8");
        }

        response.header(COMPRESSION_HEADER, "none");

        // encrypt if needed
        final String encryptionKey = request.headers(CRYPT_KEY_HEADER);

        if (!StringUtils.isBlank(encryptionKey)) {
            respBytes = Rc4.encrypt(respBytes, encryptionKey);
            response.header(CRYPT_KEY_HEADER, encryptionKey);
        }

        // send to the client
        try {
            final HttpServletResponse rawResponse = response.raw();
            response.type(MediaType.OCTET_STREAM.toString());

            rawResponse.getOutputStream().write(respBytes);
            rawResponse.getOutputStream().flush();
            rawResponse.getOutputStream().close();

            LOG.info("Response sent: " + request.queryParams("model") + " (" +
                    request.queryParams("module") + "." + request.queryParams("method") + ")");

            return 200;
        } catch (IOException e) {
            e.printStackTrace();
            return 500;
        }
    }
}