package be.iminds.ilabt.jfed.util;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

/**
 * A few helper functions for dates
 */
public class RFC3339Util {
    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(RFC3339Util.class);

    /**
     * Convert a java date object to a String representing the data in RFC 3339 format.
     *
     * @param date a java Date
     * @return a string representing the given date in RFC 3339 format
     * @see <a href="https://tools.ietf.org/html/rfc3339">RFC 3339</a>
     */
    public static String dateToRFC3339String(Date date) {
        return dateToRFC3339String(date, false, true, true);
    }

    public static String dateToRFC3339String(Date date, boolean forceZuluHack) {
        return dateToRFC3339String(date, forceZuluHack, true, true);
    }

    /**
     * Convert a java date object to a String representing the data in RFC 3339 format.
     *
     * @param date              a java Date
     * @param discardSubSeconds don't include sub second precision in the generated data string.
     * @param useZinsteadOfZero Use "Z" (allowed abbreviation for Zulu/UTC timezone) instead of +00:00. If false, use +00:00. Only has effect if timezone is actually Zulu timezone (or if automatically converted to it using forceZuluHack).
     * @param forceZuluHack     convert the timezone of the date to be in the UTC timezone ("zulu", "+00:00"). Note that the represented time does not change (eg 12:00+01:00 becomes 11:00Z).
     * @return a string representing the given date in RFC 3339 format
     * @see <a href="https://tools.ietf.org/html/rfc3339">RFC 3339</a>
     */
    public static String dateToRFC3339String(Date date, boolean forceZuluHack, boolean discardSubSeconds, boolean useZinsteadOfZero) {
        SimpleDateFormat dateFormat;
        if (date.getTime() % 1000 == 0 || discardSubSeconds)
            dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
        else
            dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

        if (forceZuluHack)
            dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));

        String res = dateFormat.format(date).replaceFirst("(\\d\\d)(\\d\\d)$", "$1:$2");

        //prefer Z instead of +00:00
        if (useZinsteadOfZero && res.endsWith("+00:00")) {
            res = res.substring(0, res.length() - "+00:00".length()) + "Z";
        }
        return res;
    }

    /**
     * Convert a String with a date in RFC 3339 format, to a java Date object
     * <p/>
     * Note: RFC3339 specifies some allowed variations:
     * - T and Z may be lowercase t and z
     * - T may be replaced by a space
     *
     * @param rfc3339date a String with a date in RFC 3339 format
     * @return the rfc3339date input, converted to a java Date object
     * @see <a href="https://tools.ietf.org/html/rfc3339">RFC 3339</a>
     */
    public static Date rfc3339StringToDate(String rfc3339date) throws ParseException {
        String parsableDateString = rfc3339date.trim().
                replace('t', 'T').
                replace('z', 'Z').
                replace(' ', 'T').
                replaceFirst("Z$", "+00:00"). //actually, normally SimpleDateFormat understands Z  (but this makes next expressions simpler)
                replaceFirst("(\\d\\d):(\\d\\d)$", "$1$2").
//                replaceFirst("(\\d\\d)\\.\\d\\d([+-]\\d\\d\\d\\d)$","$1$2"); //remove the sub second part of a RFC3339 date!
        replaceFirst("(\\d\\d)\\.(\\d\\d)([+-]\\d\\d\\d\\d)$", "$1.$20$3"). //make seconds fraction be milliseconds
                replaceFirst("(\\d\\d)\\.(\\d)([+-]\\d\\d\\d\\d)$", "$1.$200$3"); //make seconds fraction be milliseconds

        LOG.trace("rfc3339StringToDate(\"" + rfc3339date + "\") -> parsableDateString=" + parsableDateString);

        Date res;
        if (parsableDateString.matches(".*\\d\\d\\.\\d\\d\\d[+-]\\d\\d\\d\\d$"))
            res = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").parse(parsableDateString);
        else
            res = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(parsableDateString);

        LOG.trace("rfc3339StringToDate('{}') -> {}", rfc3339date, res);
        return res;
    }

    /*
     * ISO8601 allows a lot more than RFC3339.
     *
     * if no timezone is specified, it will assume local timezone!
     * */
    public static Date iso8601StringToDate(String iso6801Date) throws ParseException {
        //JAXB doesn't handle lowercase t or z or space instead of T
        String parsableDateString = iso6801Date.trim().
                replace('t', 'T').
                replace(' ', 'T').
                replace('z', 'Z');

        //JAXB know how to parse ISO8601 :-)
        Date res;
        try {
            res = javax.xml.bind.DatatypeConverter.parseDateTime(parsableDateString).getTime();
        } catch (IllegalArgumentException e) {
            throw new ParseException(e.getMessage(), 0); //dirty but it works
        }
        LOG.trace("iso8601ToDate(\"" + iso6801Date + "\") -> " + res);
        return res;
    }

    public static class TimeStampJsonDeserializer extends JsonDeserializer<Timestamp> {
        @Override
        public Timestamp deserialize(JsonParser jsonparser,
                                     DeserializationContext deserializationcontext) throws IOException {
            String date = jsonparser.getText();
            try {
                Date resDate = RFC3339Util.rfc3339StringToDate(date);
                return new Timestamp(resDate.getTime());
            } catch (ParseException e) {
                throw new RuntimeException(e);
            }

        }
    }

    public static class TimeStampJsonSerializer extends JsonSerializer<Timestamp> {
        @Override
        public void serialize(Timestamp date, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
            String dateString = RFC3339Util.dateToRFC3339String(date, true/*forceZulu*/);
            jsonGenerator.writeString(dateString);
        }
    }

    public static class DateJsonDeserializer extends JsonDeserializer<Date> {
        @Override
        public Date deserialize(JsonParser jsonparser,
                                DeserializationContext deserializationcontext) throws IOException {
            String date = jsonparser.getText();
            try {
                Date resDate = RFC3339Util.rfc3339StringToDate(date);
                return resDate;
            } catch (ParseException e) {
                throw new RuntimeException(e);
            }

        }
    }

    public static class DateJsonSerializer extends JsonSerializer<Date> {
        @Override
        public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
            String dateString = RFC3339Util.dateToRFC3339String(date, true/*forceZulu*/);
            jsonGenerator.writeString(dateString);
        }
    }
}
