package eu.dnetlib.espas.api.csw;

import eu.dnetlib.espas.api.exception.OwsException;
import eu.dnetlib.espas.api.exception.OwsExceptionCode;
import eu.dnetlib.espas.api.exception.Tuple;
import org.apache.log4j.Logger;
import org.springframework.web.HttpRequestHandler;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CSWController implements HttpRequestHandler {

    private static Logger logger = Logger.getLogger(CSWController.class);
    private CSWService cswService = null;

    @Override
    public void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Map<String, String> parameters = this.getParameters(req);
            String request = parameters.get("request");

            validateRequest(parameters);

            if (request != null) {
                resp.setContentType("application/xml");
                resp.setCharacterEncoding("UTF-8");

                if (request.equals("GetRecords")) {

                    this.getRecords(resp.getWriter(),
                            parameters.get("constraintlanguage"),
                            parameters.get("constraint"),
                            parameters.get("startposition"),
                            parameters.get("maxrecords"),
                            parameters.get("requestid"));

                } else if (request.equals("GetRecordById")) {

                    this.getRecord(resp.getWriter(),
                            parameters.get("id"));

                } else if (request.equals("DescribeRecord"))

                    throw new OwsException("Method " + request + " not yet implemented", OwsExceptionCode.OPERATION_NOT_SUPPORTED);

                else if (request.equals("GetCapabilities"))

                    throw new OwsException("Method " + request + " not yet implemented", OwsExceptionCode.OPERATION_NOT_SUPPORTED);

                else

                    throw new OwsException("Invalid request: " + request, OwsExceptionCode.OPERATION_NOT_SUPPORTED);

            } else {
                logger.error(OwsExceptionCode.NO_APPLICABLE_CODE + ": Error getting request");
                throw new OwsException("Error getting request", OwsExceptionCode.NO_APPLICABLE_CODE);
            }
        } catch (OwsException e) {
            resp.setStatus(e.getStatus());
            resp.getWriter().append(e.getMessage());
        }
    }

    private void getRecord(PrintWriter pw, String id) {
        cswService.getRecord(pw, id);
    }

    private void getRecords(PrintWriter pw, String constraintLanguage, String constraint, String startPosition, String maxRecords, String requestId) throws OwsException {
        cswService.getRecords(pw, constraintLanguage, constraint, Integer.parseInt(startPosition), Integer.parseInt(maxRecords), requestId);
    }

    private Map<String, String> getParameters(HttpServletRequest req) {
        Map<String, String> parameters = new HashMap<String, String>();

        if (req.getMethod().equalsIgnoreCase("post"))
            return readPostMessage(req);

        for (Object key : req.getParameterMap().keySet())
            parameters.put(key.toString().toLowerCase(), req.getParameter(key.toString()));

        if (parameters.get("startposition") == null)
            parameters.put("startposition", "1");
        if (parameters.get("maxrecords") == null)
            parameters.put("maxrecords", "10");
        if (parameters.get("requestid") == null)
            parameters.put("requestid", Long.toString((new Date()).getTime()));

        return parameters;
    }

    private Map<String, String> readPostMessage(HttpServletRequest request) {
        final HashMap<String, String> parameters = new HashMap<String, String>();
        try {

            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser saxParser = factory.newSAXParser();

            DefaultHandler handler = new DefaultHandler() {
                String value = "";
                StringBuilder constraintBuilder = new StringBuilder();
                boolean propertyIsLike = false;
                boolean propertyIsEqualTo = false;
                boolean andIsSet = false;
                boolean orIsSet = false;

                public void startElement(String uri, String localName, String qName,
                                         Attributes attributes) throws SAXException {
                    if (qName.toLowerCase().contains("record")) {

                        parameters.put("request", "GetRecords");
                        parameters.put("service", attributes.getValue("service"));
                        parameters.put("version", attributes.getValue("version"));
                        parameters.put("maxrecords", attributes.getValue("maxRecords"));
                        parameters.put("startposition", attributes.getValue("startPosition"));

                    } else if (qName.toLowerCase().contains("query")) {

                        parameters.put("typename", attributes.getValue("typeNames"));

                    } else if (qName.toLowerCase().contains("islike")) {

                        propertyIsLike = true;

                    } else if (qName.toLowerCase().contains("isequal")) {

                        propertyIsEqualTo = true;

                    } else if (qName.toLowerCase().contains("isbetween")) {

                        constraintBuilder.append("phenomenonTime between ");

                    } else if (qName.toLowerCase().contains("ogc:and")) {

                        andIsSet = true;

                    } else if (qName.toLowerCase().contains("ogc:or")) {

                        orIsSet = true;

                    }
                }

                public void endElement(String uri, String localName,
                                       String qName) throws SAXException {

                    if (qName.toLowerCase().contains("propertyname")) {

                        if (propertyIsEqualTo)
                            constraintBuilder.append(value).append("=");
                        else if (propertyIsLike)
                            constraintBuilder.append(value).append(" like ");

                    } else if (qName.toLowerCase().contains("literal")) {

                        if (propertyIsEqualTo)
                            constraintBuilder.append("\"").append(value).append("\"");
                        else if (propertyIsLike)
                            constraintBuilder.append("\'").append(value).append("\'");

                        if (andIsSet) constraintBuilder.append(" and ");
                        else if (orIsSet) constraintBuilder.append(" or ");

                    } else if (qName.toLowerCase().contains("record")) {

                        parameters.put("constraint", constraintBuilder.toString());
                        parameters.put("constraintlanguage", "CQLTEXT");
                    } else if (qName.toLowerCase().contains("islike")) {

                        propertyIsLike = false;

                    } else if (qName.toLowerCase().contains("isequal")) {

                        propertyIsEqualTo = false;

                    } else if (qName.toLowerCase().contains("lower")) {

                        constraintBuilder.append("\'").append(value).append("\'");

                        if (andIsSet) constraintBuilder.append(" and ");
                        else if (orIsSet) constraintBuilder.append(" or ");

                    } else if (qName.toLowerCase().contains("upper")) {

                        constraintBuilder.append("\'").append(value).append("\'");

                        if (andIsSet) constraintBuilder.append(" and ");
                        else if (orIsSet) constraintBuilder.append(" or ");
                    } else if (qName.toLowerCase().contains("ogc:and")) {
                        andIsSet = false;
                        Matcher m = Pattern.compile(" and $").matcher(constraintBuilder);
                        while(m.find()) {
                            constraintBuilder.replace(m.start(), m.end(), "");
                        }
                    } else if (qName.toLowerCase().contains("ogc:or")) {
                        orIsSet = false;
                        Matcher m = Pattern.compile(" or $").matcher(constraintBuilder);
                        while(m.find()) {
                            constraintBuilder.replace(m.start(), m.end(), "");
                        }
                    }
                }

                public void characters(char ch[], int start, int length) throws SAXException {
                    value = new String(ch, start, length);
                }
            };

            saxParser.parse(new InputSource(request.getInputStream()), handler);

        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


        return parameters;
    }

    private void validateRequest(Map<String, String> parameters) throws OwsException {
        Map<String, Tuple<String, OwsExceptionCode>> exceptions = new HashMap<String, Tuple<String, OwsExceptionCode>>();

        for (String key : parameters.keySet()) {
            String value = parameters.get(key);
            if (value == null)
                exceptions.put(key, new Tuple<String, OwsExceptionCode>("Parameter '" + key + "' is missing", OwsExceptionCode.MISSING_PARAMETER));
            else if (value.isEmpty())
                exceptions.put(key, new Tuple<String, OwsExceptionCode>("Parameter '" + key + "' is empty", OwsExceptionCode.INVALID_PARAMETER));
        }

        if (!exceptions.isEmpty())
            throw new OwsException(exceptions);
        else {
            logger.info("Valid parameters for CSW request:");
            for (String s : parameters.keySet())
                logger.info("[" + s + "]: " + parameters.get(s));
        }
    }

    public CSWService getCswService() {
        return cswService;
    }

    public void setCswService(CSWService cswService) {
        this.cswService = cswService;
    }
}
