package org.xbib.files.ftp.fs.spi;

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.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
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.util.stream.Stream;
import org.xbib.files.FileService;
import org.xbib.files.FileWalker;
import org.xbib.files.WrappedDirectoryStream;

public class FTPFileService 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--");

    private final URI uri;

    private final Map<String, ?> env;

    public FTPFileService(URI uri, Map<String, ?> env) {
        this.uri = uri;
        this.env = env;
    }

    @Override
    public Boolean exists(String path) throws IOException {
        return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path)));
    }

    @Override
    public Boolean isExecutable(String path) throws IOException {
        return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path)));
    }

    @Override
    public Boolean isDirectory(String path) throws IOException {
        return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path)));
    }

    @Override
    public Boolean isRegularFile(String path) throws IOException {
        return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path)));
    }

    @Override
    public Boolean isHidden(String path) throws IOException {
        return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path)));
    }

    @Override
    public Boolean isSameFile(String path1, String path2) throws IOException {
        return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2)));
    }

    @Override
    public Boolean isSymbolicLink(String path) throws IOException {
        return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path)));
    }

    @Override
    public Boolean isReadable(String path) throws IOException {
        return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path)));
    }

    @Override
    public Boolean isWritable(String path) throws IOException {
        return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path)));
    }

    @Override
    public void createFile(String path, FileAttribute<?>... attributes) throws IOException {
        performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes));
    }

    @Override
    public void createDirectory(String path, FileAttribute<?>... attributes) throws IOException {
        performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes));
    }

    @Override
    public void createDirectories(String path, FileAttribute<?>... attributes) throws IOException {
        performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes));
    }

    @Override
    public void setAttribute(String path, String attribute, Object value) throws IOException {
        performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value));
    }

    @Override
    public Object getAttribute(String path, String attribute) throws IOException {
        return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute));
    }

    @Override
    public void setPermissions(String path, Set<PosixFilePermission> permissions) throws IOException {
        performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
    }

    @Override
    public Set<PosixFilePermission> getPermissions(String path) throws IOException {
        return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path)));
    }

    @Override
    public void setLastModifiedTime(String path, Instant lastModified) throws IOException {
        performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), FileTime.from(lastModified)));
    }

    @Override
    public Instant getLastModifiedTime(String path) throws IOException{
        return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path)).toInstant());
    }

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

    @Override
    public PosixFileAttributes getPosixFileAttributes(String path) throws IOException {
        return performWithContext(ctx -> Files.getFileAttributeView(ctx.fileSystem.getPath(path),
                PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes());
    }

    @Override
    public void setOwner(String path, String owner) throws IOException {
        performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path),
                ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByName(owner)));
    }

    @Override
    public String getOwner(String path) throws IOException {
        return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path)).getName());
    }


    @Override
    public void setGroup(String path, String group) throws IOException {
        performWithContext(ctx -> {
            PosixFileAttributeView view = Files.getFileAttributeView(ctx.fileSystem.getPath(path),
                    PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
            view.setGroup(ctx.fileSystem.getUserPrincipalLookupService().lookupPrincipalByGroupName(group));
            return null;
        });
    }

    @Override
    public String getGroup(String path) throws IOException {
        return performWithContext(ctx -> Files.getFileAttributeView(ctx.fileSystem.getPath(path),
                PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS).readAttributes().group().getName());
    }

    @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 {
        performWithContext(ctx -> {
            upload(ctx, Files.newByteChannel(source), target, dirPerms, filePerms, copyOptions);
            return null;
        });
    }

    @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 {
        performWithContext(ctx -> {
            upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target),
                    dirPerms, filePerms, copyOptions);
            return null;
        });
    }

    @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 {
        performWithContext(ctx -> {
            upload(ctx, Channels.newChannel(source), target, dirPerms, filePerms, copyOptions);
            return null;
        });
    }

    @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 {
        performWithContext(ctx -> {
            upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target),
                    dirPerms, filePerms, copyOptions);
            return null;
        });
    }

    @Override
    public void download(Path source, Path target, CopyOption... copyOptions) throws IOException {
        performWithContext(ctx -> {
            download(ctx, source, target, copyOptions);
            return null;
        });
    }

    @Override
    public void download(String source, Path target, CopyOption... copyOptions) throws IOException {
        performWithContext(ctx -> {
            download(ctx, ctx.fileSystem.getPath(source), target, copyOptions);
            return null;
        });
    }

    @Override
    public void download(Path source, OutputStream target) throws IOException {
        performWithContext(ctx -> {
            download(ctx, source, target);
            return null;
        });
    }

    @Override
    public void download(String source, OutputStream target) throws IOException {
        performWithContext(ctx -> {
            Files.copy(ctx.fileSystem.getPath(source), target);
            return null;
        });
    }

    @Override
    public DirectoryStream<Path> stream(String path, String glob) throws IOException {
        FTPContext ctx = new FTPContext(uri, env);
        return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), glob));
    }

    @Override
    public DirectoryStream<Path> stream(String path, DirectoryStream.Filter<Path> filter) throws IOException {
        FTPContext ctx = new FTPContext(uri, env);
        return new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter));
    }

    @Override
    public Stream<Path> list(String path) throws IOException {
        FTPContext ctx = new FTPContext(uri, env);
        return FileWalker.list(new WrappedDirectoryStream<>(ctx, Files.newDirectoryStream(ctx.fileSystem.getPath(path))));
    }

    @Override
    public Stream<Path> walk(String path, FileVisitOption... options) throws IOException {
        FTPContext ctx = new FTPContext(uri, env);
        return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), Integer.MAX_VALUE, options);
    }

    @Override
    public Stream<Path> walk(String path, int maxdepth, FileVisitOption... options) throws IOException {
        FTPContext ctx = new FTPContext(uri, env);
        return FileWalker.walk(ctx, ctx.fileSystem.getPath(path), maxdepth, options);
    }

    @Override
    public void copy(String source, String target, CopyOption... copyOptions) throws IOException {
        performWithContext(ctx -> {
            Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
            return null;
        });
    }

    @Override
    public void rename(String source, String target, CopyOption... copyOptions) throws IOException {
        performWithContext(ctx -> {
            Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions);
            return null;
        });
    }

    @Override
    public void remove(String source) throws IOException {
        performWithContext(ctx -> {
            Files.deleteIfExists(ctx.fileSystem.getPath(source));
            return null;
        });
    }

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

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

    private void download(FTPContext ctx,
                          Path source,
                          WritableByteChannel writableByteChannel) throws IOException {
        transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel);
    }

    private void download(FTPContext ctx,
                          Path source,
                          Path target,
                          CopyOption... copyOptions) throws IOException {
        prepareForWrite(target);
        transfer(ctx.provider.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();
        }
    }

    private <T> T performWithContext(WithContext<T> action) throws IOException {
        FTPContext ctx = null;
        try {
            if (uri != null) {
                ctx = new FTPContext(uri, env);
                return action.perform(ctx);
            } else {
                return null;
            }
        } finally {
            if (ctx != null) {
                ctx.close();
            }
        }
    }
}
