/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */
package de.rcenvironment.core.utils.executor.fileinfo;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.rcenvironment.core.toolkitbridge.transitional.TextStreamWatcherFactory;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.textstream.TextStreamWatcher;
import de.rcenvironment.core.utils.common.textstream.receivers.CapturingTextOutReceiver;
import de.rcenvironment.core.utils.executor.CommandLineExecutor;
import de.rcenvironment.core.utils.executor.fileinfo.internal.AbstractFileService;

/**
 * {@link FileInfoService} implementation for unix systems.
 * 
 * @author Christian Weiss
 */
public class UnixFileInfoService extends AbstractFileService {

    private static final Pattern LS_DIRECTORY_PATTERN = Pattern.compile("^(.*):$");

    private static final Pattern LS_TOTAL_PATTERN = Pattern.compile("^total \\d+$");

    private static final Pattern LS_LINE_PATTERN = Pattern
        .compile("^([-dl])(?:[-r][-w][-xst]){3}(?:\\+)?\\s+\\d+\\s+([-_\\w]+)\\s+([-_\\w]+)\\s+(\\d+)\\s+(\\d+)\\s+(.*)\\s*$");

    private static final Pattern FILE_CMD_DIRECTORY_PATTERN = Pattern.compile(".*: (?:sticky )?directory$");

    private static final Pattern FILE_CMD_SYMLINK_PATTERN = Pattern.compile("^(.*): (?:broken )?symbolic link to `(.*)'$");

    private static final Pattern NON_EXISTING_PATTERN = Pattern.compile("^.*: cannot open `.*' \\(No such file or directory\\)$");

    private static final Pattern PERMISSION_DENIED_PATTERN = Pattern.compile("^.*: cannot access `.*': \\(?Permission denied\\)?$");

    public UnixFileInfoService(final CommandLineExecutor commandLineExecutor) {
        super(commandLineExecutor);
    }

    @Override
    public Collection<FileInfo> listFiles(final String directory, final boolean recursively)
        throws IOException {
        return listContent(directory, recursively, false);
    }

    @Override
    public Collection<FileInfo> listContent(final String directory, final boolean recursively)
        throws IOException {
        return listContent(directory, recursively, true);
    }

    protected Collection<FileInfo> listContent(final String directory, final boolean recursively,
        final boolean directories)
        throws IOException {
        final List<FileInfo> result = new LinkedList<FileInfo>();
        final String commandPattern;
        if (recursively) {
            commandPattern = "ls -R -l -A -b -q --time-style=+%%s --color=never %s";
        } else {
            commandPattern = "ls -l -A -b -q --time-style=+%%s --color=never %s";
        }

        final String command = StringUtils.format(commandPattern, directory);
        try {
            final Output executionOutput = exec(command);
            if (executionOutput.err.length() > 0) {
                throw new IOException(executionOutput.err);
            }
            final String output = executionOutput.out.trim();
            try (final Scanner scanner = new Scanner(output)) {

                String relativePathPrefix = "";
                while (scanner.hasNextLine()) {
                    final String line = scanner.nextLine();
                    if (line.isEmpty()) {
                        continue;
                    }
                    if (LS_TOTAL_PATTERN.matcher(line).matches()) {
                        continue;
                    }
                    final Matcher directoryMatcher = LS_DIRECTORY_PATTERN.matcher(line);
                    if (directoryMatcher.matches()) {
                        relativePathPrefix = directoryMatcher.group(1);
                        if (relativePathPrefix.equals(".")) {
                            relativePathPrefix = "";
                        }
                        relativePathPrefix = relativePathPrefix.replaceAll("\\./", "");
                        continue;
                    }
                    final FileInfo fileInfo = parseFileInfo(line, directory, relativePathPrefix);
                    // e.g. broken links result in null
                    if (fileInfo != null) {
                        // exclude directories if desired
                        if (!directories && fileInfo.isDirectory()) {
                            continue;
                        }
                        result.add(fileInfo);
                    }
                }
            }
            return result;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException(e);
        }
    }

    private FileInfo parseFileInfo(final String line, final String path, final String directory) throws IOException {
        final Matcher matcher = LS_LINE_PATTERN.matcher(line);
        if (!matcher.matches()) {
            throw new IOException(StringUtils.format("LS line does not match pattern: %s", line));
        }
        // type of entry is first char {-, d, l}
        char type = matcher.group(1).charAt(0);
        // parse size
        long size;
        try {
            final String sizeString = matcher.group(4);
            size = Long.parseLong(sizeString);
        } catch (NumberFormatException e) {
            throw new IOException(e);
        }
        // parse modification date
        Date modificationDate = null;
        try {
            final String timestampString = matcher.group(5);
            final long timestamp = Long.parseLong(timestampString) * 1000L;
            modificationDate = new Date(timestamp);
        } catch (NumberFormatException e) {
            throw new IOException(e);
        }
        // parse entry name
        String name = matcher.group(6);
        // if the entry is a link the real type, size and modification time of
        // the linked file needs to be retrieved
        if (type == 'l') {
            // adjust the name to the part before the '-> <link target>'
            name = name.substring(0, name.indexOf("->") - 1);
            // resolve the real target path
            final String targetPath = resolveTarget(path + name);
            // if the link is broken return null
            if (targetPath == null) {
                return null;
            }
            if (isDirectory(targetPath)) {
                type = 'd';
            } else {
                type = '-';
                // only for files get their real size
                size = size(targetPath);
                // only for files get their modification time
                // TODO
            }
        }
        String relativePath;
        if (directory.isEmpty()) {
            relativePath = name;
        } else {
            relativePath = directory + '/' + name;
        }
        // directories are set to size 0
        if (type == 'd') {
            size = 0;
        }
        // remove multiple slashes in path
        relativePath = relativePath.replaceAll("/+", "/");
        final FileInfo fileInfo = new FileInfo(type == '-', relativePath, modificationDate, size);
        return fileInfo;
    }

    @Override
    public boolean isDirectory(final String path) throws IOException {
        final String commandPattern = "file %s";
        final String command = StringUtils.format(commandPattern, path);
        try {
            final Output executionOutput = exec(command);
            if (executionOutput.err.length() > 0) {
                throw new IOException(executionOutput.err);
            } else {
                final String output = executionOutput.out.trim();
                final Matcher matcher1 = FILE_CMD_DIRECTORY_PATTERN.matcher(output);
                if (matcher1.matches()) {
                    return true;
                } else {
                    final Matcher matcher2 = FILE_CMD_SYMLINK_PATTERN.matcher(output);
                    if (matcher2.matches()) {
                        return isDirectory(matcher2.group(2));
                    }
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException(e);
        }
        return false;
    }

    private String resolveTarget(final String path) throws IOException {
        final String commandPattern = "file %s";
        final String command = StringUtils.format(commandPattern, path);
        try {
            final Output executionOutput = exec(command);
            if (executionOutput.err.length() > 0) {
                throw new IOException(executionOutput.err);
            } else {
                final String output = executionOutput.out.trim();
                Matcher matcher = FILE_CMD_SYMLINK_PATTERN.matcher(output);
                if (matcher.matches()) {
                    final String targetPath = matcher.group(2);
                    if (targetPath.startsWith("/")) {
                        return resolveTarget(targetPath);
                    } else {
                        final String absolutePath = path.substring(0, path.lastIndexOf('/') + 1) + targetPath;
                        return resolveTarget(absolutePath);
                    }
                }
                matcher = NON_EXISTING_PATTERN.matcher(output);
                if (matcher.matches()) {
                    return null;
                }
                return path;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException(e);
        }
    }

    @Override
    public Long size(final String path) throws IOException {
        if (isDirectory(path)) {
            return 0L;
        }
        final String commandPattern = "du --bytes %s";
        final String command = StringUtils.format(commandPattern, path);
        try {
            final Output executionOutput = exec(command);
            if (executionOutput.err.length() > 0) {
                if (PERMISSION_DENIED_PATTERN.matcher(executionOutput.err.trim()).matches()) {
                    return null;
                }
                throw new IOException(executionOutput.err);
            } else {
                final String output = executionOutput.out.trim();
                final String[] sizeInfo = output.split("\\s+");
                return Long.parseLong(sizeInfo[0]);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException(e);
        }
    };

    private Output exec(final String commandString) throws IOException, InterruptedException {
        final CommandLineExecutor executor = getCommandLineExecutor();
        // set bash language
        executor.setEnv("LANG", "en_US.UTF8");
        InputStream stdout;
        InputStream stderr;
        executor.start(commandString);
        stdout = executor.getStdout();
        stderr = executor.getStderr();
        final CapturingTextOutReceiver outReceiver = new CapturingTextOutReceiver();
        final CapturingTextOutReceiver errReceiver = new CapturingTextOutReceiver();
        final TextStreamWatcher stdoutWatcher = TextStreamWatcherFactory.create(stdout, outReceiver);
        final TextStreamWatcher stderrWatcher = TextStreamWatcherFactory.create(stderr, errReceiver);
        stdoutWatcher.start();
        stderrWatcher.start();
        executor.waitForTermination();
        stdoutWatcher.waitForTermination();
        stderrWatcher.waitForTermination();
        final Output result = new Output();
        result.out = outReceiver.getBufferedOutput();
        result.err = errReceiver.getBufferedOutput();
        return result;
    }

    /**
     * Information of output type.
     * 
     * @author Christian Weiss
     */
    private static final class Output {

        private String out;

        private String err;
    }

}
