/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.sling.scripting.jsp;

import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.sling.scripting.jsp.jasper.JasperException;
import org.apache.sling.scripting.jsp.jasper.compiler.TldLocationsCache;
import org.apache.sling.scripting.jsp.jasper.xmlparser.ParserUtils;
import org.apache.sling.scripting.jsp.jasper.xmlparser.TreeNode;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.ServiceRegistration;

/**
 * The <code>SlingTldLocationsCache</code> TODO
 */
public class SlingTldLocationsCache
    extends TldLocationsCache implements BundleListener {

    private static final String TLD_SCHEME = "tld:";

    private final Map<String, TldLocationEntry> tldLocations = new HashMap<>();

    private ServiceRegistration serviceRegistration;

    private final BundleContext bundleContext;

    public SlingTldLocationsCache(final BundleContext context) {
        this.bundleContext = context;
        context.addBundleListener(this);
        final Bundle[] bundles = context.getBundles();
        for (int i = 0; i < bundles.length; i++) {
            if (bundles[i].getState() == Bundle.RESOLVED || bundles[i].getState() == Bundle.ACTIVE ) {
                addBundle(bundles[i]);
            }
        }

        Dictionary<String, Object> tldConfigPrinterProperties = new Hashtable<>();
        tldConfigPrinterProperties.put("felix.webconsole.label", "jsptaglibs");
        tldConfigPrinterProperties.put("felix.webconsole.title", "JSP Taglibs");
        tldConfigPrinterProperties.put("felix.webconsole.configprinter.modes", "always");
        this.serviceRegistration = context.registerService(Object.class.getName(),
            this, tldConfigPrinterProperties);

    }

    public void deactivate(final BundleContext context) {
        if (this.serviceRegistration != null) {
            this.serviceRegistration.unregister();
            this.serviceRegistration = null;
        }
        context.removeBundleListener(this);
    }

    // ---------- Tld Location URL support -------------------------------------

    @Override
    public void bundleChanged(final BundleEvent event) {
        if ( event.getType() == BundleEvent.RESOLVED ) {
            this.addBundle(event.getBundle());
        } else if ( event.getType() == BundleEvent.UNRESOLVED ) {
            this.removeBundle(event.getBundle());
        }
    }

    public URL getTldLocationURL(String tldLocation) {
        if (tldLocation.startsWith(TLD_SCHEME)) {
            tldLocation = tldLocation.substring(TLD_SCHEME.length());

            TldLocationEntry tle;
            synchronized (tldLocations) {
                tle = tldLocations.get(tldLocation);
            }

            if (tle != null) {
                return tle.getTldURL();
            }
        }

        return null;
    }

    // ---------- TldLocationsCache support ------------------------------------

    @Override
    public String[] getLocation(final String uri) throws JasperException {
        synchronized (tldLocations) {
            final TldLocationEntry entry = tldLocations.get(uri);
            if ( entry != null ) {
                return new String[] { TLD_SCHEME + uri, entry.getTldURL().toString() };
            }
        }

        return null;
    }

    private void addBundle(final Bundle bundle) {
        // currently only META-INF/*.tld is supported, this should
        // be extended for registration in a Bundle Manifest Header

        final Enumeration<?> entries = bundle.findEntries("META-INF", "*.tld", false);
        if (entries != null) {
            while (entries.hasMoreElements()) {
                final URL taglib = (URL) entries.nextElement();
                final String uri = getUriFromTld(taglib);

                synchronized (tldLocations) {
                    if (uri != null && !tldLocations.containsKey(uri)) {
                        tldLocations.put(uri, new TldLocationEntry(bundle, taglib));
                    }
                }
            }
        }
    }

    private void removeBundle(final Bundle bundle) {
        synchronized (tldLocations) {
            final Iterator<Map.Entry<String, TldLocationEntry>> i = tldLocations.entrySet().iterator();
            while ( i.hasNext() ) {
                final Map.Entry<String, TldLocationEntry> entry = i.next();
                if (entry.getValue().getBundleId() == bundle.getBundleId()) {
                    i.remove();
                }
            }
        }
    }

    /*
     * Returns the value of the uri element of the given TLD, or null if the
     * given TLD does not contain any such element.
     */
    private String getUriFromTld(final URL resource) {
        InputStream stream = null;
        try {
            stream = resource.openStream();

            // Parse the tag library descriptor at the specified resource path
            TreeNode tld = new ParserUtils().parseXMLDocument(
                resource.toString(), stream);
            TreeNode uri = tld.findChild("uri");
            if (uri != null) {
                String body = uri.getBody();
                if (body != null) {
                    return body;
                }
            }
        } catch (Exception e) {
            // TODO: handle
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (Throwable t) {
                    // do nothing
                }
            }
        }

        return null;
    }

    public void printConfiguration(final PrintWriter pw) {
        pw.println("Currently available JSP Taglibs:");
        final SortedMap<String, String> taglibs = new TreeMap<>();

        for (final Map.Entry<String, TldLocationEntry> entry : tldLocations.entrySet()) {
            final long bundleId = entry.getValue().getBundleId();
            final Bundle bundle = bundleContext.getBundle(bundleId);
            if (bundle != null) {
                taglibs.put(entry.getKey(), String.format("%s (%s)", bundle.getSymbolicName(), bundleId));
            } else {
                // really shouldn't happen
                taglibs.put(entry.getKey(), String.format("INVALID BUNDLE ID: %s", bundleId));
            }
        }

        for (final Map.Entry<String, String> entry : taglibs.entrySet()) {
            pw.printf("  %s - %s\n", entry.getKey(), entry.getValue());
        }
    }


    private static final class TldLocationEntry {
        private final long bundleId;

        private final URL tldURL;

        private TldLocationEntry(final Bundle bundle, final URL tldURL) {
            this.bundleId = bundle.getBundleId();
            this.tldURL = tldURL;
        }

        long getBundleId() {
            return this.bundleId;
        }

        URL getTldURL() {
            return this.tldURL;
        }
    }
}
