// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.

package com.yahoo.vespa.config.server.filedistribution;

import com.yahoo.config.FileReference;
import com.yahoo.config.model.api.FileDistribution;
import com.yahoo.io.IOUtils;
import com.yahoo.text.Utf8;
import net.jpountz.xxhash.XXHash64;
import net.jpountz.xxhash.XXHashFactory;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;

public class FileDirectory  {
    private static final Logger log = Logger.getLogger(FileDirectory.class.getName());
    private final File root;

    public FileDirectory() {
        this(FileDistribution.getDefaultFileDBPath());
    }

    public FileDirectory(File rootDir) {
        root = rootDir;
        try {
            ensureRootExist();
        } catch (IllegalArgumentException e) {
            log.warning("Failed creating directory in constructor, will retry on demand : " + e.toString());
        }
    }

    private void ensureRootExist() {
        if (! root.exists()) {
            if ( ! root.mkdir()) {
                throw new IllegalArgumentException("Failed creating root dir '" + root.getAbsolutePath() + "'.");
            }
        } else if (!root.isDirectory()) {
            throw new IllegalArgumentException("'" + root.getAbsolutePath() + "' is not a directory");
        }
    }

    static private class Filter implements FilenameFilter {
        @Override
        public boolean accept(File dir, String name) {
            return !".".equals(name) && !"..".equals(name) ;
        }
    }

    String getPath(FileReference ref) {
        return root.getAbsolutePath() + "/" + ref.value();
    }

    File getFile(FileReference reference) {
        ensureRootExist();
        File dir = new File(getPath(reference));
        if (!dir.exists()) {
            throw new IllegalArgumentException("File reference '" + reference.toString() + "' with absolute path '" + dir.getAbsolutePath() + "' does not exist.");
        }
        if (!dir.isDirectory()) {
            throw new IllegalArgumentException("File reference '" + reference.toString() + "' with absolute path '" + dir.getAbsolutePath() + "' is not a directory.");
        }
        File [] files = dir.listFiles(new Filter());
        if (files.length != 1) {
            StringBuilder msg = new StringBuilder();
            for (File f: files) {
                msg.append(f.getName()).append("\n");
            }
            throw new IllegalArgumentException("File reference '" + reference.toString() + "' with absolute path '" + dir.getAbsolutePath() + " does not contain exactly one file, but [" + msg.toString() + "]");
        }
        return files[0];
    }

    private Long computeReference(File file) throws IOException {
        byte [] wholeFile = IOUtils.readFileBytes(file);
        XXHash64 hasher = XXHashFactory.fastestInstance().hash64();
        return hasher.hash(ByteBuffer.wrap(wholeFile), hasher.hash(ByteBuffer.wrap(Utf8.toBytes(file.getName())), 0));
    }

    public FileReference addFile(File source) {
        try {
            Long hash = computeReference(source);
            FileReference reference = new FileReference(Long.toHexString(hash));
            return addFile(source, reference);
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public FileReference addFile(File source, FileReference reference) {
        ensureRootExist();
        try {
            File destinationDir = new File(root, reference.value());
            if (!destinationDir.exists()) {
                destinationDir.mkdir();
                Path tempDestinationDir = Files.createTempDirectory(root.toPath(), "writing");
                File destination = new File(tempDestinationDir.toFile(), source.getName());
                if (source.isDirectory())
                    IOUtils.copyDirectory(source, destination);
                else
                    IOUtils.copy(source, destination);
                if (!destinationDir.exists()) {
                    if ( ! tempDestinationDir.toFile().renameTo(destinationDir)) {
                        log.warning("Failed moving '" + tempDestinationDir.toFile().getAbsolutePath() + "' to '" + destination.getAbsolutePath() + "'.");
                    }
                } else {
                    IOUtils.copyDirectory(tempDestinationDir.toFile(), destinationDir, 1);
                }
                IOUtils.recursiveDeleteDir(tempDestinationDir.toFile());
            }
            return reference;
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

@@ -290,16 +290,47 @@
 OUtils;%0A
+import com.yahoo.log.LogLevel;%0A
 import c
@@ -448,16 +448,81 @@
 o.File;%0A
+import java.io.FileInputStream;%0Aimport java.io.FileOutputStream;%0A
 import j
@@ -620,45 +620,135 @@
 nio.
-file.Files;%0Aimport java.nio.file.Path
+channels.FileChannel;%0Aimport java.nio.file.Files;%0Aimport java.nio.file.Path;%0Aimport java.nio.file.attribute.BasicFileAttributes
 ;%0Aim
@@ -3804,32 +3804,65 @@
 ;%0A        try %7B%0A
+            logfileInfo(source);%0A
             File
@@ -3910,24 +3910,24 @@
 e.value());%0A
-
             
@@ -4332,28 +4332,24 @@
         
-IOUtils.
 copy
+File
 (source,
@@ -4947,32 +4947,32 @@
 OException e) %7B%0A
-
             thro
@@ -5021,11 +5021,794 @@
  %7D%0A    %7D
+%0A%0A    private void logfileInfo(File file ) throws IOException %7B%0A        BasicFileAttributes basicFileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);%0A        log.log(LogLevel.DEBUG, %22Adding file %22 + file.getAbsolutePath() + %22 (created %22 + basicFileAttributes.creationTime() +%0A                %22, modified %22 + basicFileAttributes.lastModifiedTime() +%0A                %22, size %22 + basicFileAttributes.size() + %22)%22);%0A    %7D%0A%0A    private static void copyFile(File source, File dest) throws IOException %7B%0A        try (FileChannel sourceChannel = new FileInputStream(source).getChannel();%0A             FileChannel destChannel = new FileOutputStream(dest).getChannel()) %7B%0A            destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());%0A        %7D%0A    %7D
 %0A%7D%0A

Copy file in a way that preserves the content