package org.bndtools.core.templating.repobased;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;

import org.apache.felix.metatype.AD;
import org.apache.felix.metatype.MetaData;
import org.apache.felix.metatype.MetaDataReader;
import org.apache.felix.metatype.OCD;
import org.bndtools.templating.BytesResource;
import org.bndtools.templating.FolderResource;
import org.bndtools.templating.Resource;
import org.bndtools.templating.ResourceMap;
import org.bndtools.templating.Template;
import org.bndtools.templating.TemplateEngine;
import org.bndtools.templating.util.AttributeDefinitionImpl;
import org.bndtools.templating.util.CompositeOCD;
import org.bndtools.templating.util.ObjectClassDefinitionImpl;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.osgi.framework.Version;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.resource.Capability;
import org.osgi.service.metatype.AttributeDefinition;
import org.osgi.service.metatype.ObjectClassDefinition;
import org.osgi.service.repository.ContentNamespace;

import aQute.bnd.osgi.resource.ResourceUtils;
import aQute.lib.io.IO;
import bndtools.Plugin;

public class CapabilityBasedTemplate implements Template {

    private static final String DEFAULT_DIR = "template/";

    private final Capability capability;
    private final BundleLocator locator;
    private final TemplateEngine engine;

    private final String name;
    private final String category;
    private final String description;
    private final Version version;

    private final String dir;
    private final URI iconUri;

    private final String metaTypePath;
    private final String ocdRef;

    private final String helpPath;

    private File _bundleFile = null;
    private ResourceMap _inputResources = null;

    public CapabilityBasedTemplate(Capability capability, BundleLocator locator, TemplateEngine engine) {
        this.capability = capability;
        this.locator = locator;
        this.engine = engine;

        Map<String, Object> attrs = capability.getAttributes();

        Object nameObj = attrs.get("name");
        this.name = nameObj instanceof String ? (String) nameObj : "<<unknown>>";

        this.description = "from " + ResourceUtils.getIdentityCapability(capability.getResource())
            .osgi_identity();

        Object categoryObj = attrs.get("category");
        category = categoryObj instanceof String ? (String) categoryObj : null;

        // Get version from the capability if found, otherwise it comes from the bundle
        Object versionObj = attrs.get("version");
        if (versionObj instanceof Version)
            this.version = (Version) versionObj;
        else if (versionObj instanceof String)
            this.version = Version.parseVersion((String) versionObj);
        else {
            String v = ResourceUtils.getIdentityVersion(capability.getResource());
            this.version = v != null ? Version.parseVersion(v) : Version.emptyVersion;
        }

        Object dirObj = attrs.get("dir");
        if (dirObj instanceof String) {
            String dirStr = ((String) dirObj).trim();
            if (dirStr.charAt(dirStr.length() - 1) != '/')
                dirStr += '/';
            this.dir = dirStr;
        } else {
            this.dir = DEFAULT_DIR;
        }

        Object iconObj = attrs.get("icon");
        iconUri = iconObj instanceof String ? URI.create((String) iconObj) : null;

        Object helpObj = attrs.get("help");
        helpPath = helpObj instanceof String ? (String) helpObj : null;

        Object metaTypeObj = attrs.get("metaType");
        metaTypePath = metaTypeObj instanceof String ? (String) metaTypeObj : null;

        Object ocdObj = attrs.get("ocd");
        ocdRef = ocdObj instanceof String ? ((String) ocdObj).trim() : null;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getCategory() {
        return category;
    }

    @Override
    public String getShortDescription() {
        return description;
    }

    @Override
    public Version getVersion() {
        return version;
    }

    @Override
    public int getRanking() {
        Object rankingObj = capability.getAttributes()
            .get("ranking");
        return rankingObj instanceof Number ? ((Number) rankingObj).intValue() : 0;
    }

    @Override
    public ObjectClassDefinition getMetadata() throws Exception {
        return getMetadata(new NullProgressMonitor());
    }

    @Override
    public ObjectClassDefinition getMetadata(IProgressMonitor monitor) throws Exception {
        String resourceId = ResourceUtils.getIdentityCapability(capability.getResource())
            .osgi_identity();

        final CompositeOCD compositeOcd = new CompositeOCD(name, description, null);

        if (metaTypePath != null) {
            try (JarFile bundleJarFile = new JarFile(fetchBundle())) {
                JarEntry metaTypeEntry = bundleJarFile.getJarEntry(metaTypePath);
                try (InputStream entryInput = bundleJarFile.getInputStream(metaTypeEntry)) {
                    MetaData metaData = new MetaDataReader().parse(entryInput);

                    @SuppressWarnings("rawtypes")
                    Map ocdMap = metaData.getObjectClassDefinitions();
                    if (ocdMap != null) {
                        if (ocdMap.size() == 1) {
                            @SuppressWarnings("unchecked")
                            Entry<String, OCD> entry = (Entry<String, OCD>) ocdMap.entrySet()
                                .iterator()
                                .next();
                            // There is exactly one OCD, but if the capability specified the 'ocd' property then it must
                            // match.
                            if (ocdRef == null || ocdRef.equals(entry.getKey())) {
                                compositeOcd.addDelegate(new FelixOCDAdapter(entry.getValue()));
                            } else {
                                log(IStatus.WARNING, String.format("MetaType entry '%s' from resource '%s' did not contain an Object Class Definition with id '%s'", metaTypePath, resourceId, ocdRef), null);
                            }
                        } else {
                            // There are multiple OCDs in the MetaType record, so the capability must have specified the
                            // 'ocd' property.
                            if (ocdRef != null) {
                                OCD felixOcd = (OCD) ocdMap.get(ocdRef);
                                if (felixOcd != null) {
                                    compositeOcd.addDelegate(new FelixOCDAdapter(felixOcd));
                                } else {
                                    log(IStatus.WARNING, String.format("MetaType entry '%s' from resource '%s' did not contain an Object Class Definition with id '%s'", metaTypePath, resourceId, ocdRef), null);
                                }
                            } else {
                                log(IStatus.WARNING, String.format("MetaType entry '%s' from resource '%s' contains multiple Object Class Definitions, and no 'ocd' property was specified.", metaTypePath, resourceId), null);
                            }
                        }
                    }
                }
            }
        }

        // Add attribute definitions for any parameter names found in the templates and not already
        // loaded from the Metatype XML.
        ObjectClassDefinitionImpl ocdImpl = new ObjectClassDefinitionImpl(name, description, null);
        ResourceMap inputs = getInputSources();
        Map<String, String> params = engine.getTemplateParameters(inputs, monitor);
        for (Entry<String, String> entry : params.entrySet()) {
            AttributeDefinitionImpl ad = new AttributeDefinitionImpl(entry.getKey(), entry.getKey(), 0, AttributeDefinition.STRING);
            if (entry.getValue() != null)
                ad.setDefaultValue(new String[] {
                    entry.getValue()
                });
            ocdImpl.addAttribute(ad, true);
        }
        compositeOcd.addDelegate(ocdImpl);

        return compositeOcd;
    }

    @Override
    public ResourceMap generateOutputs(Map<String, List<Object>> parameters) throws Exception {
        return generateOutputs(parameters, new NullProgressMonitor());
    }

    @Override
    public ResourceMap generateOutputs(Map<String, List<Object>> parameters, IProgressMonitor monitor) throws Exception {
        ResourceMap inputs = getInputSources();
        return engine.generateOutputs(inputs, parameters, monitor);
    }

    @Override
    public URI getIcon() {
        return iconUri;
    }

    @Override
    public URI getHelpContent() {
        URI uri = null;
        if (helpPath != null) {
            try {
                File f = fetchBundle();
                uri = new URI("jar:" + f.toURI()
                    .toURL() + "!/" + helpPath);
            } catch (Exception e) {
                // ignore
            }
        }
        return uri;
    }

    private synchronized ResourceMap getInputSources() throws IOException {
        File bundleFile = fetchBundle();

        _inputResources = new ResourceMap();
        try (JarInputStream in = new JarInputStream(IO.stream(bundleFile))) {
            JarEntry jarEntry = in.getNextJarEntry();
            while (jarEntry != null) {
                String entryPath = jarEntry.getName()
                    .trim();
                if (entryPath.startsWith(dir)) {
                    String relativePath = entryPath.substring(dir.length());
                    if (!relativePath.isEmpty()) { // skip the root folder
                        Resource resource;
                        if (relativePath.endsWith("/")) {
                            // strip the trailing slash
                            relativePath.substring(0, relativePath.length());
                            resource = new FolderResource();
                        } else {
                            // cannot use IO.collect() because it closes the whole JarInputStream
                            resource = BytesResource.loadFrom(in);
                        }
                        _inputResources.put(relativePath, resource);
                    }

                }
                jarEntry = in.getNextJarEntry();
            }
        }
        return _inputResources;
    }

    private synchronized File fetchBundle() throws IOException {
        if (_bundleFile != null && _bundleFile.exists())
            return _bundleFile;

        Capability idCap = capability.getResource()
            .getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE)
            .get(0);
        String id = (String) idCap.getAttributes()
            .get(IdentityNamespace.IDENTITY_NAMESPACE);

        Capability contentCap = capability.getResource()
            .getCapabilities(ContentNamespace.CONTENT_NAMESPACE)
            .get(0);
        URI location;
        Object locationObj = contentCap.getAttributes()
            .get("url");
        if (locationObj instanceof URI)
            location = (URI) locationObj;
        else if (locationObj instanceof String)
            location = URI.create((String) locationObj);
        else
            throw new IOException("Template repository entry is missing url attribute");

        if ("file".equals(location.getScheme())) {
            _bundleFile = IO.getFile(location.getPath());
            return _bundleFile;
        }

        // Try to locate from the workspace and/or repositories if a BundleLocator was provide
        if (locator != null) {
            String hashStr = (String) contentCap.getAttributes()
                .get(ContentNamespace.CONTENT_NAMESPACE);
            try {
                _bundleFile = locator.locate(id, hashStr, "SHA-256", location);
                if (_bundleFile != null)
                    return _bundleFile;
            } catch (Exception e) {
                throw new IOException("Unable to fetch bundle for template: " + getName(), e);
            }
        }
        throw new IOException("Unable to fetch bundle for template: " + getName());
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((capability == null) ? 0 : capability.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        CapabilityBasedTemplate other = (CapabilityBasedTemplate) obj;
        if (capability == null) {
            if (other.capability != null)
                return false;
        } else if (!capability.equals(other.capability))
            return false;
        return true;
    }

    @Override
    public void close() throws IOException {
        // nothing to do
    }

    private static void log(int level, String message, Throwable e) {
        Plugin.getDefault()
            .getLog()
            .log(new Status(level, Plugin.PLUGIN_ID, 0, message, e));
    }

    private static class FelixADAdapter implements AttributeDefinition {

        private final AD ad;

        public FelixADAdapter(AD ad) {
            this.ad = ad;
        }

        @Override
        public String getName() {
            return ad.getName();
        }

        @Override
        public String getID() {
            return ad.getID();
        }

        @Override
        public String getDescription() {
            return ad.getDescription();
        }

        @Override
        public int getCardinality() {
            return ad.getCardinality();
        }

        @Override
        public int getType() {
            return ad.getType();
        }

        @Override
        public String[] getOptionValues() {
            return ad.getOptionValues();
        }

        @Override
        public String[] getOptionLabels() {
            return ad.getOptionLabels();
        }

        @Override
        public String validate(String value) {
            return ad.validate(value);
        }

        @Override
        public String[] getDefaultValue() {
            return ad.getDefaultValue();
        }
    }

    private static class FelixOCDAdapter implements ObjectClassDefinition {

        private final OCD ocd;

        public FelixOCDAdapter(OCD ocd) {
            if (ocd == null)
                throw new NullPointerException();
            this.ocd = ocd;
        }

        @Override
        public String getName() {
            return ocd.getName();
        }

        @Override
        public String getID() {
            return ocd.getID();
        }

        @Override
        public String getDescription() {
            return ocd.getDescription();
        }

        @SuppressWarnings("unchecked")
        @Override
        public AttributeDefinition[] getAttributeDefinitions(int filter) {
            if (ocd.getAttributeDefinitions() == null)
                return null;

            Iterator<AD> iter = ocd.getAttributeDefinitions()
                .values()
                .iterator();
            if (filter == ObjectClassDefinition.OPTIONAL || filter == ObjectClassDefinition.REQUIRED) {
                boolean required = (filter == ObjectClassDefinition.REQUIRED);
                iter = new RequiredFilterIterator(iter, required);
            } else if (filter != ObjectClassDefinition.ALL) {
                return null;
            }

            if (!iter.hasNext())
                return null;

            List<AttributeDefinition> result = new ArrayList<>();
            while (iter.hasNext()) {
                result.add(new FelixADAdapter(iter.next()));
            }
            return result.toArray(new AttributeDefinition[0]);
        }

        @Override
        public InputStream getIcon(int size) throws IOException {
            // TODO
            return null;
        }

        @SuppressWarnings("rawtypes")
        private static class RequiredFilterIterator implements Iterator {

            private final Iterator base;

            private final boolean required;

            private AD next;

            private RequiredFilterIterator(Iterator base, boolean required) {
                this.base = base;
                this.required = required;
                this.next = seek();
            }

            @Override
            public boolean hasNext() {
                return next != null;
            }

            @Override
            public Object next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }

                AD toReturn = next;
                next = seek();
                return toReturn;
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException("remove");
            }

            private AD seek() {
                if (base.hasNext()) {
                    AD next;
                    do {
                        next = (AD) base.next();
                    } while (next.isRequired() != required && base.hasNext());

                    if (next.isRequired() == required) {
                        return next;
                    }
                }

                // nothing found any more
                return null;
            }

        }
    }

}
