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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.Map;
import java.util.Properties;

import org.apache.ivy.core.IvyPatternHelper;
import org.apache.ivy.core.RelativeUrlResolver;
import org.apache.ivy.core.module.descriptor.ExtendsDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.module.status.StatusManager;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.core.settings.TimeoutConstraint;
import org.apache.ivy.plugins.IvySettingsAware;
import org.apache.ivy.plugins.conflict.ConflictManager;
import org.apache.ivy.plugins.matcher.PatternMatcher;
import org.apache.ivy.plugins.namespace.Namespace;
import org.apache.ivy.plugins.parser.ModuleDescriptorParser;
import org.apache.ivy.plugins.parser.ParserSettings;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorParser;
import org.apache.ivy.plugins.resolver.DependencyResolver;
import org.apache.ivy.util.FileUtil;

public class DefaultResolutionCacheManager implements ResolutionCacheManager, IvySettingsAware {

    private static final String DEFAULT_CACHE_RESOLVED_IVY_PATTERN = "resolved-[organisation]-[module]-[revision].xml";

    private static final String DEFAULT_CACHE_RESOLVED_IVY_PROPERTIES_PATTERN = "resolved-[organisation]-[module]-[revision].properties";

    private String resolvedIvyPattern = DEFAULT_CACHE_RESOLVED_IVY_PATTERN;

    private String resolvedIvyPropertiesPattern = DEFAULT_CACHE_RESOLVED_IVY_PROPERTIES_PATTERN;

    private File basedir;

    private String name = "resolution-cache";

    private IvySettings settings;

    public DefaultResolutionCacheManager() {
    }

    public DefaultResolutionCacheManager(File basedir) {
        setBasedir(basedir);
    }

    public void setSettings(IvySettings settings) {
        this.settings = settings;
    }

    public File getResolutionCacheRoot() {
        if (basedir == null) {
            if (settings == null) {
                throw new IllegalStateException("The 'basedir' or 'IvySettings' has not been set on the ResolutionCacheManager");
            }
            basedir = settings.getDefaultResolutionCacheBasedir();
        }
        return basedir;
    }

    public File getBasedir() {
        return basedir;
    }

    public void setBasedir(File basedir) {
        this.basedir = basedir;
    }

    public String getResolvedIvyPattern() {
        return resolvedIvyPattern;
    }

    public void setResolvedIvyPattern(String cacheResolvedIvyPattern) {
        this.resolvedIvyPattern = cacheResolvedIvyPattern;
    }

    public String getResolvedIvyPropertiesPattern() {
        return resolvedIvyPropertiesPattern;
    }

    public void setResolvedIvyPropertiesPattern(String cacheResolvedIvyPropertiesPattern) {
        this.resolvedIvyPropertiesPattern = cacheResolvedIvyPropertiesPattern;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public File getResolvedIvyFileInCache(ModuleRevisionId mrid) {
        String file = IvyPatternHelper.substitute(getResolvedIvyPattern(), mrid.getOrganisation(),
            mrid.getName(), mrid.getRevision(), "ivy", "ivy", "xml");
        return new File(getResolutionCacheRoot(), file);
    }

    public File getResolvedIvyPropertiesInCache(ModuleRevisionId mrid) {
        String file = IvyPatternHelper.substitute(getResolvedIvyPropertiesPattern(),
            mrid.getOrganisation(), mrid.getName(), mrid.getRevision(), "ivy", "ivy", "xml");
        return new File(getResolutionCacheRoot(), file);
    }

    public File getConfigurationResolveReportInCache(String resolveId, String conf) {
        return new File(getResolutionCacheRoot(), resolveId + "-" + conf + ".xml");
    }

    public File[] getConfigurationResolveReportsInCache(final String resolveId) {
        final String prefix = resolveId + "-";
        final String suffix = ".xml";
        return getResolutionCacheRoot().listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.startsWith(prefix) && name.endsWith(suffix);
            }
        });
    }

    public ModuleDescriptor getResolvedModuleDescriptor(ModuleRevisionId mrid)
            throws ParseException, IOException {
        File ivyFile = getResolvedIvyFileInCache(mrid);
        if (!ivyFile.exists()) {
            throw new IllegalStateException("Ivy file not found in cache for " + mrid + "!");
        }

        Properties paths = new Properties();

        File parentsFile = getResolvedIvyPropertiesInCache(ModuleRevisionId.newInstance(mrid,
            mrid.getRevision() + "-parents"));
        if (parentsFile.exists()) {
            FileInputStream in = new FileInputStream(parentsFile);
            paths.load(in);
            in.close();
        }

        ParserSettings pSettings = new CacheParserSettings(settings, paths);

        URL ivyFileURL = ivyFile.toURI().toURL();
        return getModuleDescriptorParser(ivyFile).parseDescriptor(pSettings, ivyFileURL, false);
    }

    /**
     * Choose write module descriptor parser for a given moduleDescriptor
     *
     * @param moduleDescriptorFile
     *            a given module descriptor
     * @return ModuleDescriptorParser
     */
    protected ModuleDescriptorParser getModuleDescriptorParser(File moduleDescriptorFile) {
        return XmlModuleDescriptorParser.getInstance();
    }

    public void saveResolvedModuleDescriptor(ModuleDescriptor md) throws ParseException,
            IOException {
        ModuleRevisionId mrevId = md.getResolvedModuleRevisionId();
        File ivyFileInCache = getResolvedIvyFileInCache(mrevId);
        assertInsideCache(ivyFileInCache);
        md.toIvyFile(ivyFileInCache);

        Properties paths = new Properties();
        saveLocalParents(mrevId, md, ivyFileInCache, paths);

        if (!paths.isEmpty()) {
            File parentsFile = getResolvedIvyPropertiesInCache(ModuleRevisionId.newInstance(mrevId,
                mrevId.getRevision() + "-parents"));
            assertInsideCache(parentsFile);
            FileOutputStream out = new FileOutputStream(parentsFile);
            paths.store(out, null);
            out.close();
        }
    }

    /**
     * @throws IllegalArgumentException if the given path points outside of the cache.
     */
    public final void assertInsideCache(File fileInCache) {
        if (!FileUtil.isLeadingPath(getResolutionCacheRoot(), fileInCache)) {
            throw new IllegalArgumentException(fileInCache + " is outside of the cache");
        }
    }

    private void saveLocalParents(ModuleRevisionId baseMrevId, ModuleDescriptor md, File mdFile,
            Properties paths) throws ParseException, IOException {
        for (ExtendsDescriptor parent : md.getInheritedDescriptors()) {
            if (!parent.isLocal()) {
                // we store only local parents in the cache!
                continue;
            }

            ModuleDescriptor parentMd = parent.getParentMd();
            ModuleRevisionId pRevId = ModuleRevisionId.newInstance(baseMrevId,
                baseMrevId.getRevision() + "-parent." + paths.size());
            File parentFile = getResolvedIvyFileInCache(pRevId);
            assertInsideCache(parentFile);
            parentMd.toIvyFile(parentFile);

            paths.setProperty(mdFile.getName() + "|" + parent.getLocation(),
                parentFile.getAbsolutePath());
            saveLocalParents(baseMrevId, parentMd, parentFile, paths);
        }
    }

    public String toString() {
        return name;
    }

    public void clean() {
        FileUtil.forceDelete(getResolutionCacheRoot());
    }

    private static class CacheParserSettings implements ParserSettings {

        private ParserSettings delegate;

        private Map<Object, Object> parentPaths;

        public CacheParserSettings(ParserSettings delegate, Map<Object, Object> parentPaths) {
            this.delegate = delegate;
            this.parentPaths = parentPaths;
        }

        public String substitute(String value) {
            return delegate.substitute(value);
        }

        public Map<String, String> substitute(Map<String, String> strings) {
            return delegate.substitute(strings);
        }

        public ResolutionCacheManager getResolutionCacheManager() {
            return delegate.getResolutionCacheManager();
        }

        public ConflictManager getConflictManager(String name) {
            return delegate.getConflictManager(name);
        }

        public PatternMatcher getMatcher(String matcherName) {
            return delegate.getMatcher(matcherName);
        }

        public Namespace getNamespace(String namespace) {
            return delegate.getNamespace(namespace);
        }

        public StatusManager getStatusManager() {
            return delegate.getStatusManager();
        }

        public RelativeUrlResolver getRelativeUrlResolver() {
            return new MapURLResolver(parentPaths, delegate.getRelativeUrlResolver());
        }

        public DependencyResolver getResolver(ModuleRevisionId mRevId) {
            return delegate.getResolver(mRevId);
        }

        public File resolveFile(String filename) {
            return delegate.resolveFile(filename);
        }

        public String getDefaultBranch(ModuleId moduleId) {
            return delegate.getDefaultBranch(moduleId);
        }

        public Namespace getContextNamespace() {
            return delegate.getContextNamespace();
        }

        public String getVariable(String value) {
            return delegate.getVariable(value);
        }

        @Override
        public TimeoutConstraint getTimeoutConstraint(final String name) {
            return this.delegate.getTimeoutConstraint(name);
        }
    }

    private static final class MapURLResolver extends RelativeUrlResolver {

        private Map<Object, Object> paths;

        private RelativeUrlResolver delegate;

        private MapURLResolver(Map<Object, Object> paths, RelativeUrlResolver delegate) {
            this.paths = paths;
            this.delegate = delegate;
        }

        public URL getURL(URL context, String url) throws MalformedURLException {
            String path = context.getPath();
            if (path.contains("/")) {
                String file = path.substring(path.lastIndexOf('/') + 1);

                if (paths.containsKey(file + "|" + url)) {
                    File result = new File(paths.get(file + "|" + url).toString());
                    return result.toURI().toURL();
                }
            }

            return delegate.getURL(context, url);
        }
    }
}
