/*
 * @(#)IOTools.java
 *
 * Copyright (c) 2002-2003 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.util;

import java.io.*;
import java.net.*;
import java.nio.channels.Channel;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Properties;
import java.util.logging.Level;

/**
 * I/O tools.
 *
 * @author Alex Usun
 */
public class IOTools {

    public static final int EOF = -1;

    public static IOException ioException(Throwable error) {
        return ExceptionTools.ioException(error);
    }

    public static InputStream skip(InputStream in, long n)
            throws IOException {
        while (n > 0) {
            long skipped = in.skip(n);

            if (skipped > 0) {
                n -= skipped;
            } else if (skipped == 0) {
                if (in.read() == -1) {
                    break;
                } else {
                    n--;
                }
            } else {
                throw new IOException("InputStream skip() error: a negative value returned.");
            }
        }

        return in;
    }

    public static void close(AutoCloseable closeable) {
        if (closeable == null) {
            return;
        }

        try {
            closeable.close();
        } catch (Throwable e) {
            logError("Error closing closable.", e);
        }
    }

    /**
     * Closes the specified stream without throwing any exception.
     *
     * @param stream stream to close
     */
    public static void close(InputStream stream) {
        if (stream == null) {
            return;
        }

        try {
            stream.close();
        } catch (Throwable e) {
            logError("Error closing stream.", e);
        }
    }

    /**
     * Closes the specified stream without throwing any exception.
     *
     * @param stream stream to close
     */
    public static void close(OutputStream stream) {
        if (stream == null) {
            return;
        }

        try {
            stream.close();
        } catch (Throwable e) {
            logError("Error closing stream.", e);
        }
    }

    public static void close(InputStream in, OutputStream out) {
        close(in);
        close(out);
    }

    /**
     * Closes the specified reader without throwing any exception.
     *
     * @param reader reader to close
     */
    public static void close(Reader reader) {
        if (reader == null) {
            return;
        }

        try {
            reader.close();
        } catch (Throwable e) {
            logError("Error closing reader.", e);
        }
    }

    /**
     * Closes the specified writer without throwing any exception.
     *
     * @param writer writer to close
     */
    public static void close(Writer writer) {
        if (writer == null) {
            return;
        }

        try {
            writer.close();
        } catch (Throwable e) {
            logError("Error closing writer.", e);
        }
    }

    /**
     * Closes the specified socket without throwing any exception.
     *
     * @param socket socket to close
     */
    public static void close(Socket socket) {
        if (socket == null) {
            return;
        }

        try {
            socket.close();
        } catch (Throwable e) {
            logError("Error closing socket.", e);
        }
    }

    public static void close(ServerSocket socket) {
        if (socket == null) {
            return;
        }

        try {
            socket.close();
        } catch (Throwable e) {
            logError("Error closing socket.", e);
        }
    }

    /**
     * Closes the specified socket without throwing any exception.
     *
     * @param socket socket to close
     */
    public static void close(DatagramSocket socket) {
        if (socket == null) {
            return;
        }

        try {
            socket.close();
        } catch (Throwable e) {
            logError("Error closing socket.", e);
        }
    }

    public static void close(HttpURLConnection connection) {
        if (connection == null) {
            return;
        }

        try {
            connection.disconnect();
        } catch (Throwable e) {
            logError("Error closing connection.", e);
        }
    }

    public static void close(Closeable target) {
        if (target == null) {
            return;
        }

        try {
            target.close();
        } catch (Throwable e) {
            logError("Error closing closeable.", e);
        }
    }

    /**
     * Closes random access file.
     *
     * @param file file
     */
    public static void close(RandomAccessFile file) {
        if (file == null) {
            return;
        }

        try {
            file.close();
        } catch (Throwable e) {
            logError("Error closing file.", e);
        }
    }

    /**
     * Closes channel.
     *
     * @param channel channel
     */
    public static void close(Channel channel) {
        if (channel == null) {
            return;
        }

        try {
            channel.close();
        } catch (Throwable e) {
            logError("Error closing channel.", e);
        }
    }

    public static long copy(InputStream in, OutputStream out, int bufferSize)
            throws IOException {
        byte[] buf = new byte[bufferSize];
        long total = 0;

        int read;
        while ((read = in.read(buf)) != EOF) {
            out.write(buf, 0, read);
            total += read;
        }

        return total;
    }

    public static long copy(InputStream in, OutputStream out)
            throws IOException {
        return copy(in, out, 1024);
    }

    public static long copy(InputStream in, File file, int bufferSize)
            throws IOException {
        try (OutputStream out = Files.newOutputStream(file.toPath())) {
            return copy(in, out, bufferSize);
        }
    }

    public static long copy(InputStream in, File file)
            throws IOException {
        return copy(in, file, 1024);
    }

    public static long copy(File file, OutputStream out, int bufferSize)
            throws IOException {
        try (InputStream in = Files.newInputStream(file.toPath())) {
            return copy(in, out, bufferSize);
        }
    }

    public static long copy(File file, OutputStream out)
            throws IOException {
        return copy(file, out, 1024);
    }

    public static long copy(Reader in, Writer out, int bufferSize)
            throws IOException {
        char[] buf = new char[bufferSize];
        long total = 0;

        int read;
        while ((read = in.read(buf)) != EOF) {
            out.write(buf, 0, read);
            total += read;
        }

        return total;
    }

    public static void copy(BufferedReader in, BufferedWriter out)
            throws IOException {
        String line;

        while ((line = in.readLine()) != null) {
            out.write(line);
            out.newLine();
        }

        out.flush();
    }

    public static long copy(File source, File destination)
            throws IOException {
        try (InputStream in = Files.newInputStream(source.toPath()); OutputStream out = Files.newOutputStream(destination.toPath())) {
            return copy(in, out);
        }
    }

    public static File copyChannel(File source, File destination)
            throws IOException {
        FileChannel in = null;
        WritableByteChannel out = null;

        try {
            in = new FileInputStream(source).getChannel();
            out = Channels.newChannel(Files.newOutputStream(destination.toPath()));
            in.transferTo(0, in.size(), out);

            return destination;
        } finally {
            close(in);
            close(out);
        }
    }

    public static long copy(URL source, File destination)
            throws IOException {
        try (InputStream in = source.openStream(); OutputStream out = Files.newOutputStream(destination.toPath())) {
            return copy(in, out);
        }
    }

    public static long copy(File source, URL destination)
            throws IOException {
        try (InputStream in = Files.newInputStream(source.toPath()); OutputStream out = destination.openConnection().getOutputStream()) {
            return copy(in, out);
        }
    }

    public static int read(InputStream in)
            throws IOException {
        int read = in.read();

        if (read == EOF) {
            throw new EOFException();
        }

        return read & 0xFF;
    }

    public static byte[] read(byte[] data, InputStream in)
            throws IOException {
        return read(data, 0, data.length, in);
    }

    public static byte[] read(byte[] data, int offset, int length, InputStream in)
            throws IOException {
        for (int read; length > 0; offset += read, length -= read) {
            read = in.read(data, offset, length);
            if (read == EOF) {
                throw new EOFException();
            }
        }

        return data;
    }

    public static byte[] readFully(InputStream in)
            throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int read;
        while ((read = in.read()) != EOF) {
            buffer.write(read);
        }

        return buffer.toByteArray();
    }

    public static String readFully(InputStream in, Charset charset)
            throws IOException {
        byte[] data = readFully(in);
        return new String(data, charset);
    }

    public static String readFully(Reader reader)
            throws IOException {
        StringBuilder buffer = new StringBuilder();
        int read;
        while ((read = reader.read()) != EOF) {
            buffer.append((char) read);
        }

        return buffer.toString();
    }

    public static byte[] readSequence(InputStream in, byte[] eol, boolean leaveEol)
            throws IOException {
        Buffer buf = new Buffer(64);
        int ret = readSequence(in, buf, eol, leaveEol, Integer.MAX_VALUE);
        return ret != EOF ? buf.toByteArray() : null;
    }

    public static int readSequence(InputStream in, Buffer buffer, byte[] eol, boolean leaveEol, int maxSize)
            throws IOException {
        int read;
        boolean somethingRead = false;
        int total = 0;

        while ((read = in.read()) != EOF) {
            somethingRead = true;
            ++total;
            buffer.append((byte) read);
            boolean eolFound = false;
            int index = 0;

            while (read != EOF && read == (eol[index] & 0xFF)) {
                ++index;

                if (index == eol.length) {
                    eolFound = true;
                    break;
                }

                read = in.read();
                ++total;
                buffer.append((byte) read);
            }

            if (read == EOF || eolFound || total >= maxSize) {
                break;
            }
        }

        if (!leaveEol && somethingRead && buffer.endsWith(eol)) {
            buffer.delete(buffer.getLength() - eol.length);
            total -= eol.length;
        }

        return somethingRead ? total : EOF;
    }

    public static int readSequence(Reader reader, StringBuffer buffer, String eol, boolean leaveEol)
            throws IOException {
        return readSequence(reader, buffer, eol, leaveEol, Integer.MAX_VALUE);
    }

    public static int readSequence(Reader reader, StringBuffer buffer, String eol, boolean leaveEol, int maxSize)
            throws IOException {
        int read;
        boolean somethingRead = false;
        int total = 0;

        while ((read = reader.read()) != EOF) {
            somethingRead = true;
            ++total;
            buffer.append((char) read);
            boolean eolFound = false;
            int index = 0;

            while (read != EOF && read == (eol.charAt(index) & 0xFFFF)) {
                ++index;

                if (index == eol.length()) {
                    eolFound = true;
                    break;
                }

                read = reader.read();
                ++total;
                buffer.append((char) read);
            }

            if (read == EOF || eolFound || total >= maxSize) {
                break;
            }
        }

        if (!leaveEol && somethingRead && buffer.toString().lastIndexOf(eol) == (buffer.length() - eol.length())) {
            buffer.delete(buffer.length() - eol.length(), buffer.length());
            total -= eol.length();
        }

        return somethingRead ? total : EOF;
    }

    public static Properties readProperties(InputStream in, Properties defaults)
            throws IOException {
        Properties properties = new Properties(defaults);
        properties.load(new InputStreamReader(in, StandardCharsets.UTF_8));
        return properties;
    }

    public static void writeProperties(Properties properties, String header, OutputStream out)
            throws IOException {
        OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
        properties.store(writer, header);
        writer.flush();
    }

    public static byte[] readFile(InputStream in)
            throws IOException {
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            copy(in, out);

            return out.toByteArray();
        } finally {
            close(in);
        }
    }

    public static String readFile(Reader reader)
            throws IOException {
        try {
            StringWriter writer = new StringWriter();
            copy(reader, writer, 1024);

            return writer.toString();
        } finally {
            close(reader);
        }
    }

    public static byte[] readFile(File file)
            throws IOException {
        return readFile(Files.newInputStream(file.toPath()));
    }

    public static String readFile(File file, Charset charset)
            throws IOException {
        try (InputStream in = Files.newInputStream(file.toPath())) {
            return readFile(new InputStreamReader(in, charset));
        }
    }

    public static Properties readPropertiesFile(InputStream in, Properties defaults)
            throws IOException {
        try {
            return readProperties(in, defaults);
        } finally {
            close(in);
        }
    }

    public static Properties readPropertiesFile(File file, Properties defaults)
            throws IOException {
        return readPropertiesFile(Files.newInputStream(file.toPath()), defaults);
    }

    public static Properties readPropertiesFile(Class aClass, String path, Properties defaults)
            throws IOException {
        return readPropertiesFile(aClass.getResourceAsStream(path), defaults);
    }

    public static void writeFile(byte[] data, OutputStream out)
            throws IOException {
        try {
            out.write(data);
            out.flush();
        } finally {
            close(out);
        }
    }

    public static void writeFile(String data, Writer writer)
            throws IOException {
        try {
            writer.write(data);
        } finally {
            close(writer);
        }
    }

    public static void writeFile(InputStream in, OutputStream out)
            throws IOException {
        try {
            copy(in, out, 1024);
            out.flush();
        } finally {
            close(in, out);
        }
    }

    public static File writeFile(byte[] data, File destination)
            throws IOException {
        writeFile(data, Files.newOutputStream(destination.toPath()));
        return destination;
    }

    public static File writeFile(InputStream in, File destination)
            throws IOException {
        try (OutputStream out = Files.newOutputStream(destination.toPath())) {
            copy(in, out, 1024);
            return destination;
        } finally {
            close(in);
        }
    }

    public static void writeFile(File source, OutputStream out)
            throws IOException {
        try (InputStream in = Files.newInputStream(source.toPath())) {
            copy(in, out, 1024);
            out.flush();
        } finally {
            close(out);
        }
    }

    public static void writeFile(String data, File file, Charset charset)
            throws IOException {
        try (OutputStream out = Files.newOutputStream(file.toPath())) {
            writeFile(data, new OutputStreamWriter(out, charset));
        }
    }

    public static void writePropertiesFile(Properties properties, String header, OutputStream out)
            throws IOException {
        try {
            writeProperties(properties, header, out);
            out.flush();
        } finally {
            close(out);
        }
    }

    public static void writePropertiesFile(Properties properties, String header, File file)
            throws IOException {
        writePropertiesFile(properties, header, Files.newOutputStream(file.toPath()));
    }

    private static void logError(String message, Throwable error) {
        java.util.logging.Logger.getLogger("com.jscape.util").log(Level.WARNING, message, error);
    }

    /**
     * Private constructor to avoid unnecessary instantiation.
     */
    private IOTools() {
        // empty
    }
}
