/*
 *  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.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.Iterator;
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.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;

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

    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.verbose("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());
        if (!populateP2Descriptor(repoUri, p2Descriptor)) {
            return null;
        }
        return p2Descriptor;
    }

    private boolean populateP2Descriptor(URI repoUri, P2Descriptor p2Descriptor)
            throws IOException, ParseException, SAXException {
        Message.verbose("Loading P2 repository " + repoUri);

        boolean artifactExists = readComposite(repoUri, "compositeArtifacts", p2Descriptor);
        if (!artifactExists) {
            artifactExists = readJarOrXml(repoUri, "artifacts", new P2ArtifactParser(p2Descriptor,
                    repoUri.toURL().toExternalForm()));
        }

        boolean contentExists = readComposite(repoUri, "compositeContent", p2Descriptor);
        if (!contentExists) {
            contentExists = readJarOrXml(repoUri, "content", new P2MetadataParser(p2Descriptor));
        }

        return artifactExists || contentExists;
    }

    private boolean readComposite(URI repoUri, String name, P2Descriptor p2Descriptor)
            throws IOException, ParseException, SAXException {
        P2CompositeParser p2CompositeParser = new P2CompositeParser();
        boolean exist = readJarOrXml(repoUri, name, p2CompositeParser);
        if (exist) {
            Iterator itChildLocation = p2CompositeParser.getChildLocations().iterator();
            while (itChildLocation.hasNext()) {
                String childLocation = (String) itChildLocation.next();
                if (!childLocation.endsWith("/")) {
                    childLocation += "/";
                }
                URI childUri = repoUri.resolve(childLocation);
                populateP2Descriptor(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);

        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);

            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, ParseException, SAXException {
        URI siteUri = normalizeSiteUri(repoUri, null);
        URL u = siteUri.resolve("site.xml").toURL();

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

    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,
            ParseException, 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);

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

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

        Iterator itFeatures = site.getFeatures().iterator();
        while (itFeatures.hasNext()) {
            EclipseFeature feature = (EclipseFeature) itFeatures.next();
            URL url = site.getUri().resolve(feature.getUrl()).toURL();

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

        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;
    }
}
