/*
 * 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.maven.archetype.common;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.IOUtils;
import org.apache.maven.archetype.downloader.DownloadException;
import org.apache.maven.archetype.downloader.DownloadNotFoundException;
import org.apache.maven.archetype.downloader.Downloader;
import org.apache.maven.archetype.exception.UnknownArchetype;
import org.apache.maven.archetype.metadata.ArchetypeDescriptor;
import org.apache.maven.archetype.metadata.io.xpp3.ArchetypeDescriptorXpp3Reader;
import org.apache.maven.archetype.old.descriptor.ArchetypeDescriptorBuilder;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.model.Model;
import org.apache.maven.project.ProjectBuildingRequest;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;

@Component(role = ArchetypeArtifactManager.class)
public class DefaultArchetypeArtifactManager extends AbstractLogEnabled implements ArchetypeArtifactManager {
    @Requirement
    private Downloader downloader;

    @Requirement
    private PomManager pomManager;

    private Map<String, File> archetypeCache = new TreeMap<>();

    @Override
    public File getArchetypeFile(
            final String groupId,
            final String artifactId,
            final String version,
            ArtifactRepository archetypeRepository,
            final ArtifactRepository localRepository,
            final List<ArtifactRepository> repositories,
            ProjectBuildingRequest buildingRequest)
            throws UnknownArchetype {
        try {
            File archetype = getArchetype(groupId, artifactId, version);

            if (archetype == null) {
                archetype = downloader.download(
                        groupId,
                        artifactId,
                        version,
                        archetypeRepository,
                        localRepository,
                        repositories,
                        buildingRequest);

                setArchetype(groupId, artifactId, version, archetype);
            }
            return archetype;
        } catch (DownloadNotFoundException ex) {
            throw new UnknownArchetype(ex);
        } catch (DownloadException ex) {
            throw new UnknownArchetype(ex);
        }
    }

    @Override
    public ClassLoader getArchetypeJarLoader(File archetypeFile) throws UnknownArchetype {
        try {
            URL[] urls = new URL[1];

            urls[0] = archetypeFile.toURI().toURL();

            return new URLClassLoader(urls);
        } catch (MalformedURLException e) {
            throw new UnknownArchetype(e);
        }
    }

    @Override
    public Model getArchetypePom(File jar) throws XmlPullParserException, UnknownArchetype, IOException {

        try (ZipFile zipFile = getArchetypeZipFile(jar)) {
            String pomFileName = null;

            Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
            while (enumeration.hasMoreElements()) {
                ZipEntry el = enumeration.nextElement();

                String entry = el.getName();
                if (entry.startsWith("META-INF") && entry.endsWith("pom.xml")) {
                    pomFileName = entry;
                }
            }

            if (pomFileName == null) {
                return null;
            }

            ZipEntry pom = zipFile.getEntry(pomFileName);

            if (pom == null) {
                return null;
            }
            return pomManager.readPom(zipFile.getInputStream(pom));
        }
    }

    @Override
    public ZipFile getArchetypeZipFile(File archetypeFile) throws UnknownArchetype {
        try {
            return new ZipFile(archetypeFile);
        } catch (IOException e) {
            throw new UnknownArchetype(e);
        }
    }

    @Override
    public boolean isFileSetArchetype(File archetypeFile) {
        getLogger().debug("checking fileset archetype status on " + archetypeFile);

        try (ZipFile zipFile = getArchetypeZipFile(archetypeFile)) {
            return isFileSetArchetype(zipFile);
        } catch (IOException | UnknownArchetype e) {
            getLogger().debug(e.toString());
            return false;
        }
    }

    @Override
    public boolean isFileSetArchetype(
            String groupId,
            String artifactId,
            String version,
            ArtifactRepository archetypeRepository,
            ArtifactRepository localRepository,
            List<ArtifactRepository> repositories,
            ProjectBuildingRequest buildingRequest) {
        try {
            File archetypeFile = getArchetypeFile(
                    groupId, artifactId, version, archetypeRepository, localRepository, repositories, buildingRequest);

            return isFileSetArchetype(archetypeFile);
        } catch (UnknownArchetype e) {
            getLogger().debug(e.toString());
            return false;
        }
    }

    @Override
    public boolean isOldArchetype(File archetypeFile) {
        getLogger().debug("checking old archetype status on " + archetypeFile);

        try (ZipFile zipFile = getArchetypeZipFile(archetypeFile)) {
            return isOldArchetype(zipFile);
        } catch (IOException | UnknownArchetype e) {
            getLogger().debug(e.toString());
            return false;
        }
    }

    @Override
    public boolean isOldArchetype(
            String groupId,
            String artifactId,
            String version,
            ArtifactRepository archetypeRepository,
            ArtifactRepository localRepository,
            List<ArtifactRepository> repositories,
            ProjectBuildingRequest buildingRequest) {
        try {
            File archetypeFile = getArchetypeFile(
                    groupId, artifactId, version, archetypeRepository, localRepository, repositories, buildingRequest);

            return isOldArchetype(archetypeFile);
        } catch (UnknownArchetype e) {
            getLogger().debug(e.toString());
            return false;
        }
    }

    @Override
    public boolean exists(
            String archetypeGroupId,
            String archetypeArtifactId,
            String archetypeVersion,
            ArtifactRepository archetypeRepository,
            ArtifactRepository localRepository,
            List<ArtifactRepository> remoteRepositories,
            ProjectBuildingRequest buildingRequest) {
        try {
            File archetype = getArchetype(archetypeGroupId, archetypeArtifactId, archetypeVersion);
            if (archetype == null) {
                archetype = downloader.download(
                        archetypeGroupId,
                        archetypeArtifactId,
                        archetypeVersion,
                        archetypeRepository,
                        localRepository,
                        remoteRepositories,
                        buildingRequest);
                setArchetype(archetypeGroupId, archetypeArtifactId, archetypeVersion, archetype);
            }

            return archetype.exists();
        } catch (DownloadException e) {
            getLogger()
                    .debug(
                            "Archetype " + archetypeGroupId + ":" + archetypeArtifactId + ":" + archetypeVersion
                                    + " doesn't exist",
                            e);
            return false;
        } catch (DownloadNotFoundException e) {
            getLogger()
                    .debug(
                            "Archetype " + archetypeGroupId + ":" + archetypeArtifactId + ":" + archetypeVersion
                                    + " doesn't exist",
                            e);
            return false;
        }
    }

    @Override
    public String getPostGenerationScript(File archetypeFile) throws UnknownArchetype {
        try (ZipFile zipFile = getArchetypeZipFile(archetypeFile)) {
            Reader reader = getDescriptorReader(zipFile, Constants.ARCHETYPE_POST_GENERATION_SCRIPT);
            return reader == null ? null : IOUtils.toString(reader);
        } catch (IOException e) {
            throw new UnknownArchetype(e);
        }
    }

    @Override
    public ArchetypeDescriptor getFileSetArchetypeDescriptor(File archetypeFile) throws UnknownArchetype {
        try (ZipFile zipFile = getArchetypeZipFile(archetypeFile)) {
            return loadFileSetArchetypeDescriptor(zipFile);
        } catch (XmlPullParserException | IOException e) {
            throw new UnknownArchetype(e);
        }
    }

    @Override
    public org.apache.maven.archetype.metadata.ArchetypeDescriptor getFileSetArchetypeDescriptor(
            String groupId,
            String artifactId,
            String version,
            ArtifactRepository archetypeRepository,
            ArtifactRepository localRepository,
            List<ArtifactRepository> repositories,
            ProjectBuildingRequest buildingRequest)
            throws UnknownArchetype {
        File archetypeFile = getArchetypeFile(
                groupId, artifactId, version, archetypeRepository, localRepository, repositories, buildingRequest);

        return getFileSetArchetypeDescriptor(archetypeFile);
    }

    @Override
    public List<String> getFilesetArchetypeResources(File archetypeFile) throws UnknownArchetype {
        getLogger().debug("getFilesetArchetypeResources( \"" + archetypeFile.getAbsolutePath() + "\" )");
        List<String> archetypeResources = new ArrayList<>();

        try (ZipFile zipFile = getArchetypeZipFile(archetypeFile)) {
            Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
            while (enumeration.hasMoreElements()) {
                ZipEntry entry = enumeration.nextElement();

                if (entry.getName().startsWith(Constants.ARCHETYPE_RESOURCES)) {
                    // not supposed to be file.separator
                    String resource = entry.getName().substring(Constants.ARCHETYPE_RESOURCES.length() + 1);
                    getLogger().debug("  - found resource (" + Constants.ARCHETYPE_RESOURCES + "/)" + resource);
                    // TODO:FIXME
                    archetypeResources.add(resource);
                } else {
                    getLogger().debug("  - ignored resource " + entry.getName());
                }
            }
            return archetypeResources;
        } catch (IOException e) {
            throw new UnknownArchetype(e);
        }
    }

    @Override
    public org.apache.maven.archetype.old.descriptor.ArchetypeDescriptor getOldArchetypeDescriptor(File archetypeFile)
            throws UnknownArchetype {
        try (ZipFile zipFile = getArchetypeZipFile(archetypeFile)) {
            return loadOldArchetypeDescriptor(zipFile);
        } catch (XmlPullParserException | IOException e) {
            throw new UnknownArchetype(e);
        }
    }

    @Override
    public org.apache.maven.archetype.old.descriptor.ArchetypeDescriptor getOldArchetypeDescriptor(
            String groupId,
            String artifactId,
            String version,
            ArtifactRepository archetypeRepository,
            ArtifactRepository localRepository,
            List<ArtifactRepository> repositories,
            ProjectBuildingRequest buildingRequest)
            throws UnknownArchetype {
        File archetypeFile = getArchetypeFile(
                groupId, artifactId, version, archetypeRepository, localRepository, repositories, buildingRequest);

        return getOldArchetypeDescriptor(archetypeFile);
    }

    private File getArchetype(String archetypeGroupId, String archetypeArtifactId, String archetypeVersion) {
        String key = archetypeGroupId + ":" + archetypeArtifactId + ":" + archetypeVersion;

        if (archetypeCache.containsKey(key)) {
            getLogger().debug("Found archetype " + key + " in cache: " + archetypeCache.get(key));

            return archetypeCache.get(key);
        }

        getLogger().debug("Not found archetype " + key + " in cache");
        return null;
    }

    private void setArchetype(
            String archetypeGroupId, String archetypeArtifactId, String archetypeVersion, File archetype) {
        String key = archetypeGroupId + ":" + archetypeArtifactId + ":" + archetypeVersion;

        archetypeCache.put(key, archetype);
    }

    private boolean isFileSetArchetype(ZipFile zipFile) throws IOException {
        try (Reader reader = getArchetypeDescriptorReader(zipFile); ) {
            return (reader != null);
        }
    }

    private boolean isOldArchetype(ZipFile zipFile) throws IOException {
        try (Reader reader = getOldArchetypeDescriptorReader(zipFile)) {
            return (reader != null);
        }
    }

    private org.apache.maven.archetype.metadata.ArchetypeDescriptor loadFileSetArchetypeDescriptor(ZipFile zipFile)
            throws IOException, XmlPullParserException {

        try (Reader reader = getArchetypeDescriptorReader(zipFile)) {
            if (reader == null) {
                return null;
            }

            ArchetypeDescriptorXpp3Reader archetypeReader = new ArchetypeDescriptorXpp3Reader();
            return archetypeReader.read(reader, false);
        } catch (IOException | XmlPullParserException e) {
            throw e;
        }
    }

    private org.apache.maven.archetype.old.descriptor.ArchetypeDescriptor loadOldArchetypeDescriptor(ZipFile zipFile)
            throws IOException, XmlPullParserException {
        try (Reader reader = getOldArchetypeDescriptorReader(zipFile)) {
            if (reader == null) {
                return null;
            }

            ArchetypeDescriptorBuilder builder = new ArchetypeDescriptorBuilder();
            return builder.build(reader);
        } catch (IOException | XmlPullParserException ex) {
            throw ex;
        }
    }

    private Reader getArchetypeDescriptorReader(ZipFile zipFile) throws IOException {
        return getDescriptorReader(zipFile, Constants.ARCHETYPE_DESCRIPTOR);
    }

    private Reader getOldArchetypeDescriptorReader(ZipFile zipFile) throws IOException {
        Reader reader = getDescriptorReader(zipFile, Constants.OLD_ARCHETYPE_DESCRIPTOR);

        if (reader == null) {
            reader = getDescriptorReader(zipFile, Constants.OLDER_ARCHETYPE_DESCRIPTOR);
        }

        return reader;
    }

    private Reader getDescriptorReader(ZipFile zipFile, String descriptor) throws IOException {
        ZipEntry entry = searchEntry(zipFile, descriptor);

        if (entry == null) {
            return null;
        }

        InputStream is = zipFile.getInputStream(entry);

        if (is == null) {
            throw new IOException("The " + descriptor + " descriptor cannot be read in " + zipFile.getName() + ".");
        }

        return ReaderFactory.newReader(is, ReaderFactory.UTF_8);
    }

    private ZipEntry searchEntry(ZipFile zipFile, String searchString) {
        getLogger().debug("Searching for " + searchString + " inside " + zipFile.getName());

        Enumeration<? extends ZipEntry> enu = zipFile.entries();
        while (enu.hasMoreElements()) {
            ZipEntry entryfound = enu.nextElement();
            getLogger().debug("  - " + entryfound.getName());

            if (searchString.equals(entryfound.getName())) {
                getLogger().debug("Entry found");
                return entryfound;
            }
        }
        return null;
    }
}
