/*
 *  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
 *
 *      https://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.ivy.osgi.updatesite;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.ivy.core.cache.CacheResourceOptions;
import org.apache.ivy.core.cache.RepositoryCacheManager;
import org.apache.ivy.core.event.EventManager;
import org.apache.ivy.core.report.ArtifactDownloadReport;
import org.apache.ivy.core.report.DownloadStatus;
import org.apache.ivy.core.settings.TimeoutConstraint;
import org.apache.ivy.osgi.core.ExecutionEnvironmentProfileProvider;
import org.apache.ivy.osgi.p2.P2ArtifactParser;
import org.apache.ivy.osgi.p2.P2CompositeParser;
import org.apache.ivy.osgi.p2.P2Descriptor;
import org.apache.ivy.osgi.p2.P2MetadataParser;
import org.apache.ivy.osgi.p2.XMLInputParser;
import org.apache.ivy.osgi.repo.RepoDescriptor;
import org.apache.ivy.osgi.updatesite.xml.EclipseFeature;
import org.apache.ivy.osgi.updatesite.xml.EclipseUpdateSiteParser;
import org.apache.ivy.osgi.updatesite.xml.FeatureParser;
import org.apache.ivy.osgi.updatesite.xml.UpdateSite;
import org.apache.ivy.osgi.updatesite.xml.UpdateSiteDigestParser;
import org.apache.ivy.plugins.repository.url.URLRepository;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.util.Message;
import org.xml.sax.SAXException;

public class UpdateSiteLoader {

    private final RepositoryCacheManager repositoryCacheManager;

    private final URLRepository urlRepository = new URLRepository();

    private final CacheResourceOptions options;

    private final TimeoutConstraint timeoutConstraint;

    private int logLevel = Message.MSG_INFO;

    public UpdateSiteLoader(final RepositoryCacheManager repositoryCacheManager,
                            final EventManager eventManager, final CacheResourceOptions options,
                            final TimeoutConstraint timeoutConstraint) {
        this.repositoryCacheManager = repositoryCacheManager;
        this.options = options;
        this.timeoutConstraint = timeoutConstraint;
        if (eventManager != null) {
            urlRepository.addTransferListener(eventManager);
        }
    }

    public void setLogLevel(int logLevel) {
        this.logLevel = logLevel;
    }

    public RepoDescriptor load(URI repoUri) throws IOException, ParseException, SAXException {
        if (!repoUri.toString().endsWith("/")) {
            try {
                repoUri = new URI(repoUri.toString() + "/");
            } catch (URISyntaxException e) {
                throw new RuntimeException("Cannot make an uri for the repo");
            }
        }
        Message.info("Loading the update site " + repoUri);
        // first look for a p2 repository
        RepoDescriptor repo = loadP2(repoUri);
        if (repo != null) {
            return repo;
        }
        Message.verbose("\tNo P2 artifacts, falling back on the old fashioned updatesite");
        // then try the old update site
        UpdateSite site = loadSite(repoUri);
        if (site == null) {
            return null;
        }
        repo = loadFromDigest(site);
        if (repo != null) {
            return repo;
        }
        return loadFromSite(site);
    }

    private P2Descriptor loadP2(URI repoUri) throws IOException, ParseException, SAXException {
        P2Descriptor p2Descriptor = new P2Descriptor(repoUri,
                ExecutionEnvironmentProfileProvider.getInstance());
        p2Descriptor.setLogLevel(logLevel);
        if (!populateP2Descriptor(repoUri, p2Descriptor)) {
            return null;
        }
        p2Descriptor.finish();
        return p2Descriptor;
    }

    private boolean populateP2Descriptor(URI repoUri, P2Descriptor p2Descriptor)
            throws IOException, ParseException, SAXException {
        Message.verbose("Loading P2 repository " + repoUri);
        boolean contentExists = readContent(repoUri, p2Descriptor);
        boolean artifactExists = readArtifacts(repoUri, p2Descriptor);
        return artifactExists || contentExists;
    }

    private boolean readContent(URI repoUri, P2Descriptor p2Descriptor) throws IOException,
            ParseException, SAXException {
        boolean contentExists = readCompositeContent(repoUri, "compositeContent", p2Descriptor);
        if (!contentExists) {
            P2MetadataParser metadataParser = new P2MetadataParser(p2Descriptor);
            metadataParser.setLogLevel(logLevel);
            contentExists = readJarOrXml(repoUri, "content", metadataParser);
        }
        return contentExists;
    }

    private boolean readArtifacts(URI repoUri, P2Descriptor p2Descriptor) throws IOException,
            ParseException, SAXException {
        boolean artifactExists = readCompositeArtifact(repoUri, "compositeArtifacts", p2Descriptor);
        if (!artifactExists) {
            artifactExists = readJarOrXml(repoUri, "artifacts", new P2ArtifactParser(p2Descriptor,
                    repoUri.toURL().toExternalForm()));
        }

        return artifactExists;
    }

    private boolean readCompositeContent(URI repoUri, String name, P2Descriptor p2Descriptor)
            throws IOException, ParseException, SAXException {
        P2CompositeParser p2CompositeParser = new P2CompositeParser();
        boolean exist = readJarOrXml(repoUri, name, p2CompositeParser);
        if (exist) {
            for (String childLocation : p2CompositeParser.getChildLocations()) {
                if (!childLocation.endsWith("/")) {
                    childLocation += "/";
                }
                URI childUri = repoUri.resolve(childLocation);
                readContent(childUri, p2Descriptor);
            }
        }
        return exist;
    }

    private boolean readCompositeArtifact(URI repoUri, String name, P2Descriptor p2Descriptor)
            throws IOException, ParseException, SAXException {
        P2CompositeParser p2CompositeParser = new P2CompositeParser();
        boolean exist = readJarOrXml(repoUri, name, p2CompositeParser);
        if (exist) {
            for (String childLocation : p2CompositeParser.getChildLocations()) {
                if (!childLocation.endsWith("/")) {
                    childLocation += "/";
                }
                URI childUri = repoUri.resolve(childLocation);
                readArtifacts(childUri, p2Descriptor);
            }
        }
        return exist;
    }

    private boolean readJarOrXml(URI repoUri, String baseName, XMLInputParser reader)
            throws IOException, ParseException, SAXException {
        InputStream readIn = null; // the input stream from which the xml should be read

        URL contentUrl = repoUri.resolve(baseName + ".jar").toURL();
        URLResource res = new URLResource(contentUrl, this.timeoutConstraint);

        ArtifactDownloadReport report = repositoryCacheManager.downloadRepositoryResource(res,
            baseName, baseName, "jar", options, urlRepository);

        if (report.getDownloadStatus() == DownloadStatus.FAILED) {
            // no jar file, try the xml one
            contentUrl = repoUri.resolve(baseName + ".xml").toURL();
            res = new URLResource(contentUrl, this.timeoutConstraint);

            report = repositoryCacheManager.downloadRepositoryResource(res, baseName, baseName,
                "xml", options, urlRepository);

            if (report.getDownloadStatus() == DownloadStatus.FAILED) {
                // no xml either
                return false;
            }

            readIn = new FileInputStream(report.getLocalFile());
        } else {
            InputStream in = new FileInputStream(report.getLocalFile());

            try {
                // compressed, let's get the pointer on the actual xml
                readIn = findEntry(in, baseName + ".xml");
                if (readIn == null) {
                    in.close();
                    return false;
                }
            } catch (IOException e) {
                in.close();
                throw e;
            }

        }

        try {
            reader.parse(readIn);
        } finally {
            readIn.close();
        }

        return true;
    }

    private UpdateSite loadSite(URI repoUri) throws IOException, SAXException {
        URI siteUri = normalizeSiteUri(repoUri, null);
        URL u = siteUri.resolve("site.xml").toURL();

        final URLResource res = new URLResource(u, this.timeoutConstraint);
        ArtifactDownloadReport report = repositoryCacheManager.downloadRepositoryResource(res,
            "site", "updatesite", "xml", options, urlRepository);
        if (report.getDownloadStatus() == DownloadStatus.FAILED) {
            return null;
        }
        try (InputStream in = new FileInputStream(report.getLocalFile())) {
            UpdateSite site = EclipseUpdateSiteParser.parse(in);
            site.setUri(normalizeSiteUri(site.getUri(), siteUri));
            return site;
        }
    }

    private URI normalizeSiteUri(URI uri, URI defaultValue) {
        if (uri == null) {
            return defaultValue;
        }
        String uriString = uri.toString();
        if (uriString.endsWith("site.xml")) {
            try {
                return new URI(uriString.substring(0, uriString.length() - 8));
            } catch (URISyntaxException e) {
                throw new RuntimeException("Illegal uri", e);
            }
        }
        if (!uriString.endsWith("/")) {
            try {
                return new URI(uriString + "/");
            } catch (URISyntaxException e) {
                throw new RuntimeException("Illegal uri", e);
            }
        }
        return uri;
    }

    private UpdateSiteDescriptor loadFromDigest(UpdateSite site) throws IOException,
            SAXException {
        URI digestBaseUri = site.getDigestUri();
        if (digestBaseUri == null) {
            digestBaseUri = site.getUri();
        } else if (!digestBaseUri.isAbsolute()) {
            digestBaseUri = site.getUri().resolve(digestBaseUri);
        }
        URL digest = digestBaseUri.resolve("digest.zip").toURL();
        Message.verbose("\tReading " + digest);

        final URLResource res = new URLResource(digest, this.timeoutConstraint);
        ArtifactDownloadReport report = repositoryCacheManager.downloadRepositoryResource(res,
            "digest", "digest", "zip", options, urlRepository);
        if (report.getDownloadStatus() == DownloadStatus.FAILED) {
            return null;
        }
        try (InputStream in = new FileInputStream(report.getLocalFile())) {
            ZipInputStream zipped = findEntry(in, "digest.xml");
            if (zipped == null) {
                return null;
            }
            return UpdateSiteDigestParser.parse(zipped, site);
        }
    }

    private UpdateSiteDescriptor loadFromSite(UpdateSite site) throws IOException, SAXException {
        UpdateSiteDescriptor repoDescriptor = new UpdateSiteDescriptor(site.getUri(),
                ExecutionEnvironmentProfileProvider.getInstance());

        for (EclipseFeature feature : site.getFeatures()) {
            URL url = site.getUri().resolve(feature.getUrl()).toURL();

            final URLResource res = new URLResource(url, this.timeoutConstraint);
            ArtifactDownloadReport report = repositoryCacheManager.downloadRepositoryResource(res,
                feature.getId(), "feature", "jar", options, urlRepository);
            if (report.getDownloadStatus() == DownloadStatus.FAILED) {
                return null;
            }
            try (InputStream in = new FileInputStream(report.getLocalFile())) {
                ZipInputStream zipped = findEntry(in, "feature.xml");
                if (zipped == null) {
                    return null;
                }
                EclipseFeature f = FeatureParser.parse(zipped);
                f.setURL(feature.getUrl());
                repoDescriptor.addFeature(f);
            }
        }

        return repoDescriptor;
    }

    private ZipInputStream findEntry(InputStream in, String entryName) throws IOException {
        ZipInputStream zipped = new ZipInputStream(in);
        ZipEntry zipEntry = zipped.getNextEntry();
        while (zipEntry != null && !zipEntry.getName().equals(entryName)) {
            zipEntry = zipped.getNextEntry();
        }
        if (zipEntry == null) {
            return null;
        }
        return zipped;
    }
}