/*
 * 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.karaf.cave.repository.service.bundlerepository;

import javax.xml.stream.Location;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import java.io.InputStream;
import java.io.Writer;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.felix.utils.version.VersionTable;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.resource.Capability;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
import org.osgi.service.repository.ContentNamespace;

import static javax.xml.stream.XMLStreamConstants.CHARACTERS;
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;

/**
 * Repository XML xml based on StaX
 */
public final class StaxParser {

    public static final String REPOSITORY = "repository";
    public static final String REPO_NAME = "name";
    public static final String INCREMENT = "increment";
    public static final String REFERRAL = "referral";
    public static final String DEPTH = "depth";
    public static final String URL = "url";
    public static final String RESOURCE = "resource";
    public static final String CAPABILITY = "capability";
    public static final String REQUIREMENT = "requirement";
    public static final String NAMESPACE = "namespace";
    public static final String ATTRIBUTE = "attribute";
    public static final String DIRECTIVE = "directive";
    public static final String NAME = "name";
    public static final String VALUE = "value";
    public static final String TYPE = "type";

    public static final String REPOSITORY_NAMESPACE = "http://www.osgi.org/xmlns/repository/v1.0.0";

    static XMLInputFactory inputFactory;
    static XMLOutputFactory outputFactory;

    private StaxParser() {
    }

    public static class Referral {
        public String url;
        public int depth = Integer.MAX_VALUE;
    }

    public static class XmlRepository {
        public String name;
        public long increment;
        public List<Referral> referrals = new ArrayList<>();
        public List<Resource> resources = new ArrayList<>();
    }

    public static void write(XmlRepository repository, Writer os) throws XMLStreamException {
        XMLStreamWriter writer = getOutputFactory().createXMLStreamWriter(os);
        try {
            writer.writeStartDocument();
            writer.setDefaultNamespace(REPOSITORY_NAMESPACE);
            // repository element
            writer.writeStartElement(REPOSITORY_NAMESPACE, REPOSITORY);
            writer.writeAttribute("xmlns", REPOSITORY_NAMESPACE);
            writer.writeAttribute(REPO_NAME, repository.name);
            writer.writeAttribute(INCREMENT, Long.toString(repository.increment));
            // referrals
            for (Referral referral : repository.referrals) {
                writer.writeStartElement(REPOSITORY_NAMESPACE, REFERRAL);
                writer.writeAttribute(DEPTH, Integer.toString(referral.depth));
                writer.writeAttribute(URL, referral.url);
                writer.writeEndElement();
            }
            // resources
            for (Resource resource : repository.resources) {
                writer.writeStartElement(REPOSITORY_NAMESPACE, RESOURCE);
                for (Capability cap : resource.getCapabilities(null)) {
                    writeClause(writer, CAPABILITY, cap.getNamespace(), cap.getDirectives(), cap.getAttributes());
                }
                for (Requirement req : resource.getRequirements(null)) {
                    writeClause(writer, REQUIREMENT, req.getNamespace(), req.getDirectives(), req.getAttributes());
                }
                writer.writeEndElement();
            }
            writer.writeEndDocument();
            writer.flush();
        } finally {
            writer.close();
        }
    }

    private static void writeClause(XMLStreamWriter writer, String element, String namespace, Map<String, String> directives, Map<String, Object> attributes) throws XMLStreamException {
        writer.writeStartElement(REPOSITORY_NAMESPACE, element);
        writer.writeAttribute(NAMESPACE, namespace);
        for (Map.Entry<String, String> dir : directives.entrySet()) {
            writer.writeStartElement(REPOSITORY_NAMESPACE, DIRECTIVE);
            writer.writeAttribute(NAME, dir.getKey());
            writer.writeAttribute(VALUE, dir.getValue());
            writer.writeEndElement();
        }
        for (Map.Entry<String, Object> att : attributes.entrySet()) {
            String key = att.getKey();
            Object val = att.getValue();
            writer.writeStartElement(REPOSITORY_NAMESPACE, ATTRIBUTE);
            writer.writeAttribute(NAME, key);
            if (val instanceof Version) {
                writer.writeAttribute(TYPE, "Version");
            } else if (val instanceof Long) {
                writer.writeAttribute(TYPE, "Long");
            } else if (val instanceof Double) {
                writer.writeAttribute(TYPE, "Double");
            } else if (val instanceof Iterable) {
                Iterable it = (Iterable) att.getValue();
                String scalar = null;
                for (Object o : it) {
                    String ts;
                    if (o instanceof String) {
                        ts = "String";
                    } else if (o instanceof Long) {
                        ts = "Long";
                    } else if (o instanceof Double) {
                        ts = "Double";
                    } else if (o instanceof Version) {
                        ts = "Version";
                    } else {
                        throw new IllegalArgumentException("Unsupported scalar type: " + o);
                    }
                    if (scalar == null) {
                        scalar = ts;
                    } else if (!scalar.equals(ts)) {
                        throw new IllegalArgumentException("Unconsistent list type for attribute " + key);
                    }
                }
                writer.writeAttribute(TYPE, "List<" + scalar + ">");
                StringBuilder sb = new StringBuilder();
                boolean first = true;
                for (Object o : it) {
                    if (first) {
                        first = false;
                    } else {
                        sb.append(",");
                    }
                    sb.append(o.toString().replace(",", "\\,"));
                }
                val = sb.toString();
            }
            writer.writeAttribute(VALUE, val.toString());
            writer.writeEndElement();
        }
        writer.writeEndElement();
    }

    public static XmlRepository parse(InputStream is) throws XMLStreamException {
        return parse(null, is, null);
    }

    public static XmlRepository parse(URI repositoryUrl, InputStream is) throws XMLStreamException {
        return parse(repositoryUrl, is, null);
    }

    public static XmlRepository parse(URI repositoryUrl, InputStream is, XmlRepository previous) throws XMLStreamException {
        XMLStreamReader reader = getInputFactory().createXMLStreamReader(is);
        try {
            int event = reader.nextTag();
            if (event != START_ELEMENT || !REPOSITORY.equals(reader.getLocalName())) {
                throw new IllegalStateException("Expected element 'repository' at the root of the document");
            }
            XmlRepository repo = new XmlRepository();
            for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
                String attrName = reader.getAttributeLocalName(i);
                String attrValue = reader.getAttributeValue(i);
                switch (attrName) {
                    case REPO_NAME:
                        repo.name = attrValue;
                        break;
                    case INCREMENT:
                        repo.increment = Long.parseLong(attrValue);
                        break;
                    default:
                        throw new IllegalStateException("Unexpected attribute '" + attrName + "'");
                }
            }
            if (previous != null && repo.increment == previous.increment) {
                return previous;
            }
            while ((event = reader.nextTag()) == START_ELEMENT) {
                String element = reader.getLocalName();
                switch (element) {
                    case REFERRAL:
                        Referral referral = new Referral();
                        for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
                            String attrName = reader.getAttributeLocalName(i);
                            String attrValue = reader.getAttributeValue(i);
                            switch (attrName) {
                                case DEPTH:
                                    referral.depth = Integer.parseInt(attrValue);
                                    break;
                                case URL:
                                    referral.url = attrValue;
                                    break;
                                default:
                                    throw new IllegalStateException("Unexpected attribute '" + attrName + "'");
                            }
                        }
                        if (referral.url == null) {
                            throw new IllegalStateException("Expected attribute '" + URL + "'");
                        }
                        repo.referrals.add(referral);
                        sanityCheckEndElement(reader, reader.nextTag(), REFERRAL);
                        break;
                    case RESOURCE:
                        repo.resources.add(parseResource(repositoryUrl, reader));
                        break;
                    default:
                        throw new IllegalStateException("Unsupported element '" + element + "'. Expected 'referral' or 'resource'");
                }
            }
            // Sanity check
            sanityCheckEndElement(reader, event, REPOSITORY);
            return repo;
        } finally {
            reader.close();
        }
    }

    private static void sanityCheckEndElement(XMLStreamReader reader, int event, String element) {
        if (event != END_ELEMENT || !element.equals(reader.getLocalName())) {
            throw new IllegalStateException("Unexpected state while finishing element " + element);
        }
    }

    private static ResourceImpl parseResource(URI repositoryUrl, XMLStreamReader reader) {
        try {
            if (reader.getAttributeCount() > 0) {
                throw new IllegalStateException("Unexpected attribute '" + reader.getAttributeLocalName(0) + "'");
            }
            ResourceImpl resource = new ResourceImpl();
            int event;
            while ((event = reader.nextTag()) == START_ELEMENT) {
                String element = reader.getLocalName();
                switch (element) {
                    case CAPABILITY:
                        CapabilityImpl cap = parseCapability(reader, resource);
                        // Resolve relative resource urls now
                        if (repositoryUrl != null && ContentNamespace.CONTENT_NAMESPACE.equals(cap.getNamespace())) {
                            Object url = cap.getAttributes().get(ContentNamespace.CAPABILITY_URL_ATTRIBUTE);
                            if (url instanceof String) {
                                url = repositoryUrl.resolve(url.toString()).toString();
                                cap.getAttributes().put(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, url);
                            }
                        }
                        resource.addCapability(cap);
                        break;
                    case REQUIREMENT:
                        resource.addRequirement(parseRequirement(reader, resource));
                        break;
                    default:
                        while ((event = reader.next()) != END_ELEMENT) {
                            switch (event) {
                                case START_ELEMENT:
                                    throw new IllegalStateException("Unexpected element '" + reader.getLocalName() + "' inside 'resource' element");
                                case CHARACTERS:
                                    throw new IllegalStateException("Unexpected text inside 'resource' element");
                                default:
                                    break;
                            }
                        }
                        break;
                }
            }
            // Sanity check
            sanityCheckEndElement(reader, event, RESOURCE);
            return resource;
        } catch (Exception e) {
            Location loc = reader.getLocation();
            if (loc != null) {
                throw new IllegalStateException("Error while parsing resource at line " + loc.getLineNumber() + " and column " + loc.getColumnNumber(), e);
            } else {
                throw new IllegalStateException("Error while parsing resource", e);
            }
        }
    }

    private static CapabilityImpl parseCapability(XMLStreamReader reader, ResourceImpl resource) throws XMLStreamException {
        String[] namespace = new String[1];
        Map<String, String> directives = new HashMap<>();
        Map<String, Object> attributes = new HashMap<>();
        parseClause(reader, namespace, directives, attributes);
        sanityCheckEndElement(reader, reader.getEventType(), CAPABILITY);
        return new CapabilityImpl(resource, namespace[0], directives, attributes);
    }

    private static RequirementImpl parseRequirement(XMLStreamReader reader, ResourceImpl resource) throws XMLStreamException {
        String[] namespace = new String[1];
        Map<String, String> directives = new HashMap<>();
        Map<String, Object> attributes = new HashMap<>();
        parseClause(reader, namespace, directives, attributes);
        sanityCheckEndElement(reader, reader.getEventType(), REQUIREMENT);
        String filterStr = directives.get(Constants.FILTER_DIRECTIVE);
        SimpleFilter sf = (filterStr != null)
                ? SimpleFilter.parse(filterStr)
                : SimpleFilter.convert(attributes);
        return new RequirementImpl(resource, namespace[0], directives, attributes, sf);
    }

    private static void parseClause(XMLStreamReader reader, String[] namespace, Map<String, String> directives, Map<String, Object> attributes) throws XMLStreamException {
        namespace[0] = null;
        for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
            String name = reader.getAttributeLocalName(i);
            String value = reader.getAttributeValue(i);
            if (NAMESPACE.equals(name)) {
                namespace[0] = value;
            } else {
                throw new IllegalStateException("Unexpected attribute: '" + name + "'. Expected 'namespace'");
            }
        }
        if (namespace[0] == null) {
            throw new IllegalStateException("Expected attribute 'namespace'");
        }
        while (reader.nextTag() == START_ELEMENT) {
            String element = reader.getLocalName();
            switch (element) {
                case DIRECTIVE: {
                    String name = null;
                    String value = null;
                    for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
                        String attName = reader.getAttributeLocalName(i);
                        String attValue = reader.getAttributeValue(i);
                        switch (attName) {
                            case NAME:
                                name = attValue;
                                break;
                            case VALUE:
                                value = attValue;
                                break;
                            default:
                                throw new IllegalStateException("Unexpected attribute: '" + attName + "'. Expected 'name', or 'value'.");
                        }
                    }
                    if (name == null || value == null) {
                        throw new IllegalStateException("Expected attribute 'name' and 'value'");
                    }
                    directives.put(name, value);
                    sanityCheckEndElement(reader, reader.nextTag(), DIRECTIVE);
                    break;
                }
                case ATTRIBUTE: {
                    String name = null;
                    String value = null;
                    String type = "String";
                    for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) {
                        String attName = reader.getAttributeLocalName(i);
                        String attValue = reader.getAttributeValue(i);
                        switch (attName) {
                            case NAME:
                                name = attValue;
                                break;
                            case VALUE:
                                value = attValue;
                                break;
                            case TYPE:
                                type = attValue;
                                break;
                            default:
                                throw new IllegalStateException("Unexpected attribute: '" + attName + "'. Expected 'name', 'value' or 'type'.");
                        }
                    }
                    if (name == null || value == null) {
                        throw new IllegalStateException("Expected attribute 'name' and 'value'");
                    }
                    attributes.put(name, parseAttribute(value, type));
                    sanityCheckEndElement(reader, reader.nextTag(), ATTRIBUTE);
                    break;
                }
                default:
                    throw new IllegalStateException("Unexpected element: '" + element + ". Expected 'directive' or 'attribute'");
            }
        }
    }

    private static Object parseAttribute(String value, String type) {
        if ("String".equals(type)) {
            return value;
        } else if ("Version".equals(type)) {
            return VersionTable.getVersion(value);
        } else if ("Long".equals(type)) {
            return Long.parseLong(value.trim());
        } else if ("Double".equals(type)) {
            return Double.parseDouble(value.trim());
        } else if (type.startsWith("List<") && type.endsWith(">")) {
            type = type.substring("List<".length(), type.length() - 1);
            List<Object> list = new ArrayList<>();
            for (String s : value.split(",")) {
                list.add(parseAttribute(s.trim(), type));
            }
            return list;
        } else {
            throw new IllegalStateException("Unexpected type: '" + type + "'");
        }
    }

    private static synchronized XMLInputFactory getInputFactory() {
        if (StaxParser.inputFactory == null) {
            XMLInputFactory factory = XMLInputFactory.newInstance();
            factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
            StaxParser.inputFactory = factory;
        }
        return StaxParser.inputFactory;
    }

    private static synchronized XMLOutputFactory getOutputFactory() {
        if (StaxParser.outputFactory == null) {
            StaxParser.outputFactory = XMLOutputFactory.newInstance();
        }
        return StaxParser.outputFactory;
    }

}
