/*
 * Copyright (c) 2022 XMLmind Software. All rights reserved.
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind Assembly Processor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.domutil;

import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URL;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import org.xml.sax.InputSource;
import org.xmlresolver.ResolverFeature;
import org.xmlresolver.XMLResolverConfiguration;
import org.xmlresolver.CatalogManager;
import com.xmlmind.util.ThrowableUtil;
import com.xmlmind.util.StringUtil;
import com.xmlmind.util.URLUtil;
import com.xmlmind.util.XMLText;
import com.xmlmind.util.XMLResolver;

/**
 * An implementation of <code>XMLResolver</code> based on 
 * <a href="https://xmlresolver.org/"><cite>XMLResolver: An enhanced XML 
 * resolver with XML Catalog support.</cite></a>
 * <p>This class is thread-safe.
 */
public final class XMLResolverImpl implements XMLResolver {
    private final CatalogManager catalogManager;
    private final boolean verbose;
    
    // -----------------------------------------------------------------------

    /**
     * Equivalent to {@link #XMLResolverImpl(URL[]) XMLResolverImpl(null)}.
     * <p>Useful when the list of XML catalog files is specified by
     * the value of system property <code>xml.catalog.files</code>.
     */
    public XMLResolverImpl() {
        this(null);
    }

    /**
     * Constructs an <code>XMLResolverImpl</code>.
     *
     * @param urls the URLs of the XML catalog files to be used 
     * by this XML resolver. If <code>null</code>, this list is specified by
     * the value of system property <code>xml.catalog.files</code>, if any.
     */
    public XMLResolverImpl(URL[] urls) {
        // Setting/clearing system properties BEFORE creating
        // XMLResolverConfiguration seems to be the only way to prevent this
        // class from loading/scanning a lot of files.
        
        String catalogFilesProp = System.clearProperty("xml.catalog.files");
        if (catalogFilesProp != null &&
            (catalogFilesProp = catalogFilesProp.trim()).length() == 0) {
            catalogFilesProp = null;
        }
        
        if (urls == null) {
            if (catalogFilesProp != null) {
                urls = XMLResolverImpl.parseCatalogFiles(catalogFilesProp);
            } 
            if (urls == null) {
                urls = URLUtil.EMPTY_LIST;
            }
        }

        // Not sure these features are useful as we directly use a
        // CatalogManager. Anyway consider these as a documentation of what we
        // want here.
        
        final String[] props = {
            "xml.catalog.classpathCatalogs", "false",
            
            // It's OK for a resolved URI to be a "jar:" URI.
            "xml.catalog.maskJarUris", "false",
            
            // <uri> catalog entries can be used to resolve external
            // identifiers. This is in fact the default value.
            "xml.catalog.uriForSystem", "true",
        };
        for (int k = 0; k < props.length; k+=2) {
            System.setProperty(props[k], props[k+1]);
        }
        
        XMLResolverConfiguration config = new XMLResolverConfiguration();
        
        // Allowed values are: "none", "warn", "info", "debug".
        // Example: -Dxml.catalog.defaultLoggerLogLevel=debug
        String logLevel =
            config.getFeature(ResolverFeature.DEFAULT_LOGGER_LOG_LEVEL);
        verbose = ("info".equals(logLevel) || "debug".equals(logLevel));
        
        for (URL url : urls) {
            InputStream in = null;
            try {
                in = URLUtil.openStreamNoCache(url);
                InputSource inSource = new InputSource(in);
                
                URI inURI = url.toURI();
                inSource.setSystemId(inURI.toASCIIString());

                config.addCatalog(inURI, inSource);
                verbose("added XML catalog " + inURI);
            } catch (Exception ex) {
                System.err.println("Cannot load XML catalog '" + url + "':\n" + 
                                   ThrowableUtil.reason(ex));
            } finally {
                if (in != null) {
                    try { in.close(); } catch (IOException ignored) {}
                }
            }
        }
        
        // CatalogManager has no public constructor.
        catalogManager = config.getFeature(ResolverFeature.CATALOG_MANAGER);
        
        // Restore system property "xml.catalog.files" if any.
        if (catalogFilesProp != null) {
            System.setProperty("xml.catalog.files", catalogFilesProp);
        }

    }

    private void verbose(String msg) {
        if (verbose) {
            System.err.println("verbose: " + msg);
        }
    }
    
    public void addRewriteURIEntry(String uriStartString,
                                   String rewritePrefix) {
        StringBuilder buffer = new StringBuilder();
        buffer.append("<?xml version='1.0' ?>\n");
        buffer.append("<catalog");
        buffer.append(" xmlns='urn:oasis:names:tc:entity:xmlns:xml:catalog'");
        buffer.append(" prefer='public'>\n");
        buffer.append("<rewriteURI uriStartString='");
        XMLText.escapeXML(uriStartString, buffer);
        buffer.append("' rewritePrefix='");
        XMLText.escapeXML(rewritePrefix, buffer);
        buffer.append("' />\n");
        buffer.append("</catalog>\n");
        
        InputSource inSource =
            new InputSource(new StringReader(buffer.toString()));

        buffer.setLength(0);
        String tmpDir = System.getProperty("java.io.tmpdir");
        if (tmpDir != null) {
            buffer.append(tmpDir);
        }
        buffer.append(File.separatorChar);
        buffer.append("catalog_");
        buffer.append(Integer.toString(System.identityHashCode(uriStartString),
                                       Character.MAX_RADIX));
        buffer.append(".xml");
        
        URI inURI = (new File(buffer.toString())).toURI();
        inSource.setSystemId(inURI.toASCIIString());
        
        synchronized (catalogManager) {
            ((XMLResolverConfiguration)
             catalogManager.getResolverConfiguration()).addCatalog(inURI,
                                                                   inSource);
        }
        verbose("added XML catalog " + inURI);
    }
    
    public String getResolvedEntity(String publicId, String systemId) {
        String resolved = null;
        synchronized (catalogManager) {
            URI resolvedURI = catalogManager.lookupPublic(systemId, publicId);
            if (resolvedURI != null) {
                resolved = resolvedURI.toASCIIString();
            } else {
                if (systemId != null) {
                    // ResolverFeature.URI_FOR_SYSTEM is true by default.
                    resolved = lookupURI(catalogManager, systemId);
                }
            }
        }
        verbose("publicId='" + publicId + "', systemId='" + systemId +
                "' resolved to " + resolved);
        
        return resolved;
    }
    
    public String resolveURI(String uri) {
        // Why not use org.xmlresolver.CatalogResolver?
        // CatalogResolver.resolveURI returns ResolvedResource which has an
        // opened input stream. We don't need this.
        // 
        // Why not use org.xmlresolver.Resolver?
        // Resolver as an URIResolver returns a SAXSource without
        // a XMLReader that performs catalog resolution. 

        String resolved = null;
        synchronized (catalogManager) {
            resolved = lookupURI(catalogManager, uri);
        }
        verbose("URI " + uri + " resolved to " + resolved);
        
        return resolved;
    }

    private static String lookupURI(CatalogManager catalogManager, String uri) {
        // catalogManager.lookupURI does NOT support fragments.
        String location;
        String fragment;
        int pos = uri.lastIndexOf('#');
        if (pos >= 0) {
            location = uri.substring(0, pos);
            fragment = uri.substring(pos); // Including '#'.
        } else {
            location = uri;
            fragment = null;
        }
        
        String resolved = null;
        URI resolvedURI = catalogManager.lookupURI(location);
        if (resolvedURI != null) {
            resolved = resolvedURI.toASCIIString();
        }
        
        if (resolved != null && fragment != null) {
            resolved += fragment;
        }
        
        return resolved;
    }
    
    // -----------------------------------------------------------------------
    // Utilities related to system property "xml.catalog.files"
    // -----------------------------------------------------------------------
    
    /**
     * Parses specified list of XML catalog files (like the value 
     * of system property <code>xml.catalog.files</code>).
     * 
     * @param list a string containing XML catalog locations (filenames or URLs)
     * separated by ';'
     * @return parsed, possibly empty, list of XML catalog URLs
     */
    static URL[] parseCatalogFiles(String list) {
        if (list == null || (list = list.trim()).length() == 0) {
            return URLUtil.EMPTY_LIST;
        }

        String[] split = StringUtil.split(list, ';');
        ArrayList<URL> urlList = new ArrayList<URL>();

        for (String location : split) {
            location = location.trim();
            if (location.length() == 0) {
                continue;
            }

            URL url = URLUtil.urlOrFile(location);
            if (url != null) {
                urlList.add(url);
            }
        }

        return urlList.toArray(URLUtil.EMPTY_LIST);
    }

    /**
     * Join specified URLs to form a list of XML catalog files (like the value 
     * of system property <code>xml.catalog.files</code>).
     *
     * @param urls the URLs to be joigned
     * @return a string containing XML catalog locations separated by ';'
     */
    static String joinCatalogFiles(URL[] urls) {
        StringBuilder buffer = new StringBuilder();
        final int count = urls.length;
        for (int i = 0; i < count; ++i) {
            if (i > 0) {
                buffer.append(';');
            }
            buffer.append(urls[i]);
        }

        return buffer.toString();
    }
    
    // -----------------------------------------------------------------------

    /*TEST_RESOLVE

    public static void main(String[] args) {
        XMLResolverImpl xmlResolver = new XMLResolverImpl(null);
        
        final int count = args.length;
        int i = 0;
        for (; i < count; ++i) {
            String arg = args[i];

            if ("-a".equals(arg)) {
                if (i+2 > count) {
                    usage();
                    //NOTREACHED
                }
                String uriStartString = args[i+1];
                String rewritePrefix = args[i+2];
                i += 2;
                
                xmlResolver.addRewriteURIEntry(uriStartString, rewritePrefix);
            } else {
                if (arg.startsWith("-")) {
                    usage();
                    //NOTREACHED
                }
                break; // Done with options.
            }
        }

        for (; i < count; ++i) {
            String unresolvedURI = args[i];
            
            String resolvedURI = null;
            if (unresolvedURI.startsWith("PUBLIC=")) {
                unresolvedURI = unresolvedURI.substring(7);
                resolvedURI =
                    xmlResolver.getResolvedEntity(unresolvedURI, null);
            } else if (unresolvedURI.startsWith("SYSTEM=")) {
                unresolvedURI = unresolvedURI.substring(7);
                resolvedURI =
                    xmlResolver.getResolvedEntity(null, unresolvedURI);
            } else {
                resolvedURI = xmlResolver.resolveURI(unresolvedURI);
            }
            
            System.out.println(unresolvedURI + " --> " + resolvedURI);
        }
    }

    private static void usage() {
        System.err.println(
            "usage: java com.xmlmind.domutil.XMLResolverImpl" +
            " [-a uri_start_string rewrite_prefix]" +
            " unresolved_uri ... unresolved_uri");
        System.exit(1);
    }

    TEST_RESOLVE*/
}

