package com.thegrizzlylabs.sardineandroid.util;

import com.thegrizzlylabs.sardineandroid.model.Prop;
import com.thegrizzlylabs.sardineandroid.model.Property;
import com.thegrizzlylabs.sardineandroid.model.Resourcetype;

import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.convert.Registry;
import org.simpleframework.xml.convert.RegistryStrategy;
import org.simpleframework.xml.core.Persister;
import org.simpleframework.xml.strategy.Strategy;
import org.simpleframework.xml.stream.Format;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * Basic utility code. I borrowed some code from the webdavlib for
 * parsing dates.
 *
 * @author jonstevens
 */
public final class SardineUtil {
    private SardineUtil() {
    }

    private final static String[] SUPPORTED_DATE_FORMATS = new String[]{
            "yyyy-MM-dd'T'HH:mm:ss'Z'",
            "EEE, dd MMM yyyy HH:mm:ss zzz",
            "yyyy-MM-dd'T'HH:mm:ss.sss'Z'",
            "yyyy-MM-dd'T'HH:mm:ssZ",
            "EEE MMM dd HH:mm:ss zzz yyyy",
            "EEEEEE, dd-MMM-yy HH:mm:ss zzz",
            "EEE MMMM d HH:mm:ss yyyy"};

    /**
     * Default namespace prefix
     */
    public static final String CUSTOM_NAMESPACE_PREFIX = "s";

    /**
     * Default namespace URI
     */
    public static final String CUSTOM_NAMESPACE_URI = "SAR:";

    /**
     * Default namespace prefix
     */
    public static final String DEFAULT_NAMESPACE_PREFIX = "D";

    /**
     * Default namespace URI
     */
    public static final String DEFAULT_NAMESPACE_URI = "DAV:";

    private static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

    /**
     * Date formats using for Date parsing.
     */
    private static final List<ThreadLocal<SimpleDateFormat>> DATETIME_FORMATS;

    static {
        List<ThreadLocal<SimpleDateFormat>> l = new ArrayList<>(SUPPORTED_DATE_FORMATS.length);
        for (int i = 0; i < SUPPORTED_DATE_FORMATS.length; i++) {
            l.add(new ThreadLocal<SimpleDateFormat>());
        }
        DATETIME_FORMATS = Collections.unmodifiableList(l);
    }

    /**
     * Loops over all the possible date formats and tries to find the right one.
     *
     * @param value ISO date string
     * @return Null if there is a parsing failure
     */
    public static Date parseDate(String value) {
        if (value == null) {
            return null;
        }
        Date date = null;
        for (int i = 0; i < DATETIME_FORMATS.size(); i++) {
            ThreadLocal<SimpleDateFormat> format = DATETIME_FORMATS.get(i);
            SimpleDateFormat sdf = format.get();
            if (sdf == null) {
                sdf = new SimpleDateFormat(SUPPORTED_DATE_FORMATS[i], Locale.US);
                sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
                format.set(sdf);
            }
            try {
                date = sdf.parse(value);
                break;
            } catch (ParseException e) {
                // We loop through this until we found a valid one.
            }
        }
        return date;
    }

    private static Serializer getSerializer() throws Exception {
        Format format = new Format("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        Registry registry = new Registry();
        Strategy strategy = new RegistryStrategy(registry);
        Serializer serializer = new Persister(strategy, format);

        registry.bind(Prop.class, new EntityWithAnyElementConverter<>(serializer, Prop.class));
        registry.bind(Resourcetype.class, new EntityWithAnyElementConverter<>(serializer, Resourcetype.class));
        registry.bind(Property.class, Property.PropertyConverter.class);

        return serializer;
    }

    @SuppressWarnings("unchecked")
    public static <T> T unmarshal(Class<? extends T> type, InputStream in) throws IOException {
        try {
            return getSerializer().read(type, in);
        } catch (SAXException e) {
            throw new RuntimeException(e.getMessage(), e);
        } catch (Exception e) {
            // Server does not return any valid WebDAV XML that matches our JAXB context
            throw new IOException("Not a valid DAV response", e);
        }
    }

    /**
     * @param jaxbElement An object from the model
     * @return The XML string for the WebDAV request
     * @throws RuntimeException When there is a JAXB error
     */
    public static String toXml(Object jaxbElement) {
        StringWriter writer = new StringWriter();
        try {
            getSerializer().write(jaxbElement, writer);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
        return writer.toString();
    }

    /**
     * @return New XML document from the default document builder factory.
     */
    private static Document createDocument() {
        DocumentBuilder builder;
        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
        return builder.newDocument();
    }

    /** */
    public static Map<QName, String> toQName(Map<String, String> setProps) {
        if (setProps == null) {
            return Collections.emptyMap();
        }
        Map<QName, String> result = new HashMap<>(setProps.size());
        for (Map.Entry<String, String> entry : setProps.entrySet()) {
            result.put(createQNameWithCustomNamespace(entry.getKey()), entry.getValue());
        }
        return result;
    }

    /** */
    public static List<QName> toQName(List<String> removeProps) {
        if (removeProps == null) {
            return Collections.emptyList();
        }
        List<QName> result = new ArrayList<>(removeProps.size());
        for (String entry : removeProps) {
            result.add(createQNameWithCustomNamespace(entry));
        }
        return result;
    }

    public static QName toQName(Element element) {
        String namespace = element.getNamespaceURI();
        if (namespace == null) {
            return new QName(SardineUtil.DEFAULT_NAMESPACE_URI,
                    element.getLocalName(),
                    SardineUtil.DEFAULT_NAMESPACE_PREFIX);
        } else if (element.getPrefix() == null) {
            return new QName(element.getNamespaceURI(),
                    element.getLocalName());
        } else {
            return new QName(element.getNamespaceURI(),
                    element.getLocalName(),
                    element.getPrefix());
        }

    }

    /**
     * @param key Local element name.
     */
    public static QName createQNameWithCustomNamespace(String key) {
        return new QName(CUSTOM_NAMESPACE_URI, key, CUSTOM_NAMESPACE_PREFIX);
    }

    /**
     * @param key Local element name.
     */
    public static QName createQNameWithDefaultNamespace(String key) {
        return new QName(DEFAULT_NAMESPACE_URI, key, DEFAULT_NAMESPACE_PREFIX);
    }

    /**
     * @param key Fully qualified element name.
     */
    public static Element createElement(QName key) {
        return createDocument().createElementNS(key.getNamespaceURI(), key.getPrefix() + ":" + key.getLocalPart());
    }

    /**
     * @param key Fully qualified element name.
     */
    public static Element createElement(Element parent, QName key) {
        return parent.getOwnerDocument().createElementNS(key.getNamespaceURI(), key.getPrefix() + ":" + key.getLocalPart());
    }
   
    /**
     *  @return standard UTF8 charset on any version of Android
     */
    public static Charset standardUTF8() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
            return StandardCharsets.UTF_8;
        } else {
            return Charset.forName("UTF-8");
        }
    }
}