/*
 * Copyright 2015-2018 OpenEstate.org.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openestate.io.trovit;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Currency;
import java.util.Date;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.DatatypeConverter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.openestate.io.core.XmlUtils;
import org.openestate.io.core.XmlValidationHandler;
import org.openestate.io.trovit.xml.ObjectFactory;
import org.openestate.io.trovit.xml.types.AreaUnitValue;
import org.openestate.io.trovit.xml.types.ForeclosureTypeValue;
import org.openestate.io.trovit.xml.types.OrientationValue;
import org.openestate.io.trovit.xml.types.PricePeriodValue;
import org.openestate.io.trovit.xml.types.TypeValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Some helper functions for the Trovit-XML format.
 *
 * @author Andreas Rudolph
 * @since 1.0
 */
@SuppressWarnings("WeakerAccess")
public class TrovitUtils {
    @SuppressWarnings("unused")
    private final static Logger LOGGER = LoggerFactory.getLogger(TrovitUtils.class);
    private final static Pattern ROOMS_INTERVAL = Pattern.compile("^(\\d+)\\-(\\d+)$");
    private static JAXBContext JAXB = null;

    /*
     * the latest implemented version of this format
     *
     * public final static String VERSION = "1.0";
     */

    /**
     * the XML target namespace of this format
     */
    @SuppressWarnings("unused")
    public final static String NAMESPACE = StringUtils.EMPTY;

    /**
     * the package, where generated JAXB classes are located
     */
    @SuppressWarnings("unused")
    public final static String PACKAGE = "org.openestate.io.trovit.xml";

    /**
     * the factory for creation of JAXB objects
     */
    @SuppressWarnings("unused")
    public final static ObjectFactory FACTORY = new ObjectFactory();

    private TrovitUtils() {
    }

    /**
     * Creates a {@link TrovitDocument} from an {@link InputStream}.
     *
     * @param input XML input
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static TrovitDocument createDocument(InputStream input) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(input, true));
    }

    /**
     * Creates a {@link TrovitDocument} from a {@link File}.
     *
     * @param xmlFile XML file
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static TrovitDocument createDocument(File xmlFile) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(xmlFile, true));
    }

    /**
     * Creates a {@link TrovitDocument} from a {@link String}.
     *
     * @param xmlString XML string
     * @return created document or null, of the document is not supported by this format
     * @throws SAXException                 if XML is invalid
     * @throws IOException                  if reading failed
     * @throws ParserConfigurationException if the parser is not properly configured
     */
    public static TrovitDocument createDocument(String xmlString) throws SAXException, IOException, ParserConfigurationException {
        return createDocument(XmlUtils.newDocument(xmlString, true));
    }

    /**
     * Creates a {@link TrovitDocument} from a {@link Document}.
     *
     * @param doc XML document
     * @return created document or null, of the document is not supported by this format
     */
    public static TrovitDocument createDocument(Document doc) {
        if (TrovitDocument.isReadable(doc))
            return new TrovitDocument(doc);
        else
            return null;
    }

    /**
     * Creates a {@link Marshaller} to write JAXB objects into XML.
     *
     * @return created marshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    @SuppressWarnings("unused")
    public static Marshaller createMarshaller() throws JAXBException {
        return createMarshaller(Charset.defaultCharset().name(), true);
    }

    /**
     * Creates a {@link Marshaller} to write JAXB objects into XML.
     *
     * @param encoding  encoding of written XML
     * @param formatted if written XML is pretty printed
     * @return created marshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    @SuppressWarnings("Duplicates")
    public static Marshaller createMarshaller(String encoding, boolean formatted) throws JAXBException {
        Marshaller m = getContext().createMarshaller();
        m.setProperty(Marshaller.JAXB_ENCODING, encoding);
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, formatted);
        m.setEventHandler(new XmlValidationHandler());
        return m;
    }

    /**
     * Creates a {@link Unmarshaller} to read JAXB objects from XML.
     *
     * @return created unmarshaller
     * @throws JAXBException if a problem with JAXB occurred
     */
    public static Unmarshaller createUnmarshaller() throws JAXBException {
        Unmarshaller m = getContext().createUnmarshaller();
        m.setEventHandler(new XmlValidationHandler());
        return m;
    }

    /**
     * Returns the {@link JAXBContext} for this format.
     *
     * @return context
     * @throws JAXBException if a problem with JAXB occurred
     */
    public synchronized static JAXBContext getContext() throws JAXBException {
        if (JAXB == null) initContext(Thread.currentThread().getContextClassLoader());
        return JAXB;
    }

    /**
     * Returns the {@link ObjectFactory} for this format.
     *
     * @return object factory
     */
    public synchronized static ObjectFactory getFactory() {
        return FACTORY;
    }

    /**
     * Initializes the {@link JAXBContext} for this format.
     *
     * @param classloader the classloader to load the generated JAXB classes with
     * @throws JAXBException if a problem with JAXB occurred
     */
    public synchronized static void initContext(ClassLoader classloader) throws JAXBException {
        JAXB = JAXBContext.newInstance(PACKAGE, classloader);
    }

    /**
     * Read a {@link AreaUnitValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static AreaUnitValue parseAreaUnitValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final AreaUnitValue unit = AreaUnitValue.fromXmlValue(value);
        if (unit != null) return unit;

        throw new IllegalArgumentException("Can't parse foreclosure type value '" + value + "'!");
    }

    /**
     * Read a {@link Boolean} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static Boolean parseBooleanValue(String value) {
        value = StringUtils.lowerCase(StringUtils.trimToEmpty(value), Locale.ENGLISH);
        switch (value) {
            case "true":
            case "yes":
            case "si":
            case "1":
                return Boolean.TRUE;

            case "false":
            case "no":
            case "0":
                return Boolean.FALSE;

            default:
                throw new IllegalArgumentException("Can't parse boolean value '" + value + "'!");
        }
    }

    /**
     * Read a {@link String} value from XML for description.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static String parseContentValue(String value) {
        return StringUtils.trimToNull(value);
    }

    /**
     * Read a {@link String} value from XML for a country code.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static String parseCountryValue(String value) {
        return StringUtils.trimToNull(value);
    }

    /**
     * Read a {@link Calendar} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static Calendar parseDateValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final String[] patterns = new String[]{
                "dd/MM/yyyy",
                "dd/MM/yyyy hh:mm:ss",
                "dd-MM-yyyy",
                "dd-MM-yyyy hh:mm:ss",
                "yyyy/MM/dd",
                "yyyy/MM/dd hh:mm:ss",
                "yyyy-MM-dd",
                "yyyy-MM-dd hh:mm:ss"
        };

        try {
            Date date = DateUtils.parseDateStrictly(value, Locale.ENGLISH, patterns);
            Calendar cal = Calendar.getInstance(Locale.getDefault());
            cal.setTime(date);
            return cal;
        } catch (ParseException ex) {
            throw new IllegalArgumentException("Can't parse date value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link BigInteger} value from XML for a floor area.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigInteger parseFloorAreaValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseInteger(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse floor area value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link ForeclosureTypeValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static ForeclosureTypeValue parseForeclosureTypeValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final ForeclosureTypeValue foreclosure = ForeclosureTypeValue.fromXmlValue(value);
        if (foreclosure != null) return foreclosure;

        throw new IllegalArgumentException("Can't parse foreclosure type value '" + value + "'!");
    }

    /**
     * Read a {@link BigDecimal} value from XML
     * with a valid latitude range.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parseLatitudeValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseDecimal(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse latitude value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link BigDecimal} value from XML
     * with a valid longitude range.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parseLongitudeValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseDecimal(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse longitude value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link OrientationValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static OrientationValue parseOrientationValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final OrientationValue orientation = OrientationValue.fromXmlValue(value);
        if (orientation != null) return orientation;

        throw new IllegalArgumentException("Can't parse orientation value '" + value + "'!");
    }

    /**
     * Read a {@link BigInteger} value from XML for a plot area.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigInteger parsePlotAreaValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseInteger(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse plot area value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link Currency} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static Currency parsePriceCurrencyValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        try {
            return Currency.getInstance(value.toUpperCase());
        } catch (IllegalArgumentException ex) {
            throw new IllegalArgumentException("Can't parse price currency value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link PricePeriodValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static PricePeriodValue parsePricePeriodValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final PricePeriodValue period = PricePeriodValue.fromXmlValue(value);
        if (period != null) return period;

        throw new IllegalArgumentException("Can't parse price period value '" + value + "'!");
    }

    /**
     * Read a {@link BigDecimal} value from XML for a price.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parsePriceValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;
        try {
            return DatatypeConverter.parseDecimal(value);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse price value '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link BigDecimal} value from XML for a number of rooms.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigDecimal parseRoomsValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final Matcher m = ROOMS_INTERVAL.matcher(value);
        if (m.find()) {
            final int from = Integer.parseInt(m.group(1));
            final int to = Integer.parseInt(m.group(2));
            if ((to - from) != -1) {
                throw new IllegalArgumentException("Can't parse rooms value '" + value + "' because of an invalid interval!");
            }
            return DatatypeConverter.parseDecimal(to + ".5");
        }

        try {
            return DatatypeConverter.parseDecimal(value);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse rooms value '" + value + "'!", ex);
        }
    }

    /**
     * Read an {@link URI} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static URI parseUriValue(String value) {
        value = StringUtils.trimToNull(value);

        if (value != null && !StringUtils.startsWithIgnoreCase(value, "http://") && !StringUtils.startsWithIgnoreCase(value, "https://"))
            value = "http://" + value;

        try {
            return (value != null) ? new URI(value) : null;
        } catch (URISyntaxException ex) {
            throw new IllegalArgumentException("Can't parse URI '" + value + "'!", ex);
        }
    }

    /**
     * Read a {@link TypeValue} value from XML.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static TypeValue parseTypeValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null) return null;

        final TypeValue type = TypeValue.fromXmlValue(value);
        if (type != null) return type;

        throw new IllegalArgumentException("Can't parse type value '" + value + "'!");
    }

    /**
     * Read a {@link BigInteger} value from XML for a year number.
     *
     * @param value XML string
     * @return parsed value or null, if the value is invalid
     */
    public static BigInteger parseYearValue(String value) {
        try {
            value = StringUtils.trimToNull(value);
            return (value != null) ? DatatypeConverter.parseInteger(value) : null;
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException("Can't parse year value '" + value + "'!", ex);
        }
    }

    /**
     * Write a {@link AreaUnitValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printAreaUnitValue(AreaUnitValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty area unit value!");

        return value.write();
    }

    /**
     * Write a {@link Boolean} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printBooleanValue(Boolean value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty boolean value!");

        return DatatypeConverter.printBoolean(value);
    }

    /**
     * Write a {@link String} value for a description into XML output.
     * <p>
     * The description must contain at least 30 characters.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printContentValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null)
            throw new IllegalArgumentException("Can't print empty content value!");
        if (value.length() < 30)
            throw new IllegalArgumentException("Can't print content value '" + value + "' because it is shorter than 30 characters!");

        return value;
    }

    /**
     * Write a {@link String} value for a country code into XML output.
     * <p>
     * The country has to be represendet by a ISO-Code wirh two or three
     * characters.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printCountryValue(String value) {
        value = StringUtils.trimToNull(value);
        if (value == null)
            throw new IllegalArgumentException("Can't print empty country value!");
        if (value.length() != 2 && value.length() != 3)
            throw new IllegalArgumentException("Can't print country value '" + value + "' because it is neither an ISO-2-Code nor an ISO-3-Code!");

        return StringUtils.upperCase(value, Locale.ENGLISH);
    }

    /**
     * Write a {@link Calendar} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printDateValue(Calendar value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty date value!");

        return new SimpleDateFormat("dd-MM-yyyy hh:mm:ss", Locale.ENGLISH)
                .format(value.getTime());
    }

    /**
     * Write a {@link BigInteger} value into XML output for a floor area.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printFloorAreaValue(BigInteger value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty floor area value!");
        if (value.compareTo(BigInteger.valueOf(20L)) < 0)
            throw new IllegalArgumentException("Can't print floor area value '" + value + "' because it is below 20!");
        if (value.compareTo(BigInteger.valueOf(50000L)) > 0)
            throw new IllegalArgumentException("Can't print floor area value '" + value + "' because it is above 50000!");

        return DatatypeConverter.printInteger(value);
    }

    /**
     * Write a {@link ForeclosureTypeValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printForeclosureTypeValue(ForeclosureTypeValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty foreclosure type value!");

        return value.write();
    }

    /**
     * Write a {@link BigDecimal} value into XML output
     * with a valid latitude range.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printLatitudeValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty latitude value!");
        if (value.compareTo(new BigDecimal("-90")) < 0)
            throw new IllegalArgumentException("Can't print latitude value '" + value + "' because it is below -90!");
        if (value.compareTo(new BigDecimal("90")) > 0)
            throw new IllegalArgumentException("Can't print latitude value '" + value + "' because it is above 90!");

        value = value.setScale(10, BigDecimal.ROUND_HALF_UP);
        return DatatypeConverter.printDecimal(value);
    }

    /**
     * Write a {@link BigDecimal} value into XML output
     * with a valid longitude range.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printLongitudeValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty longitude value!");
        if (value.compareTo(new BigDecimal("-180")) < 0)
            throw new IllegalArgumentException("Can't print longitude value '" + value + "' because it is below -180!");
        if (value.compareTo(new BigDecimal("180")) > 0)
            throw new IllegalArgumentException("Can't print longitude value '" + value + "' because it is above 180!");

        value = value.setScale(10, BigDecimal.ROUND_HALF_UP);
        return DatatypeConverter.printDecimal(value);
    }

    /**
     * Write a {@link OrientationValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printOrientationValue(OrientationValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty orientation value!");

        return value.write();
    }

    /**
     * Write a {@link BigInteger} value into XML output for a plot area.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPlotAreaValue(BigInteger value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty plot area value!");
        if (value.compareTo(BigInteger.ONE) < 0)
            throw new IllegalArgumentException("Can't print floor plot value '" + value + "' because it is below 1!");
        if (value.compareTo(BigInteger.valueOf(1000000000L)) > 0)
            throw new IllegalArgumentException("Can't print floor plot value '" + value + "' because it is above 1000000000!");

        return DatatypeConverter.printInteger(value);
    }

    /**
     * Write a {@link Currency} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPriceCurrencyValue(Currency value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty price currency value!");

        return value.getCurrencyCode();
    }

    /**
     * Write a {@link PricePeriodValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPricePeriodValue(PricePeriodValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty price period value!");

        return value.write();
    }

    /**
     * Write a {@link BigDecimal} value into XML output for a price.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printPriceValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty price value!");
        if (value.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Can't print price value '" + value + "' because it is below 0!");
        if (value.compareTo(new BigDecimal("1000000000")) > 0)
            throw new IllegalArgumentException("Can't print price value '" + value + "' because it is above 1000000000!");

        value = value.setScale(2, BigDecimal.ROUND_HALF_UP);
        return DatatypeConverter.printDecimal(value);
    }

    /**
     * Write a {@link BigDecimal} value into XML output for a room number.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printRoomsValue(BigDecimal value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty rooms value!");
        if (value.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Can't print rooms value '" + value + "' because it is below 0!");
        if (value.compareTo(new BigDecimal("20")) > 0)
            throw new IllegalArgumentException("Can't print rooms value '" + value + "' because it is above 20!");

        value = value.setScale(1, BigDecimal.ROUND_HALF_UP);
        //return DatatypeConverter.printDecimal( value );

        final BigInteger integerPart = value.toBigInteger();
        final BigInteger decimalPart = value.subtract(new BigDecimal(integerPart, 1)).multiply(BigDecimal.TEN).toBigInteger();
        if (decimalPart.compareTo(BigInteger.ZERO) != 0)
            return integerPart.toString() + ".5";

        return DatatypeConverter.printInteger(integerPart);
    }

    /**
     * Write a {@link TypeValue} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printTypeValue(TypeValue value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty type value!");

        return value.write();
    }

    /**
     * Write an {@link URI} value into XML output.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printUriValue(URI value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty URI value!");
        if ("http".equalsIgnoreCase(value.getScheme()))
            return value.toString();
        if ("https".equalsIgnoreCase(value.getScheme()))
            return value.toString();

        throw new IllegalArgumentException("Can't print URI '" + value + "' because of an unsupported scheme!");
    }

    /**
     * Write a {@link BigInteger} value into XML output for a year number.
     *
     * @param value value to write
     * @return XML string
     * @throws IllegalArgumentException if a validation error occurred
     */
    public static String printYearValue(BigInteger value) {
        if (value == null)
            throw new IllegalArgumentException("Can't print empty year value!");
        if (value.compareTo(BigInteger.valueOf(1700L)) < 0)
            throw new IllegalArgumentException("Can't print year value '" + value + "' because it is below 1700!");
        if (value.compareTo(BigInteger.valueOf(9999L)) > 0)
            throw new IllegalArgumentException("Can't print year value '" + value + "' because it is above 9999!");

        return DatatypeConverter.printInteger(value);
    }
}