/*
* Licensed Materials - Property of IBM* and/or HCL**
* UrbanCode Deploy
* UrbanCode Build
* UrbanCode Release
* AnthillPro
* (c) Copyright IBM Corporation 2011, 2017. All Rights Reserved.
* (c) Copyright HCL Technologies Ltd. 2018. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*
* * Trademark of International Business Machines
* ** Trademark of HCL Technologies Limited
*/
package com.urbancode.air

import groovy.lang.Closure;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;

public class FileSet {
    def isWindows = (System.getProperty('os.name') =~ /(?i)windows/).find()
    def base
    def includes = []
    def excludes = []

    public FileSet(base) {
        if (base instanceof File) {
            this.base = base
        }
        else if (base == null) {
            this.base = new File('.').absolutePath
        }
        else {
            this.base = new File(base)
        }
    }

    public def include(antPattern) {
        forNonEmptyLines(antPattern) {
            includes << convertToPattern(it)
        }
    }

    public def exclude(antPattern) {
        forNonEmptyLines(antPattern) {
            excludes << convertToPattern(it)
        }
    }

    public def each(Closure closure) {
        Path p = base.toPath();
        scanDirectory(p, attrs(p), closure);
    }

    private void callClosureIfMatches(File file, Closure closure) {
        String path = file.path.replace('\\', '/').substring(base.path.length())
        boolean matches = false
        for (p in includes) {
            if (path =~ p) {
                matches = true
                break;
            }
        }
        if (matches) {
            for (p in excludes) {
                if (path =~ p) {
                    matches = false;
                    break;
                }
            }
        }
        if (matches) {
            closure(file)
        }
    }

    // Concurrently opens number of directory FDs proportional to tree depth
    private void scanDirectory(Path dir, BasicFileAttributes dirAttrs, Closure closure)
    throws FileNotFoundException {
        checkIsDir(dir, dirAttrs);
        DirectoryStream<Path> ds = opendir(dir);
        if (ds == null) {
            return;
        }
        try {
            for (Path p : ds) {
                BasicFileAttributes pAttrs = attrs(p);
                callClosureIfMatches(p.toFile(), closure);
                if (pAttrs != null && pAttrs.isDirectory()) {
                    scanDirectory(p, pAttrs, closure);
                }
            }
        }
        finally {
            closedir(ds);
        }
    }

    private BasicFileAttributes attrs(Path p) {
        try {
            // We are following symlinks, which might produce wrong results if
            // the symlink forms a loop, but this is consistent with the original
            // behavior of this class. Following loops is probably undesirable
            // for most users, but following symlinks to external directories
            // might be actively used, and it is hard to prevent one and allow
            // the other.
            return Files.readAttributes(p, BasicFileAttributes.class);
        }
        catch (IOException e) {
            return null;
        }
    }

    private void checkIsDir(Path dir, BasicFileAttributes attrs)
    throws FileNotFoundException {
        if (attrs == null) {
            throw new FileNotFoundException(dir.toFile().getAbsolutePath());
        }
        if (!attrs.isDirectory()) {
            throw new IllegalArgumentException("The provided File object is not a directory: " + dir.toFile().getAbsolutePath());
        }
    }

    private DirectoryStream<Path> opendir(Path dir) {
        try {
            return Files.newDirectoryStream(dir);
        }
        catch (IOException e) {
            return null;
        }
    }

    private void closedir(DirectoryStream<Path> ds) {
        if (ds == null) {
            return;
        }
        try {
            ds.close();
        }
        catch (IOException swallow) {
        }
    }

    public def files() {
        def list = []
        each { list << it }
        return list
    }

    private forNonEmptyLines(strings, Closure closure) {
        if (strings instanceof Collection) {
            strings.each {
                def trimmed = it?.trim()
                if (trimmed?.length() > 0) {
                    closure(it)
                }
            }
        }
        else if (strings != null) {
            forNonEmptyLines(strings.readLines(), closure)
        }
    }

    private convertToPattern(antPattern) {
        // normalize file separator in pattern
        antPattern = antPattern.replace('\\', '/');

        // ensure leading / character from pattern
        def pattern = antPattern.startsWith('/') ? antPattern : '/'+antPattern

        // deal with special regex-characters that should be interpreted as literals
        '\\.+[]^${}|()'.toCharArray().each{ c ->
            pattern = pattern.replace(''+c, '\\'+c)
        }
        pattern = pattern.replace('?', '.') // ? is a single-char wildcard

        // deal with ant-style wildcards
        StringBuffer result = new StringBuffer()
        result.append("^")
        def m = (pattern =~ '\\*\\*/|\\*\\*|\\*')
        while (m.find()) {
            def token = m.group()
            def replacement;
            if (token == '**/') {
                replacement = '.*(?<=/)'
            }
            else if (token == '**') {
                replacement = '.*'
            }
            else {
                replacement = '[^/]*'
            }
            m.appendReplacement(result, Matcher.quoteReplacement(replacement))
        }
        m.appendTail(result)
        result.append("\$")
        def flags = 0
        if (isWindows) {
            flags |= Pattern.CASE_INSENSITIVE
        }
        return Pattern.compile(result.toString(), flags)
    }
}