/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.atlas.io;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.util.function.Function;

import org.apache.jena.atlas.RuntimeIOException;
import org.apache.jena.atlas.logging.FmtLog;

public class IOX {

    public static final Path currentDirectory = Path.of(".");

    /** A Consumer that can throw {@link IOException}. */
    @FunctionalInterface
    public interface IOConsumer<X> {
        void actionEx(X arg) throws IOException;
    }

    // Maybe change to java.io.UncheckedIOException
    /**
     * Convert an {@link IOException} into a {@link RuntimeIOException}.
     * <p>
     * Idiom:
     * <pre>
     *     catch(IOException ex) { throw new exception(ex); }
     * </pre>
     *
     * @param ioException
     * @return RuntimeIOException
     */
    public static RuntimeIOException exception(IOException ioException) {
        return new RuntimeIOException(ioException);
    }

    /**
     * Convert an {@link IOException} into a {@link RuntimeIOException}.
     * <p>
     * Idiom:
     * <pre>
     *     catch(IOException ex) { throw new exception("Oh dear", ex); }
     * </pre>
     *
     * @param message
     * @param ioException
     * @return RuntimeIOException
     */
    public static RuntimeIOException exception(String message, IOException ioException) {
        return new RuntimeIOException(message, ioException);
    }

    /** Run I/O code */
    @FunctionalInterface
    public interface ActionIO {
        void run() throws IOException;
    }

    /**
     * Run an action, converting an {@link IOException} into a
     * {@link RuntimeIOException}.
     * <p>
     * Idiom:
     *
     * <pre>
     *     run(()-&gt;...));
     * </pre>
     */
    public static void run(ActionIO action) {
        try {
            action.run();
        } catch (IOException e) {
            throw exception(e);
        }
    }

    /**
     * Write a file safely - the change happens (the function returns true) or
     * something went wrong (the function throws a runtime exception) and the file is
     * not changed.
     */
    public static boolean safeWrite(Path file, IOConsumer<OutputStream> writerAction) {
        Path tmp = createTempFile(file.toAbsolutePath().getParent(), file.getFileName().toString(), ".tmp");
        return safeWrite(file, tmp, writerAction);
    }

    /**
     * Write a file safely - the change happens (the function returns true) or
     * something went wrong (the function throws a runtime exception) and the file is
     * not changed. Note that the tmpfile must be in the same directory as the actual
     * file so an OS-atomic rename can be done.
     */
    public static boolean safeWrite(Path file, Path tmpFile, IOConsumer<OutputStream> writerAction) {
        try {
            try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmpFile))) {
                writerAction.actionEx(out);
            }
            move(tmpFile, file);
            return true;
        } catch (IOException ex) {
            throw IOX.exception(ex);
        }
    }

    /**
     * Write a file safely, but allow system to use copy-delete if the change can not
     * be done atomically. Prefer {@link #safeWrite} which requires an atomic move.
     */
    public static boolean safeWriteOrCopy(Path file, Path tmpFile, IOConsumer<OutputStream> writerAction) {
        try {
            try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmpFile))) {
                writerAction.actionEx(out);
            }
            moveAllowCopy(tmpFile, file);
            return true;
        } catch (IOException ex) {
            throw IOX.exception(ex);
        }
    }

    /** Delete a file. */
    public static void delete(Path path) {
        try {
            Files.delete(path);
        } catch (IOException ex) {
            FmtLog.error(IOX.class, ex, "IOException deleting %s", path);
            throw IOX.exception(ex);
        }
    }

    /** Atomically move a file. */
    public static void move(Path src, Path dst) {
        try {
            Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE);
        } catch (IOException ex) {
            FmtLog.error(IOX.class, ex, "IOException moving %s to %s", src, dst);
            throw IOX.exception(ex);
        }
    }

    /**
     * Move a file, allowing the system to copy-delete it if it can not be moved
     * atomically.
     */
    public static void moveAllowCopy(Path src, Path dst) {
        try {
            Files.move(src, dst);
        } catch (IOException ex) {
            FmtLog.error(IOX.class, ex, "IOException moving %s to %s", src, dst);
            throw IOX.exception(ex);
        }
    }

    public static void deleteAll(String start) {
        deleteAll(Paths.get(start));
    }

    /**
     * Delete everything from a {@code Path} start point, including the path itself.
     * Works on files or directories. Walks down the tree and deletes directories on
     * the way backup.
     */
    public static void deleteAll(Path start) {
        if ( !Files.exists(start) )
            return;
        try {
            Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
                    if ( e == null ) {
                        Files.delete(dir);
                        return FileVisitResult.CONTINUE;
                    } else {
                        throw e;
                    }
                }
            });
        } catch (IOException ex) {
            throw IOX.exception(ex);
        }
    }

    /**
     * Copy a file, not atomic. * Can copy to a directory or over an existing file.
     *
     * @param srcFilename
     * @param dstFilename
     */
    public static void copy(String srcFilename, String dstFilename) {
        Path src = Path.of(srcFilename);
        if ( !Files.exists(src) )
            throw new RuntimeIOException("No such file: " + srcFilename);

        Path dst = Path.of(dstFilename);
        if ( Files.isDirectory(dst) )
            dst = dst.resolve(src.getFileName());

        try {
            Files.copy(src, dst);
        } catch (IOException ex) {
            FmtLog.error(IOX.class, ex, "IOException copying %s to %s", srcFilename, dstFilename);
            throw IOX.exception(ex);
        }
    }

    /**
     * Create a directory - throw a runtime exception if there are any problems. This
     * function wraps {@code Files.createDirectory}.
     */
    public static void createDirectory(Path dir) {
        try {
            Files.createDirectory(dir);
        } catch (IOException ex) {
            throw IOX.exception(ex);
        }
    }

    /** Read the whole of a file */
    public static byte[] readAll(Path pathname) {
        try {
            return Files.readAllBytes(pathname);
        } catch (IOException ex) {
            throw IOX.exception(ex);
        }
    }

    /** Write the whole of a file */
    public static void writeAll(Path pathname, byte[] value) {
        try {
            Files.write(pathname, value, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
        } catch (IOException ex) {
            throw IOX.exception(ex);
        }
    }

// /**
// * Ensure a directory exists, creating the directory and any missing
// * directories on the path to it.
// */
// public static void ensureDirectory(Path dir, FileAttribute<? >... attrs) {
// if ( Files.exists(dir) ) {
// if ( ! Files.isDirectory(dir) )
// throw new DeltaFileException("Exists but not a directory: "+dir);
// }
// try { Files.createDirectories(dir, attrs); }
// catch (IOException ex) { new DeltaFileException("Failed to create directory:
// "+dir, ex);}
// }
//
// /**
// * Ensure a file exists - create an empty one if not. This operation does
// * not create a directory path to the file.
// */
// public static void ensureFile(Path filePath, FileAttribute<? >... attrs) {
// if ( Files.exists(filePath) ) {
// if ( ! Files.isRegularFile(filePath) )
// throw new DeltaFileException("Exists but not a regular file: "+filePath);
// }
// try { Files.createFile(filePath); }
// catch (IOException ex) { new DeltaFileException("Failed to create file:
// "+filePath, ex);}
// }

    /**
     * Return a temporary filename path.
     * <p>
     * This operation is thread-safe.
     */
    public static Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<? >...attrs) {
        try {
            return Files.createTempFile(dir, prefix, suffix, attrs);
        } catch (IOException ex) {
            throw IOX.exception(ex);
        }
    }

    /**
     * Generate a unique place related to path; Optionally, provide a mapping of old
     * name to new namebase. This method always adds "-1", "-2" etc.
     */
    public static Path uniqueDerivedPath(Path path, Function<String, String> basenameMapping) {
        String base = path.getFileName().toString();
        if ( basenameMapping != null )
            base = basenameMapping.apply(base);
        // Some large limit "just in case"
        for ( int x = 0 ; x < 10_000 ; x++ ) {
            String destname = base + "-" + x;
            Path destpath = path.resolveSibling(destname);
            if ( !Files.exists(destpath) )
                return destpath;
        }
        return null;
    }
}
