/*
 *  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.tools.ant.types.resources;

import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.Vector;
import java.util.stream.Collectors;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.PatternSet;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.selectors.AbstractSelectorContainer;
import org.apache.tools.ant.types.selectors.FileSelector;

/**
 * ResourceCollection implementation; like AbstractFileSet with absolute paths.
 * @since Ant 1.7
 */
public class Files extends AbstractSelectorContainer
    implements ResourceCollection {

    private PatternSet defaultPatterns = new PatternSet();
    private Vector<PatternSet> additionalPatterns = new Vector<>();

    private boolean useDefaultExcludes = true;
    private boolean caseSensitive = true;
    private boolean followSymlinks = true;

    /* cached DirectoryScanner instance */
    private DirectoryScanner ds = null;

    /**
     * Construct a new <code>Files</code> collection.
     */
    public Files() {
        super();
    }

    /**
     * Construct a new <code>Files</code> collection, shallowly cloned
     * from the specified <code>Files</code>.
     * @param f the <code>Files</code> to use as a template.
     */
    protected Files(Files f) {
        this.defaultPatterns = f.defaultPatterns;
        this.additionalPatterns = f.additionalPatterns;
        this.useDefaultExcludes = f.useDefaultExcludes;
        this.caseSensitive = f.caseSensitive;
        this.followSymlinks = f.followSymlinks;
        this.ds = f.ds;
        setProject(f.getProject());
    }

    /**
     * Make this instance in effect a reference to another instance.
     *
     * <p>You must not set another attribute or nest elements inside
     * this element if you make it a reference.</p>
     * @param r the <code>Reference</code> to use.
     * @throws BuildException if there is a problem.
     */
    @Override
    public void setRefid(Reference r) throws BuildException {
        if (hasPatterns(defaultPatterns)) {
            throw tooManyAttributes();
        }
        if (!additionalPatterns.isEmpty()) {
            throw noChildrenAllowed();
        }
        if (hasSelectors()) {
            throw noChildrenAllowed();
        }
        super.setRefid(r);
    }

    /**
     * Create a nested patternset.
     * @return <code>PatternSet</code>.
     */
    public synchronized PatternSet createPatternSet() {
        if (isReference()) {
            throw noChildrenAllowed();
        }
        PatternSet patterns = new PatternSet();
        additionalPatterns.addElement(patterns);
        ds = null;
        setChecked(false);
        return patterns;
    }

    /**
     * Add a name entry to the include list.
     * @return <code>PatternSet.NameEntry</code>.
     */
    public synchronized PatternSet.NameEntry createInclude() {
        if (isReference()) {
            throw noChildrenAllowed();
        }
        ds = null;
        return defaultPatterns.createInclude();
    }

    /**
     * Add a name entry to the include files list.
     * @return <code>PatternSet.PatternFileNameEntry</code>.
     */
    public synchronized PatternSet.NameEntry createIncludesFile() {
        if (isReference()) {
            throw noChildrenAllowed();
        }
        ds = null;
        return defaultPatterns.createIncludesFile();
    }

    /**
     * Add a name entry to the exclude list.
     * @return <code>PatternSet.NameEntry</code>.
     */
    public synchronized PatternSet.NameEntry createExclude() {
        if (isReference()) {
            throw noChildrenAllowed();
        }
        ds = null;
        return defaultPatterns.createExclude();
    }

    /**
     * Add a name entry to the excludes files list.
     * @return <code>PatternSet.PatternFileNameEntry</code>.
     */
    public synchronized PatternSet.NameEntry createExcludesFile() {
        if (isReference()) {
            throw noChildrenAllowed();
        }
        ds = null;
        return defaultPatterns.createExcludesFile();
    }

    /**
     * Append <code>includes</code> to the current list of include
     * patterns.
     *
     * <p>Patterns may be separated by a comma or a space.</p>
     *
     * @param includes the <code>String</code> containing the include patterns.
     */
    public synchronized void setIncludes(String includes) {
        checkAttributesAllowed();
        defaultPatterns.setIncludes(includes);
        ds = null;
    }

    /**
     * Append <code>includes</code> to the current list of include
     * patterns.
     *
     * @param includes array containing the include patterns.
     */
    public synchronized void appendIncludes(String[] includes) {
        checkAttributesAllowed();
        if (includes != null) {
            for (String include : includes) {
                defaultPatterns.createInclude().setName(include);
            }
            ds = null;
        }
    }

    /**
     * Append <code>excludes</code> to the current list of exclude
     * patterns.
     *
     * <p>Patterns may be separated by a comma or a space.</p>
     *
     * @param excludes the <code>String</code> containing the exclude patterns.
     */
    public synchronized void setExcludes(String excludes) {
        checkAttributesAllowed();
        defaultPatterns.setExcludes(excludes);
        ds = null;
    }

    /**
     * Append <code>excludes</code> to the current list of include
     * patterns.
     *
     * @param excludes array containing the exclude patterns.
     */
    public synchronized void appendExcludes(String[] excludes) {
        checkAttributesAllowed();
        if (excludes != null) {
            for (String exclude : excludes) {
                defaultPatterns.createExclude().setName(exclude);
            }
            ds = null;
        }
    }

    /**
     * Set the <code>File</code> containing the includes patterns.
     *
     * @param incl <code>File</code> instance.
     * @throws BuildException if there is a problem.
     */
    public synchronized void setIncludesfile(File incl) throws BuildException {
        checkAttributesAllowed();
        defaultPatterns.setIncludesfile(incl);
        ds = null;
    }

    /**
     * Set the <code>File</code> containing the excludes patterns.
     *
     * @param excl <code>File</code> instance.
     * @throws BuildException if there is a problem.
     */
    public synchronized void setExcludesfile(File excl) throws BuildException {
        checkAttributesAllowed();
        defaultPatterns.setExcludesfile(excl);
        ds = null;
    }

    /**
     * Set whether default exclusions should be used or not.
     *
     * @param useDefaultExcludes <code>boolean</code>.
     */
    public synchronized void setDefaultexcludes(boolean useDefaultExcludes) {
        checkAttributesAllowed();
        this.useDefaultExcludes = useDefaultExcludes;
        ds = null;
    }

    /**
     * Get whether default exclusions should be used or not.
     * @return the defaultexclusions value.
     */
    public synchronized boolean getDefaultexcludes() {
        return isReference()
            ? getRef().getDefaultexcludes() : useDefaultExcludes;
    }

    /**
     * Set case-sensitivity of the Files collection.
     *
     * @param caseSensitive <code>boolean</code>.
     */
    public synchronized void setCaseSensitive(boolean caseSensitive) {
        checkAttributesAllowed();
        this.caseSensitive = caseSensitive;
        ds = null;
    }

    /**
     * Find out if this Files collection is case-sensitive.
     *
     * @return <code>boolean</code> indicating whether the Files
     * collection is case-sensitive.
     */
    public synchronized boolean isCaseSensitive() {
        return isReference()
            ? getRef().isCaseSensitive() : caseSensitive;
    }

    /**
     * Set whether or not symbolic links or Windows junctions should be followed.
     *
     * @param followSymlinks whether or not symbolic links should be followed.
     */
    public synchronized void setFollowSymlinks(boolean followSymlinks) {
        checkAttributesAllowed();
        this.followSymlinks = followSymlinks;
        ds = null;
    }

    /**
     * Find out whether symbolic links should be followed.
     *
     * @return <code>boolean</code> indicating whether symbolic links
     *         should be followed.
     */
    public synchronized boolean isFollowSymlinks() {
        return isReference()
            ? getRef().isFollowSymlinks() : followSymlinks;
    }

    /**
     * Fulfill the ResourceCollection contract.
     * @return an Iterator of Resources.
     */
    @Override
    public synchronized Iterator<Resource> iterator() {
        if (isReference()) {
            return getRef().iterator();
        }
        ensureDirectoryScannerSetup();
        ds.scan();
        int fct = ds.getIncludedFilesCount();
        int dct = ds.getIncludedDirsCount();
        if (fct + dct == 0) {
            return Collections.emptyIterator();
        }
        FileResourceIterator result = new FileResourceIterator(getProject());
        if (fct > 0) {
            result.addFiles(ds.getIncludedFiles());
        }
        if (dct > 0) {
            result.addFiles(ds.getIncludedDirectories());
        }
        return result;
    }

    /**
     * Fulfill the ResourceCollection contract.
     * @return number of elements as int.
     */
    @Override
    public synchronized int size() {
        if (isReference()) {
            return getRef().size();
        }
        ensureDirectoryScannerSetup();
        ds.scan();
        return ds.getIncludedFilesCount() + ds.getIncludedDirsCount();
    }

    /**
     * Find out whether this Files collection has patterns.
     *
     * @return whether any patterns are in this container.
     */
    public synchronized boolean hasPatterns() {
        if (isReference()) {
            return getRef().hasPatterns();
        }
        dieOnCircularReference();
        return hasPatterns(defaultPatterns)
            || additionalPatterns.stream().anyMatch(this::hasPatterns);
    }

    /**
     * Add a new selector into this container.
     *
     * @param selector the new <code>FileSelector</code> to add.
     */
    @Override
    public synchronized void appendSelector(FileSelector selector) {
        if (isReference()) {
            throw noChildrenAllowed();
        }
        super.appendSelector(selector);
        ds = null;
    }

    /**
     * Format this Files collection as a String.
     * @return a descriptive <code>String</code>.
     */
    @Override
    public String toString() {
        if (isReference()) {
            return getRef().toString();
        }
        return isEmpty() ? "" : stream().map(Object::toString)
            .collect(Collectors.joining(File.pathSeparator));
    }

    /**
     * Create a deep clone of this instance, except for the nested selectors
     * (the list of selectors is a shallow clone of this instance's list).
     * @return a cloned Object.
     */
    @Override
    public synchronized Object clone() {
        if (isReference()) {
            return getRef().clone();
        }
        Files f = (Files) super.clone();
        f.defaultPatterns = (PatternSet) defaultPatterns.clone();
        f.additionalPatterns = new Vector<>(additionalPatterns.size());
        for (PatternSet ps : additionalPatterns) {
            f.additionalPatterns.add((PatternSet) ps.clone());
        }
        return f;
    }

    /**
     * Get the merged include patterns for this Files collection.
     * @param p Project instance.
     * @return the include patterns of the default pattern set and all
     * nested patternsets.
     */
    public String[] mergeIncludes(Project p) {
        return mergePatterns(p).getIncludePatterns(p);
    }

    /**
     * Get the merged exclude patterns for this Files collection.
     * @param p Project instance.
     * @return the exclude patterns of the default pattern set and all
     * nested patternsets.
     */
    public String[] mergeExcludes(Project p) {
        return mergePatterns(p).getExcludePatterns(p);
    }

    /**
     * Get the merged patterns for this Files collection.
     * @param p Project instance.
     * @return the default patternset merged with the additional sets
     * in a new PatternSet instance.
     */
    public synchronized PatternSet mergePatterns(Project p) {
        if (isReference()) {
            return getRef().mergePatterns(p);
        }
        dieOnCircularReference();
        PatternSet ps = new PatternSet();
        ps.append(defaultPatterns, p);
        additionalPatterns.forEach(pat -> ps.append(pat, p));
        return ps;
    }

    /**
     * Always returns true.
     * @return true indicating that all elements of a Files collection
     *              will be FileResources.
     */
    @Override
    public boolean isFilesystemOnly() {
        return true;
    }

    /**
     * Perform the check for circular references and return the
     * referenced Files collection.
     * @return <code>FileCollection</code>.
     */
    protected Files getRef() {
        return getCheckedRef(Files.class);
    }

    private synchronized void ensureDirectoryScannerSetup() {
        dieOnCircularReference();
        if (ds == null) {
            ds = new DirectoryScanner();
            PatternSet ps = mergePatterns(getProject());
            ds.setIncludes(ps.getIncludePatterns(getProject()));
            ds.setExcludes(ps.getExcludePatterns(getProject()));
            ds.setSelectors(getSelectors(getProject()));
            if (useDefaultExcludes) {
                ds.addDefaultExcludes();
            }
            ds.setCaseSensitive(caseSensitive);
            ds.setFollowSymlinks(followSymlinks);
        }
    }

    private boolean hasPatterns(PatternSet ps) {
        String[] includePatterns = ps.getIncludePatterns(getProject());
        String[] excludePatterns = ps.getExcludePatterns(getProject());
        return (includePatterns != null && includePatterns.length > 0)
            || (excludePatterns != null && excludePatterns.length > 0);
    }

}
