/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.io;

import java.io.*;
import java.util.Arrays;
import java.util.MissingResourceException;
import net.ranides.assira.annotations.PMD;
import net.ranides.assira.generic.PrivilegedActions;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.trace.StackInspector;

/**
 * Klasa pomocnicza udostępniające metody do wykonania bardzo powszechnych
 * czynności na plikach.
 * @author ranides
 */
@PMD.StaticHelper
public final class FileHelper {
    
    private static final int BUFFER_SIZE = 4096;

    private FileHelper() { }

    /**
     * Usuwa wskazany plik. Nie zwraca uwagi na to, czy plik jest katalogiem,
     * pustym katalogiem, czy czymkolwiek innym... po prostu usuwa.
     * @param file
     * @return
     */
    public static boolean delete(File file) {
        if(!file.exists()) { return true; }
        try {
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                for (int i = 0; i < files.length; i++) { delete(files[i]); }
            }
            return file.delete();
        } catch(SecurityException _e) {
            return false;
        }
    }

    /**
     * Ładuje zawartość podanego pliku i zwraca w postaci danych binarnych.
     * @param file
     * @return
     * @throws IOException
     */
    public static byte[] getContent(File file) throws IOException {
        RandomAccessFile istream = new RandomAccessFile(file, "r");
        byte[] content = new byte[ (int)istream.length() ];
        istream.readFully(content);
        istream.close();
        return content;
    }

    /**
     * Ładuje zawartość podanego pliku i zwraca w postaci danych binarnych.
     * @param file
     * @return
     * @throws IOException
     */
    public static byte[] getContent(String file) throws IOException {
        return getContent(new File(file));
    }

    /**
     * Zamyka podany plik. Funkcja odporna na {@code null}. Nie rzuca wyjątków.
     * <div class="message-note">{@code null} safe method</div>
     * @param object
     * @return {@code false} - tylko, jeśli zamknięcie pliku nie powiodło się.
     * Jeśli plik udało się zamknąć, albo przekazano do funkcji {@code null}, to zwraca {@code true}
     */
    public static boolean close(Closeable object) {
        try {
            if(null!=object) { object.close(); }
            return true;
        } catch (IOException _e) {
            return false;
        }
    }
    
    public static void close(Object object) throws IOException {
        if(object instanceof Closeable) { 
            ((Closeable)object).close(); 
        }
    }

    /**
     * Kopiuje dane z jednego strumienia do drugiego, używając do transportu
     * bufora o podanym rozmiarze. Oba strumienie są zamykane po zakończeniu
     * operacji.
     * @param output
     * @param input
     * @param bufsize
     * @throws IOException
     */
    public static void copy(OutputStream output, InputStream input, int bufsize) throws IOException {
        try {
            append(output, input, bufsize);
        } finally {
            close(output);
        }
    }

    /**
     * Kopiuje dane z jednego strumienia do drugiego, automatycznie określając
     * optymalny rozmiar bufora. <b>Note:</b> na ten moment rozmiar bufora
     * wynosi po prostu 4096. Oba strumienie są zamykane po zakończeniu
     * operacji.
     * @param output
     * @param input
     * @throws IOException
     */
    public static void copy(OutputStream output, InputStream input) throws IOException {
        copy(output, input, BUFFER_SIZE);
    }

    /**
     * Kopiuje dane z pliku źródłowego do docelowego, w razie potrzeby tworząc
     * plik docelowy. <b>Note:</b> funkcja tworząc plik nie próbuje tworzyć całej
     * koniecznej struktury katalogów.
     * @param output
     * @param input
     * @throws IOException
     */
    public static void copy(File output, File input) throws IOException {
        copy(new FileOutputStream(output), new FileInputStream(input));
    }

    /**
     * Kopiuje dane z pliku źródłowego do docelowego, w razie potrzeby tworząc
     * plik docelowy. <b>Note:</b> funkcja tworząc plik nie próbuje tworzyć całej
     * koniecznej struktury katalogów.
     * @param output
     * @param input
     * @throws IOException
     */
    public static void copy(String output, String input) throws IOException {
        copy(new FileOutputStream(output), new FileInputStream(input));
    }
    
    public static void append(OutputStream output, InputStream input, int bufsize) throws IOException {
        try {
            byte[] buffer = new byte[bufsize];
            int n;
            while( (n = input.read(buffer)) > 0 ) {
                output.write(buffer, 0, n);
            }
        } finally {
            close(input);
        }
    }
    
    public static void append(OutputStream output, InputStream input) throws IOException {
        append(output, input, BUFFER_SIZE);
    }

    /**
     * Funkcja porównuje binarnie zawartość podanych strumieni. Strumienie nie są
     * zamykane po zakończeniu operacji. Strumienie nie są przewijane na początek
     * przed rozpoczęciem operacji.
     * @param left
     * @param right
     * @param bufsize
     * @return
     * @throws IOException
     */
    public static boolean equals(InputStream left, InputStream right, int bufsize) throws IOException {
        byte[] bleft  = new byte[bufsize];
        byte[] bright = new byte[bufsize];
        while (true) {
            int ileft  = left.read(bleft, 0, bufsize);
            int iright = right.read(bright, 0, bufsize);
            if (ileft != iright) {
                return false;
            }
            if (ileft <= 0) {
                return true;
            }
            if( !Arrays.equals(bleft, bright) ) {
                return false;
            }
        }
    }

    /**
     * Funkcja porównuje binarnie zawartość podanych strumieni. Strumienie nie są
     * zamykane po zakończeniu operacji. Strumienie nie są przewijane na początek
     * przed rozpoczęciem operacji. <b>Note:</b> na ten moment rozmiar bufora
     * wynosi po prostu 4096.
     * @param left
     * @param right
     * @return
     * @throws IOException
     */
    public static boolean equals(InputStream left, InputStream right) throws IOException {
        return equals(left, right, BUFFER_SIZE);
    }
    
    public static boolean equals(File left, File right) throws IOException {
		if(left.equals(right)) {
			return true;
		}
		if(left.length() != right.length()) {
			return false;
		}
        InputStream istream1 = null;
        InputStream istream2 = null;
        try {
            istream1 = new FileInputStream(left);
            istream2 = new FileInputStream(right);
            return equals(istream1, istream2);
        } finally {
            close(istream1);
            close(istream2);
        }
    }

    /**
     * Wypakowuje z pliku JAR zasób o podanej nazwie do pliku zewnętrznego o
     * podanej lokalizacji.
     * @param clazz zasób jest szukany w tym samym pliku JAR, z którego załadowana
     * została podana klasa
     * @param resource nazwa zasobu
     * @param path lokalizacja pliku, do którego zostaną zapisane wypakowane dane.
     * @throws IOException
     */
    public static void extractResource(Class<?> clazz, String resource, File path) throws IOException {
        InputStream istream = null;
        OutputStream ostream = null;
        try {
            istream = clazz.getResourceAsStream(resource);
            if(null==istream) {
                throw new MissingResourceException("", clazz.getName(), resource);
            }
            ostream = new BufferedOutputStream(new FileOutputStream(path));
            copy(ostream, istream);
        } finally {
            close(ostream);
            close(istream);
        }
    }

    /**
     * Wypakowuje z pliku JAR zasób o podanej nazwie do pliku zewnętrznego o
     * podanej lokalizacji. Zasób jest szukany w tym samym pliku JAR, z którego
     * została załadowana klasa wywołująca metodę {@code extractResource} (mówiąc
     * obrazowo: z <i>bieżącego</i> pliku JAR).
     * @param resource nazwa zasobu
     * @param path lokalizacja pliku, do którego zostaną zapisane wypakowane dane.
     * @throws IOException
     */
    public static void extractResource(String resource, File path) throws IOException {
        extractResource(StackInspector.frameClass(StackInspector.CALLER), resource, path);
    }


    private static final class Li { // NOPMD - lazy init idiom

        private static final File LOCATION =  new File(PrivilegedActions.getProperty("java.io.tmpdir"));

        public static String path() {
            return LOCATION.getPath();
        }
        public static File location(String name) {
            return new File(LOCATION.getPath() + File.separator + name);
        }
        public static File location() {
            return LOCATION;
        }
    }


    /**
     * Tworzy plik tymczasowy wygenerowany na podstawie przekazanych argumentów i
     * zwraca jego lokalizację. Wygenerowana lokalizacja ma postać:
     * <pre> {@code %temp%/{package...}/{classname}/{prefix}{random}{.suffix} }</pre>
     * @param prefix
     * @param suffix
     * @param clazz
     * @return
     * @throws IOException
     */
    public static File createTempFile(String prefix, String suffix, Class<?> clazz) throws IOException {
        final String dir = getClassDir(clazz);
        final File file = Li.location(dir);
        if(!file.exists() && !file.mkdirs()) {
            throw new IOException("can't create directory: " + file);
        }
        if(!file.isDirectory()) {
            throw new IOException("path is not a directory: " + file);
        }
        return File.createTempFile(clazz.getSimpleName() + prefix, suffix, file);
    }

    /**
     * Tworzy plik tymczasowy wygenerowany na podstawie przekazanych argumentów i
     * zwraca jego lokalizację. Plik nie zawiera żadnych losowych fragmentów - wygenerowana
     * lokalizacja ma w pełni deterministyczną postać:
     * <pre> {@code %temp%/{package...}/{classname}{.suffix}}</pre>
     * Jeśli {@code suffix = null}, to plik przyjmuje rozszerzenie {@code ".tmp"}
     * @param suffix
     * @param clazz
     * @return
     * @throws IOException
     */
    public static File createTempFile(String suffix, Class<?> clazz) throws IOException {
        final String dir = getClassDir(clazz);
        final File file = Li.location(dir);
        if(!file.exists() && !file.mkdirs()) {
            throw new IOException("can't create directory: " + file);
        }
        if(!file.isDirectory()) {
            throw new IOException("path is not a directory: " + file);
        }
        File result = new File(Li.path() + File.separator +dir + File.separator + clazz.getSimpleName() + ValueUtils.or(suffix,".tmp"));
        if(!result.exists() && !result.createNewFile()) {
            throw new IOException("can't create file: " + file);
        }
        return result;
    }

    private static String getClassDir(Class<?> clazz) {
        return clazz.getPackage().getName().replace(".", File.separator);
    }

}
