package org.xbib.files;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;

import java.nio.file.Files;
import java.util.stream.Stream;

public class DefaultFileService implements FileService {

    private static final int BUFFER_SIZE = 128 * 1024;

    private static final Set<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
            PosixFilePermissions.fromString("rwxr-xr-x");

    private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
            PosixFilePermissions.fromString("rw-r--r--");

    public DefaultFileService(URI uri, Map<String, ?> env) {
    }

    @Override
    public Boolean exists(String path) {
        return Files.exists(Paths.get(path));
    }

    @Override
    public Boolean isExecutable(String path) {
        return Files.isExecutable(Paths.get(path));
    }

    @Override
    public Boolean isDirectory(String path) {
        return Files.isDirectory(Paths.get(path));
    }

    @Override
    public Boolean isRegularFile(String path) {
        return Files.isRegularFile(Paths.get(path));
    }

    @Override
    public Boolean isHidden(String path) throws IOException {
        return Files.isHidden(Paths.get(path));
    }

    @Override
    public Boolean isSameFile(String path1, String path2) throws IOException {
        return Files.isSameFile(Paths.get(path1), Paths.get(path2));
    }

    @Override
    public Boolean isSymbolicLink(String path) {
        return Files.isSymbolicLink(Paths.get(path));
    }

    @Override
    public Boolean isReadable(String path) {
        return Files.isReadable(Paths.get(path));
    }

    @Override
    public Boolean isWritable(String path) {
        return Files.isWritable(Paths.get(path));
    }

    @Override
    public void setAttribute(String path, String attribute, Object value) throws IOException {
        Files.setAttribute(Paths.get(path), attribute, value);
    }

    @Override
    public Object getAttribute(String path, String attribute) throws IOException {
        return Files.getAttribute(Paths.get(path), attribute);
    }

    @Override
    public void setPermissions(String path, Set<PosixFilePermission> permissions) throws IOException {
        Files.setPosixFilePermissions(Paths.get(path), permissions);
    }

    @Override
    public Set<PosixFilePermission> getPermissions(String path) throws IOException {
        return Files.getPosixFilePermissions(Paths.get(path));
    }

    @Override
    public void setPosixFileAttributes(String path,
                                       String owner,
                                       String group,
                                       Instant lastModifiedTime,
                                       Instant lastAccessTime,
                                       Instant createTime) throws IOException {
        FileSystem fileSystem = FileSystems.getDefault();
        PosixFileAttributeView view = Files.getFileAttributeView(fileSystem.getPath(path),
                PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
        view.setOwner(fileSystem.getUserPrincipalLookupService().lookupPrincipalByName(owner));
        view.setGroup(fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group));
        view.setTimes(FileTime.from(lastModifiedTime),
                FileTime.from(lastAccessTime),
                FileTime.from(createTime));
    }

    @Override
    public PosixFileAttributes getPosixFileAttributes(String path) throws IOException {
        return Files.getFileAttributeView(FileSystems.getDefault().getPath(path),
                PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes();
    }

    @Override
    public void setOwner(String path, String owner) throws IOException {
        Files.setOwner(Paths.get(path),
                FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName(owner));
    }

    @Override
    public String getOwner(String path) throws IOException {
        return Files.getOwner(Paths.get(path)).getName();
    }

    @Override
    public void setGroup(String path, String group) throws IOException {
        FileSystem fileSystem = FileSystems.getDefault();
        PosixFileAttributeView view = Files.getFileAttributeView(fileSystem.getPath(path),
                PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
        view.setGroup(fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group));
    }

    @Override
    public String getGroup(String path) throws IOException {
        return Files.getFileAttributeView(FileSystems.getDefault().getPath(path),
                PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes().group().getName();
    }

    @Override
    public void setLastModifiedTime(String path, Instant lastModified) throws IOException {
        Files.setLastModifiedTime(Paths.get(path), FileTime.from(lastModified));
    }

    @Override
    public Instant getLastModifiedTime(String path) throws IOException {
        return Files.getLastModifiedTime(Paths.get(path)).toInstant();
    }

    @Override
    public void createFile(String path, FileAttribute<?>... attributes) throws IOException {
        Files.createFile(Paths.get(path), attributes);
    }

    @Override
    public void createDirectory(String path, FileAttribute<?>... attributes) throws IOException {
        Files.createDirectory(Paths.get(path), attributes);
    }

    @Override
    public void createDirectories(String path, FileAttribute<?>... attributes) throws IOException {
        Files.createDirectories(Paths.get(path), attributes);
    }

    @Override
    public void upload(Path source,
                       String target,
                       CopyOption... copyOptions) throws IOException {
        upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
    }

    @Override
    public void upload(Path source,
                       String target,
                       Set<PosixFilePermission> dirPerms,
                       Set<PosixFilePermission> filePerms,
                       CopyOption... copyOptions) throws IOException {
        upload(Files.newByteChannel(source), Paths.get(target), dirPerms, filePerms, copyOptions);
    }

    @Override
    public void upload(Path source,
                       Path target,
                       CopyOption... copyOptions) throws IOException {
        upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
    }

    @Override
    public void upload(Path source,
                       Path target,
                       Set<PosixFilePermission> dirPerms,
                       Set<PosixFilePermission> filePerms,
                       CopyOption... copyOptions) throws IOException {
        upload(Files.newByteChannel(source), target, dirPerms, filePerms, copyOptions);
    }

    @Override
    public void upload(InputStream source,
                       String target,
                       CopyOption... copyOptions) throws IOException {
        upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
    }

    @Override
    public void upload(InputStream source,
                       String target,
                       Set<PosixFilePermission> dirPerms,
                       Set<PosixFilePermission> filePerms,
                       CopyOption... copyOptions) throws IOException {
        upload(Channels.newChannel(source), Paths.get(target), dirPerms, filePerms, copyOptions);
    }

    @Override
    public void upload(InputStream source,
                       Path target,
                       CopyOption... copyOptions) throws IOException {
        upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions);
    }

    @Override
    public void upload(InputStream source,
                       Path target,
                       Set<PosixFilePermission> dirPerms,
                       Set<PosixFilePermission> filePerms,
                       CopyOption... copyOptions) throws IOException {
        upload(Channels.newChannel(source), target, dirPerms, filePerms, copyOptions);
    }

    @Override
    public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
        download(source, target, copyOptions);
    }

    @Override
    public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
        download(Paths.get(source), target, copyOptions);
    }

    @Override
    public void download(Path source, OutputStream target) throws IOException {
        download(source, target);
    }

    @Override
    public void download(String source, OutputStream target) throws IOException {
        download(Paths.get(source), target);
    }

    @Override
    public void rename(String source, String target, CopyOption... copyOptions) throws IOException {
        Files.move(Paths.get(source), Paths.get(target), copyOptions);
    }

    @Override
    public void remove(String source) throws IOException {
        Files.deleteIfExists(Paths.get(source));
    }

    @Override
    public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
        Files.copy(Paths.get(source), Paths.get(target), copyOptions);
    }

    @Override
    public DirectoryStream<Path> stream(String path, String glob) throws IOException {
        return Files.newDirectoryStream(Paths.get(path), glob);
    }

    @Override
    public DirectoryStream<Path> stream(String path, DirectoryStream.Filter<Path> filter) throws IOException {
        return Files.newDirectoryStream(Paths.get(path), filter);
    }

    @Override
    public Stream<Path> list(String path) throws IOException {
        return Files.list(Paths.get(path));
    }

    @Override
    public Stream<Path> walk(String path, FileVisitOption... options) throws IOException {
        return Files.walk(Paths.get(path), options);
    }

    @Override
    public Stream<Path> walk(String path, int maxdepth, FileVisitOption... options) throws IOException {
        return Files.walk(Paths.get(path), maxdepth, options);
    }

    private void upload(ReadableByteChannel source,
                        Path target,
                        Set<PosixFilePermission> dirPerms,
                        Set<PosixFilePermission> filePerms,
                        CopyOption... copyOptions) throws IOException {
        prepareForWrite(target, dirPerms, filePerms);
        transfer(source, Files.newByteChannel(target, prepareWriteOptions(copyOptions)));
    }

    private void download(Path source,
                          OutputStream outputStream,
                          int bufferSize) throws IOException {
        download(source, Channels.newChannel(outputStream), bufferSize);
    }

    private void download(Path source,
                          WritableByteChannel writableByteChannel,
                          int bufferSize) throws IOException {
        transfer(Files.newByteChannel(source, prepareReadOptions()), writableByteChannel);
    }

    private void download(Path source,
                          Path target,
                          int bufferSize,
                          CopyOption... copyOptions) throws IOException {
        prepareForWrite(target);
        transfer(Files.newByteChannel(source, prepareReadOptions(copyOptions)),
                Files.newByteChannel(target, prepareWriteOptions(copyOptions)));
    }

    private void prepareForWrite(Path path) throws IOException {
        if (path == null) {
            return;
        }
        Path parent = path.getParent();
        if (parent != null) {
            if (!Files.exists(parent)) {
                Files.createDirectories(parent);
            }
        }
        if (!Files.exists(path)) {
            Files.createFile(path);
        }
    }

    private void prepareForWrite(Path path,
                                 Set<PosixFilePermission> dirPerms,
                                 Set<PosixFilePermission> filePerms) throws IOException {
        if (path == null) {
            return;
        }
        Path parent = path.getParent();
        if (parent != null) {
            if (!Files.exists(parent)) {
                Files.createDirectories(parent);
            }
            PosixFileAttributeView posixFileAttributeView =
                    Files.getFileAttributeView(parent, PosixFileAttributeView.class);
            posixFileAttributeView.setPermissions(dirPerms);
        }
        if (!Files.exists(path)) {
            Files.createFile(path);
        }
        PosixFileAttributeView posixFileAttributeView =
                Files.getFileAttributeView(path, PosixFileAttributeView.class);
        posixFileAttributeView.setPermissions(filePerms);
    }

    private Set<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
        // ignore user copy options
        return EnumSet.of(StandardOpenOption.READ);
    }

    private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
        Set<? extends OpenOption> options = null;
        for (CopyOption copyOption : copyOptions) {
            if (copyOption == StandardCopyOption.REPLACE_EXISTING) {
                options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
            }
        }
        if (options == null) {
            // we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile()
            options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE);
        }
        return options;
    }

    private void transfer(ReadableByteChannel readableByteChannel,
                          WritableByteChannel writableByteChannel) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        int read;
        while ((read = readableByteChannel.read(buffer)) > 0) {
            buffer.flip();
            while (read > 0) {
                read -= writableByteChannel.write(buffer);
            }
            buffer.clear();
        }
    }
}
