/* A collection of IO utility functions.
 *
 *  Copyright (C) 2010, 2011 Darrell Karbott
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.0 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *  Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
 *
 *  This file was developed as component of
 * "fniki" (a wiki implementation running over Freenet).
 */

package wormarc;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.util.Random;

public class IOUtil {
    private final static Random sRandom = new Random();
    private final static int BUF_LEN = 1024 * 32;

    // Don't change without reviewing code.
    public final static String UTF8 = "utf8";

    public static void silentlyClose(InputStream stream) {
        if (stream == null) {
            return;
        }
        try {
            stream.close();
        } catch (IOException ioe) {
            // NOP
        }
    }

    public final static void copyAndClose(InputStream fromStream, OutputStream toStream) throws IOException {
        if (fromStream == null || toStream == null) {
            throw new IllegalArgumentException();
        }

        try {
            byte[] buffer = new byte[BUF_LEN];
            while (true) {
                int bytesRead = fromStream.read(buffer);
                if (bytesRead == -1) {
                    return;
                }
                toStream.write(buffer, 0, bytesRead);
            }
        }
        finally {
            try {
                fromStream.close();
            }
            finally {
                toStream.close();
            }
        }
    }

    // Closes stream.
    public final static LinkDigest getFileDigest(InputStream fromStream) throws IOException {
        MessageDigest sha1 = null;
        try {
            try {
                sha1 = MessageDigest.getInstance("SHA");
            }
            catch (NoSuchAlgorithmException nsae) {
                throw new IOException("Couldn't load SHA1 algorithm.");
            }

            // DCI: Better to use a wrapper stream filter from the java crypto lib?
            byte[] buffer = new byte[BUF_LEN];
            while (true) {
                int bytesRead = fromStream.read(buffer);
                if (bytesRead == -1) {
                    break;
                }
                sha1.update(buffer, 0, bytesRead);
            }
            return new LinkDigest(sha1.digest());
        }
        finally {
            fromStream.close();
        }
    }

    public final static void copyAndClose(InputStream fromStream, String toFileName) throws IOException {
        copyAndClose(fromStream, new FileOutputStream(toFileName));
    }

    public final static byte[] readAndClose(InputStream fromStream) throws IOException {
        if (fromStream == null) {
            throw new IllegalArgumentException();
        }

        ByteArrayOutputStream toStream = new ByteArrayOutputStream();
        copyAndClose(fromStream, toStream);

        byte[] bytes = toStream.toByteArray();
        return bytes;
    }

    public final static byte[] readFully(String inputFile) throws IOException {
        return readAndClose(new FileInputStream(inputFile));
    }

    public final static void writeFully(byte[] data, String outputFile) throws IOException {
        copyAndClose(new ByteArrayInputStream(data), new FileOutputStream(outputFile));
    }

    public final static String readUtf8StringAndClose(InputStream fromStream) throws IOException {
        ByteArrayOutputStream toStream = new ByteArrayOutputStream();
        copyAndClose(fromStream, toStream);
        return new String(toStream.toByteArray(), UTF8);
    }

    public final static InputStream toStreamAsUtf8(String value) throws IOException {
        return new ByteArrayInputStream(value.getBytes(UTF8));
    }

    // ATTRIBUTION: mmyers, SO
    // http://stackoverflow.com/questions/140131/convert-a-string-representation-of-a-hex-dump-to-a-byte-array-using-java
    public final static byte[] fromHexString(final String encoded) {
        if ((encoded.length() % 2) != 0)
            throw new IllegalArgumentException("Input string must contain an even number of characters");

        final byte result[] = new byte[encoded.length()/2];
        final char enc[] = encoded.toCharArray();
        for (int i = 0; i < enc.length; i += 2) {
            StringBuilder curr = new StringBuilder(2);
            curr.append(enc[i]).append(enc[i + 1]);
            result[i/2] = (byte) Integer.parseInt(curr.toString(), 16);
        }
        return result;
    }

    // ATTRIBUTION: Peter Lawrey, SO
    // http://stackoverflow.com/questions/332079/in-java-how-do-i-convert-a-byte-array-to-a-string-of-hex-digits-while-keeping-le
    public final static String toHexString(byte[] bytes, int maxBytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%1$02x", b));
            maxBytes -= 1;
            if (maxBytes == 0) {
                break;
            }
        }
        return sb.toString();
    }

    public final static String randomHexString(int length) {
        if (length < 1) {
            throw new IllegalArgumentException();
        }
        byte[] bytes = new byte[length];
        sRandom.nextBytes(bytes);
        return toHexString(bytes, bytes.length);
    }

    // ATTRIBUTION: erikson, SO
    // http://stackoverflow.com/questions/779519/delete-files-recursively-in-java
    public final static void delete(File f) throws IOException {
        if (f.isDirectory()) {
            for (File c : f.listFiles()) {
                delete(c);
            }
        }
        if (!f.delete()) {
            throw new FileNotFoundException("Failed to delete file: " + f);
        }
    }
    public final static void delete(String fileOrDirectory) throws IOException { delete( new File(fileOrDirectory) ); }

    ////////////////////////////////////////////////////////////
    // Binary IO
    ////////////////////////////////////////////////////////////

    public static byte[] readBytes(DataInputStream source, int numberOfBytes) throws IOException {
        byte[] data = new byte[numberOfBytes];
        int count = 0;
        while (count < data.length) {
            int bytesRead = source.read(data, count, data.length - count);
            if (bytesRead == -1) {
                throw new IOException("Unexpected EOF reading bytes");
            }
            count += bytesRead;
        }
        return data;
    }

    // DCI: remove? is there other code that should be using this?
    // public static void writeString(DataOutputStream outputStream, String value) throws IOException {
    //     byte[] bytes = value.getBytes(UTF8);
    //     if (bytes.length > 32767) { // DCI: is this required?
    //         throw new IOException("Length doesn't fit in a signed short");
    //     }

    //     outputStream.writeShort(bytes.length);
    //     outputStream.write(bytes);
    // }

    public static String readString(DataInputStream source) throws IOException {
        int length = source.readShort();
        if (length > 32767) { // DCI: is this required?
            throw new IOException("Length doesn't fit in a signed short");
        }
        return new String(readBytes(source, length), UTF8);
    }
}