/*
 * Copyright (C) 2013 4th Line GmbH, Switzerland
 *
 * The contents of this file are subject to the terms of either the GNU
 * Lesser General Public License Version 2 or later ("LGPL") or the
 * Common Development and Distribution License Version 1 or later
 * ("CDDL") (collectively, the "License"). You may not use this file
 * except in compliance with the License. See LICENSE.txt for more
 * information.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */

package org.fourthline.cling.binding.xml;

import org.fourthline.cling.model.ValidationException;
import org.fourthline.cling.model.meta.Device;
import org.seamless.util.Exceptions;
import org.seamless.xml.ParserException;
import org.seamless.xml.XmlPullParserUtils;
import org.xml.sax.SAXParseException;

import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Michael Pujos
 */
public class RecoveringUDA10DeviceDescriptorBinderImpl extends UDA10DeviceDescriptorBinderImpl {

    private static Logger log = Logger.getLogger(RecoveringUDA10DeviceDescriptorBinderImpl.class.getName());

    @Override
    public <D extends Device> D describe(D undescribedDevice, String descriptorXml) throws DescriptorBindingException, ValidationException {

        D device = null;
        DescriptorBindingException originalException;
        try {

            try {
                descriptorXml = descriptorXml.trim(); // Always trim whitespace
                device = super.describe(undescribedDevice, descriptorXml);
                return device;
            } catch (DescriptorBindingException ex) {
                log.warning("Regular parsing failed: " + Exceptions.unwrap(ex).getMessage());
                originalException = ex;
            }

            String fixedXml;
            // The following modifications are not cumulative!

            fixedXml = fixGarbageTrailingChars(descriptorXml, originalException);
            if (fixedXml != null) {
                try {
                    device = super.describe(undescribedDevice, fixedXml);
                    return device;
                } catch (DescriptorBindingException ex) {
                    log.warning("Removing trailing garbage didn't work: " + Exceptions.unwrap(ex).getMessage());
                }
            }

            // Try to fix "up to five" missing namespace declarations
            DescriptorBindingException lastException = originalException;
            fixedXml = descriptorXml;
            for (int retryCount = 0; retryCount < 5; retryCount++) {
                fixedXml = fixMissingNamespaces(fixedXml, lastException);
                if (fixedXml != null) {
                    try {
                        device = super.describe(undescribedDevice, fixedXml);
                        return device;
                    } catch (DescriptorBindingException ex) {
                        log.warning("Fixing namespace prefix didn't work: " + Exceptions.unwrap(ex).getMessage());
                        lastException = ex;
                    }
                } else {
                    break; // We can stop, no more namespace fixing can be done
                }
            }

            fixedXml = XmlPullParserUtils.fixXMLEntities(descriptorXml);
            if(!fixedXml.equals(descriptorXml)) {
                try {
                    device = super.describe(undescribedDevice, fixedXml);
                    return device;
                } catch (DescriptorBindingException ex) {
                    log.warning("Fixing XML entities didn't work: " + Exceptions.unwrap(ex).getMessage());
                }
            }

            handleInvalidDescriptor(descriptorXml, originalException);

        } catch (ValidationException ex) {
            device = handleInvalidDevice(descriptorXml, device, ex);
            if (device != null)
                return device;
        }
        throw new IllegalStateException("No device produced, did you swallow exceptions in your subclass?");
    }

    protected String fixGarbageTrailingChars(String descriptorXml, DescriptorBindingException ex) {
        int index = descriptorXml.indexOf("</root>");
        if (index == -1) {
            log.warning("No closing </root> element in descriptor");
            return null;
        }
        if (descriptorXml.length() != index + "</root>".length()) {
            log.warning("Detected garbage characters after <root> node, removing");
            return descriptorXml.substring(0, index) + "</root>";
        }
        return null;
    }

    protected String fixMissingNamespaces(String descriptorXml, DescriptorBindingException ex) {
        // Windows: org.fourthline.cling.binding.xml.DescriptorBindingException: Could not parse device descriptor: org.seamless.xml.ParserException: org.xml.sax.SAXParseException: The prefix "dlna" for element "dlna:X_DLNADOC" is not bound.
        // Android: org.xmlpull.v1.XmlPullParserException: undefined prefix: dlna (position:START_TAG <{null}dlna:X_DLNADOC>@19:17 in java.io.StringReader@406dff48)

        // We can only handle certain exceptions, depending on their type and message
        Throwable cause = ex.getCause();
        if (!((cause instanceof SAXParseException) || (cause instanceof ParserException)))
            return null;
        String message = cause.getMessage();
        if (message == null)
            return null;

        Pattern pattern = Pattern.compile("The prefix \"(.*)\" for element"); // Windows
        Matcher matcher = pattern.matcher(message);
        if (!matcher.find() || matcher.groupCount() != 1) {
            pattern = Pattern.compile("undefined prefix: ([^ ]*)"); // Android
            matcher = pattern.matcher(message);
            if (!matcher.find() || matcher.groupCount() != 1)
                return null;
        }

        String missingNS = matcher.group(1);
        log.warning("Fixing missing namespace declaration for: " + missingNS);

        // Extract <root> attributes
        pattern = Pattern.compile("<root([^>]*)");
        matcher = pattern.matcher(descriptorXml);
        if (!matcher.find() || matcher.groupCount() != 1) {
            log.fine("Could not find <root> element attributes");
            return null;
        }

        String rootAttributes = matcher.group(1);
        log.fine("Preserving existing <root> element attributes/namespace declarations: " + matcher.group(0));

        // Extract <root> body
        pattern = Pattern.compile("<root[^>]*>(.*)</root>", Pattern.DOTALL);
        matcher = pattern.matcher(descriptorXml);
        if (!matcher.find() || matcher.groupCount() != 1) {
            log.fine("Could not extract body of <root> element");
            return null;
        }

        String rootBody = matcher.group(1);

        // Add missing namespace, it only matters that it is defined, not that it is correct
        return "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
            + "<root "
            + String.format("xmlns:%s=\"urn:schemas-dlna-org:device-1-0\"", missingNS) + rootAttributes + ">"
            + rootBody
            + "</root>";

        // TODO: Should we match different undeclared prefixes with their correct namespace?
        // So if it's "dlna" we use "urn:schemas-dlna-org:device-1-0" etc.
    }

    /**
     * Handle processing errors while reading XML descriptors.
     * <p/>
     * <p>
     * Typically you want to log this problem or create an error report, and in any
     * case, throw a {@link DescriptorBindingException} to notify the caller of the
     * binder of this failure. The default implementation simply rethrows the
     * given exception.
     * </p>
     *
     * @param xml       The original XML causing the parsing failure.
     * @param exception The original exception while parsing the XML.
     */
    protected void handleInvalidDescriptor(String xml, DescriptorBindingException exception)
        throws DescriptorBindingException {
        throw exception;
    }

    /**
     * Handle processing errors while binding XML descriptors.
     * <p/>
     * <p>
     * Typically you want to log this problem or create an error report. You
     * should throw a {@link ValidationException} to notify the caller of the
     * binder of failure. The default implementation simply rethrows the
     * given exception.
     * </p>
     * <p>
     * This method gives you a final chance to fix the problem, instead of
     * throwing an exception, you could try to create valid {@link Device}
     * model and return it.
     * </p>
     *
     * @param xml       The original XML causing the binding failure.
     * @param device    The unfinished {@link Device} that failed validation
     * @param exception The errors found when validating the {@link Device} model.
     * @return Device A "fixed" {@link Device} model, instead of throwing an exception.
     */
    protected <D extends Device> D handleInvalidDevice(String xml, D device, ValidationException exception)
        throws ValidationException {
        throw exception;
    }
}
