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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.ivy.core.IvyPatternHelper;
import org.apache.ivy.core.cache.ArtifactOrigin;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.DefaultArtifact;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.report.DownloadReport;
import org.apache.ivy.core.resolve.DownloadOptions;
import org.apache.ivy.core.resolve.ResolveData;
import org.apache.ivy.core.resolve.ResolvedModuleRevision;
import org.apache.ivy.core.search.ModuleEntry;
import org.apache.ivy.core.search.OrganisationEntry;
import org.apache.ivy.core.search.RevisionEntry;
import org.apache.ivy.plugins.matcher.PatternMatcher;
import org.apache.ivy.plugins.repository.Repository;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.plugins.resolver.util.ResolvedResource;
import org.apache.ivy.util.ContextualSAXHandler;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.XMLHelper;
import org.xml.sax.SAXException;

/**
 * IBiblioResolver is a resolver which can be used to resolve dependencies found in the ibiblio
 * maven repository, or similar repositories.
 * <p>
 * For more flexibility with url and patterns, see
 * {@link org.apache.ivy.plugins.resolver.URLResolver}.
 */
public class IBiblioResolver extends URLResolver {
    private static final String M2_PER_MODULE_PATTERN 
                                    = "[revision]/[artifact]-[revision](-[classifier]).[ext]";
    private static final String M2_PATTERN  = "[organisation]/[module]/" + M2_PER_MODULE_PATTERN;

    public static final String DEFAULT_PATTERN = "[module]/[type]s/[artifact]-[revision].[ext]";

    public static final String DEFAULT_ROOT = "http://www.ibiblio.org/maven/";
    public static final String DEFAULT_M2_ROOT = "http://repo1.maven.org/maven2/";

    private String root = null;

    private String pattern = null;

    // use poms if m2 compatible is true
    private boolean usepoms = true;
    
    // use maven-metadata.xml is exists to list revisions
    private boolean useMavenMetadata = true;

    public IBiblioResolver() {
        // SNAPSHOT revisions are changing revisions
        setChangingMatcher(PatternMatcher.REGEXP);
        setChangingPattern(".*-SNAPSHOT");
    }

    public ResolvedResource findIvyFileRef(DependencyDescriptor dd, ResolveData data) {
        if (isM2compatible() && isUsepoms()) {
            ModuleRevisionId mrid = dd.getDependencyRevisionId();
            mrid = convertM2IdForResourceSearch(mrid);
            
            ResolvedResource rres = null;
            if (dd.getDependencyRevisionId().getRevision().endsWith("SNAPSHOT")) {
                rres = findSnapshotDescriptor(dd, data, mrid);
                if (rres != null) {
                    return rres;
                }
            }
            
            rres = findResourceUsingPatterns(mrid, getIvyPatterns(),
                DefaultArtifact.newPomArtifact(mrid, data.getDate()), getRMDParser(dd, data), data
                        .getDate());
            return rres;
        } else {
            return null;
        }
    }

    public ResolvedResource findArtifactRef(Artifact artifact, Date date) {
        ensureConfigured(getSettings());
        ModuleRevisionId mrid = artifact.getModuleRevisionId();
        if (isM2compatible()) {
            mrid = convertM2IdForResourceSearch(mrid);
        }
        ResolvedResource rres = null;
        if (artifact.getId().getRevision().endsWith("SNAPSHOT") && isM2compatible()) {
            rres = findSnapshotArtifact(artifact, date, mrid);            
            if (rres != null) {
                return rres;
            }
        }
        return findResourceUsingPatterns(mrid, getArtifactPatterns(), artifact,
            getDefaultRMDParser(artifact.getModuleRevisionId().getModuleId()), date);
    }

    private ResolvedResource findSnapshotArtifact(Artifact artifact, Date date,
            ModuleRevisionId mrid) {
        String rev = findSnapshotVersion(mrid);
        if (rev != null) {
            // replace the revision token in file name with the resolved revision
            String pattern = getWholePattern().replaceFirst("\\-\\[revision\\]", "-" + rev);
            return findResourceUsingPattern(mrid, pattern, artifact,
                getDefaultRMDParser(artifact.getModuleRevisionId().getModuleId()), date);
        }
        return null;
    }

    private ResolvedResource findSnapshotDescriptor(DependencyDescriptor dd, ResolveData data,
            ModuleRevisionId mrid) {
        String rev = findSnapshotVersion(mrid);
        if (rev != null) {
            // here it would be nice to be able to store the resolved snapshot version, to avoid
            // having to follow the same process to download artifacts
            
            Message.verbose("[" + rev + "] " + mrid);

            // replace the revision token in file name with the resolved revision
            String pattern = getWholePattern().replaceFirst("\\-\\[revision\\]", "-" + rev);
            return findResourceUsingPattern(mrid, pattern,
                DefaultArtifact.newPomArtifact(
                    mrid, data.getDate()), getRMDParser(dd, data), data.getDate());
        }
        return null;
    }
    
    private String findSnapshotVersion(ModuleRevisionId mrid) {
        if (!isM2compatible()) {
            return null;
        }
        
        if (shouldUseMavenMetadata(getWholePattern())) {
            InputStream metadataStream = null;
            try {
                String metadataLocation = IvyPatternHelper.substitute(
                    root + "[organisation]/[module]/[revision]/maven-metadata.xml", mrid);
                Resource metadata = getRepository().getResource(metadataLocation);
                if (metadata.exists()) {
                    metadataStream = metadata.openStream();
                    final StringBuffer timestamp = new StringBuffer();
                    final StringBuffer buildNumer = new StringBuffer();
                    XMLHelper.parse(metadataStream, null, new ContextualSAXHandler() {
                        public void endElement(String uri, String localName, String qName) 
                                throws SAXException {
                            if ("metadata/versioning/snapshot/timestamp".equals(getContext())) {
                                timestamp.append(getText());
                            }
                            if ("metadata/versioning/snapshot/buildNumber"
                                    .equals(getContext())) {
                                buildNumer.append(getText());
                            }
                            super.endElement(uri, localName, qName);
                        }
                    }, null);
                    if (timestamp.length() > 0) {
                        // we have found a timestamp, so this is a snapshot unique version
                        String rev = mrid.getRevision();
                        rev = rev.substring(0, rev.length() - "SNAPSHOT".length());
                        rev = rev + timestamp.toString() + "-" + buildNumer.toString();
                        
                        return rev;
                    }
                } else {
                    Message.verbose("\tmaven-metadata not available: " + metadata);
                }
            } catch (IOException e) {
                Message.verbose(
                    "impossible to access maven metadata file, ignored: " + e.getMessage());
            } catch (SAXException e) {
                Message.verbose(
                    "impossible to parse maven metadata file, ignored: " + e.getMessage());
            } catch (ParserConfigurationException e) {
                Message.verbose(
                    "impossible to parse maven metadata file, ignored: " + e.getMessage());
            } finally {
                if (metadataStream != null) {
                    try {
                        metadataStream.close();
                    } catch (IOException e) {
                        // ignored
                    }
                }
            }
        }
        return null;
    }

    public void setM2compatible(boolean m2compatible) {
        super.setM2compatible(m2compatible);
        if (m2compatible) {
            if (root == null) {
                root = DEFAULT_M2_ROOT;
            }
            if (pattern == null) {
                pattern = M2_PATTERN;
            }
            updateWholePattern();
        }
    }

    public void ensureConfigured(ResolverSettings settings) {
        if (settings != null && (root == null || pattern == null)) {
            if (root == null) {
                String root = settings.getVariable("ivy.ibiblio.default.artifact.root");
                if (root != null) {
                    this.root = root;
                } else {
                    settings.configureRepositories(true);
                    this.root = settings.getVariable("ivy.ibiblio.default.artifact.root");
                }
            }
            if (pattern == null) {
                String pattern = settings.getVariable("ivy.ibiblio.default.artifact.pattern");
                if (pattern != null) {
                    this.pattern = pattern;
                } else {
                    settings.configureRepositories(false);
                    this.pattern = settings.getVariable("ivy.ibiblio.default.artifact.pattern");
                }
            }
            updateWholePattern();
        }
    }

    protected String getModuleDescriptorExtension() {
        return "pom";
    }
    
    private String getWholePattern() {
        return root + pattern;
    }

    public String getPattern() {
        return pattern;
    }

    public void setPattern(String pattern) {
        if (pattern == null) {
            throw new NullPointerException("pattern must not be null");
        }
        this.pattern = pattern;
        ensureConfigured(getSettings());
        updateWholePattern();
    }

    public String getRoot() {
        return root;
    }

    /**
     * Sets the root of the maven like repository. The maven like repository is necessarily an http
     * repository.
     * 
     * @param root
     *            the root of the maven like repository
     * @throws IllegalArgumentException
     *             if root does not start with "http://"
     */
    public void setRoot(String root) {
        if (root == null) {
            throw new NullPointerException("root must not be null");
        }
        if (!root.endsWith("/")) {
            this.root = root + "/";
        } else {
            this.root = root;
        }
        ensureConfigured(getSettings());
        updateWholePattern();
    }

    private void updateWholePattern() {
        if (isM2compatible() && isUsepoms()) {
            setIvyPatterns(Collections.singletonList(getWholePattern()));
        } else {
            setIvyPatterns(Collections.EMPTY_LIST);
        }
        setArtifactPatterns(Collections.singletonList(getWholePattern()));
    }

    public void publish(Artifact artifact, File src) {
        throw new UnsupportedOperationException("publish not supported by IBiblioResolver");
    }

    // we do not allow to list organisations on ibiblio, nor modules in ibiblio 1
    public String[] listTokenValues(String token, Map otherTokenValues) {
        if (IvyPatternHelper.ORGANISATION_KEY.equals(token)) {
            return new String[0];
        }
        if (IvyPatternHelper.MODULE_KEY.equals(token) && !isM2compatible()) {
            return new String[0];
        }
        ensureConfigured(getSettings());
        return super.listTokenValues(token, otherTokenValues);
    }
    
    protected String[] listTokenValues(String pattern, String token) {
        if (IvyPatternHelper.ORGANISATION_KEY.equals(token)) {
            return new String[0];
        }
        if (IvyPatternHelper.MODULE_KEY.equals(token) && !isM2compatible()) {
            return new String[0];
        }
        ensureConfigured(getSettings());
        
        // let's see if we should use maven metadata for this listing...
        if (IvyPatternHelper.REVISION_KEY.equals(token) 
                && shouldUseMavenMetadata(getWholePattern())) {
            // now we must use metadata if available
            /*
             * we substitute tokens with ext token only in the m2 per module pattern, to match
             * has has been done in the given pattern
             */
            String partiallyResolvedM2PerModulePattern = IvyPatternHelper.substituteTokens(
                M2_PER_MODULE_PATTERN, 
                Collections.singletonMap(IvyPatternHelper.EXT_KEY, "pom"));
            if (pattern.endsWith(partiallyResolvedM2PerModulePattern)) {
                /*
                 * the given pattern already contain resolved org and module, we just have to
                 * replace the per module pattern at the end by 'maven-metadata.xml' to have the
                 * maven metadata file location
                 */
                String metadataLocation = pattern.substring(0, pattern
                    .lastIndexOf(partiallyResolvedM2PerModulePattern))
                    + "maven-metadata.xml";
                List revs = listRevisionsWithMavenMetadata(getRepository(), metadataLocation);
                if (revs != null) {
                    return (String[]) revs.toArray(new String[revs.size()]);
                }
            } else {
                /*
                 * this is probably because the given pattern has been substituted with jar ext,
                 * if this resolver has optional module descriptors. But since we have to use
                 * maven metadata, we don't care about this case, maven metadata has already
                 * been used when looking for revisions with the pattern substituted with
                 * ext=xml for the "ivy" pattern.
                 */  
                return new String[0];
            }
        }
        return super.listTokenValues(pattern, token);
    }
    
    public OrganisationEntry[] listOrganisations() {
        return new OrganisationEntry[0];
    }

    public ModuleEntry[] listModules(OrganisationEntry org) {
        if (isM2compatible()) {
            ensureConfigured(getSettings());
            return super.listModules(org);
        }
        return new ModuleEntry[0];
    }

    public RevisionEntry[] listRevisions(ModuleEntry mod) {
        ensureConfigured(getSettings());
        return super.listRevisions(mod);
    }
    
    protected ResolvedResource[] listResources(
            Repository repository, ModuleRevisionId mrid, String pattern, Artifact artifact) {
        if (shouldUseMavenMetadata(pattern)) {
            List revs = listRevisionsWithMavenMetadata(
                repository, mrid.getModuleId().getAttributes());
            if (revs != null) {
                Message.debug("\tfound revs: " + revs);
                List rres = new ArrayList();
                for (Iterator iter = revs.iterator(); iter.hasNext();) {
                    String rev = (String) iter.next();
                    String resolvedPattern = IvyPatternHelper.substitute(
                        pattern, ModuleRevisionId.newInstance(mrid, rev), artifact);
                    try {
                        Resource res = repository.getResource(resolvedPattern);
                        if ((res != null) && res.exists()) {
                            rres.add(new ResolvedResource(res, rev));
                        }
                    } catch (IOException e) {
                        Message.warn(
                            "impossible to get resource from name listed by maven-metadata.xml:"
                            + rres + ": " + e.getMessage());
                    }
                }
                return (ResolvedResource[]) rres.toArray(new ResolvedResource[rres.size()]);
            } else {
                // maven metadata not available or something went wrong, 
                // use default listing capability
                return super.listResources(repository, mrid, pattern, artifact);
            }
        } else {
            return super.listResources(repository, mrid, pattern, artifact);
        }
    }

    private List listRevisionsWithMavenMetadata(Repository repository, Map tokenValues) {
        String metadataLocation = IvyPatternHelper.substituteTokens(
            root + "[organisation]/[module]/maven-metadata.xml", tokenValues);
        return listRevisionsWithMavenMetadata(repository, metadataLocation);
    }

    private List listRevisionsWithMavenMetadata(Repository repository, String metadataLocation) {
        List revs = null;
        InputStream metadataStream = null;
        try {
            Resource metadata = repository.getResource(metadataLocation);
            if (metadata.exists()) {
                Message.verbose("\tlisting revisions from maven-metadata: " + metadata);
                final List metadataRevs = new ArrayList();
                metadataStream = metadata.openStream();
                XMLHelper.parse(metadataStream, null, new ContextualSAXHandler() {
                    public void endElement(String uri, String localName, String qName) 
                            throws SAXException {
                        if ("metadata/versioning/versions/version".equals(getContext())) {
                            metadataRevs.add(getText().trim());
                        }
                        super.endElement(uri, localName, qName);
                    }
                }, null);
                revs = metadataRevs;
            } else {
                Message.verbose("\tmaven-metadata not available: " + metadata);
            }
        } catch (IOException e) {
            Message.verbose(
                "impossible to access maven metadata file, ignored: " + e.getMessage());
        } catch (SAXException e) {
            Message.verbose(
                "impossible to parse maven metadata file, ignored: " + e.getMessage());
        } catch (ParserConfigurationException e) {
            Message.verbose(
                "impossible to parse maven metadata file, ignored: " + e.getMessage());
        } finally {
            if (metadataStream != null) {
                try {
                    metadataStream.close();
                } catch (IOException e) {
                    // ignored
                }
            }
        }
        return revs;
    }
    
    protected void findTokenValues(Collection names, List patterns, Map tokenValues, String token) {
        if (IvyPatternHelper.REVISION_KEY.equals(token)) {
            if (shouldUseMavenMetadata(getWholePattern())) {
                List revs = listRevisionsWithMavenMetadata(getRepository(), tokenValues);
                if (revs != null) {
                    names.addAll(filterNames(revs));
                    return;
                }
            }
        }
        super.findTokenValues(names, patterns, tokenValues, token);
    }

    private boolean shouldUseMavenMetadata(String pattern) {
        return isUseMavenMetadata() && isM2compatible() && pattern.endsWith(M2_PATTERN);
    }


    public String getTypeName() {
        return "ibiblio";
    }
    
    

    // override some methods to ensure configuration
    public ResolvedModuleRevision getDependency(DependencyDescriptor dd, ResolveData data)
            throws ParseException {
        ensureConfigured(data.getSettings());
        return super.getDependency(dd, data);
    }

    public DownloadReport download(Artifact[] artifacts, DownloadOptions options) {
        ensureConfigured(getSettings());
        return super.download(artifacts, options);
    }

    public boolean exists(Artifact artifact) {
        ensureConfigured(getSettings());
        return super.exists(artifact);
    }
    
    public ArtifactOrigin locate(Artifact artifact) {
        ensureConfigured(getSettings());
        return super.locate(artifact);
    }

    public List getArtifactPatterns() {
        ensureConfigured(getSettings());
        return super.getArtifactPatterns();
    }

    public boolean isUsepoms() {
        return usepoms;
    }

    public void setUsepoms(boolean usepoms) {
        this.usepoms = usepoms;
        updateWholePattern();
    }

    public boolean isUseMavenMetadata() {
        return useMavenMetadata;
    }

    public void setUseMavenMetadata(boolean useMavenMetadata) {
        this.useMavenMetadata = useMavenMetadata;
    }

    public void dumpSettings() {
        ensureConfigured(getSettings());
        super.dumpSettings();
        Message.debug("\t\troot: " + getRoot());
        Message.debug("\t\tpattern: " + getPattern());
        Message.debug("\t\tusepoms: " + usepoms);
        Message.debug("\t\tuseMavenMetadata: " + useMavenMetadata);
    }
}
