package com.srbenoit.util;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Properties;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import com.srbenoit.log.LoggedObject;

/**
 * Utility class to load resources, line-oriented files, properties files, and images. This class
 * should be able to load from a local file or a file in a JAR in the classpath, using a given
 * class a the base of the package in which to search for the resource.
 */
public final class ResourceLoader extends LoggedObject {

    /** the file extension for properties files */
    private static final String PROPS_EXTENSION = ".properties";

    /**
     * Private constructor to prevent instantiation.
     */
    private ResourceLoader() {

        super();
    }

    /**
     * Loads a text file, storing the file contents in a <code>String</code>. Lines in the returned
     * file are separated by single '\n' characters regardless of the line terminator in the source
     * file. The last line will end with a '\n' character, whether or not there was one in the
     * input file.
     *
     * @param   file  the file to read
     * @return  the loaded file contents, or <code>null</code> if unable to load
     */
    public static String loadFile(final File file) {

        InputStream input;
        BufferedReader reader;
        String line;
        StringBuilder builder;
        String result = null;

        try {
            input = new FileInputStream(file);

            try {
                reader = new BufferedReader(new InputStreamReader(input));

                builder = new StringBuilder((int) file.length());

                try {
                    line = reader.readLine();

                    while (line != null) {
                        builder.append(line);
                        builder.append("\n");
                        line = reader.readLine();
                    }

                    result = builder.toString();
                } catch (Exception e) {
                    logException("load file 1", file.getName(), e);
                }

                close(reader, file.getName());
            } catch (Exception e) {
                logException("load file 2", file.getName(), e);
            } finally {
                close(input, file.getName());
            }
        } catch (Exception e) {
            logException("load file 3", file.getName(), e);
        }

        return result;
    }

    /**
     * Loads a text file, storing the file contents in a <code>String</code>. Lines in the returned
     * file are separated by single '\n' characters regardless of the line terminator in the source
     * file. The last line will end with a '\n' character, whether or not there was one in the
     * input file.
     *
     * @param   caller  the class of the object making the call, so that relative resource paths
     *                  are based on the caller's position in the source tree
     * @param   name    the name of the file to read
     * @return  the loaded file contents, or <code>null</code> if unable to load
     */
    public static String loadFile(final Class<?> caller, final String name) {

        InputStream input;
        BufferedReader reader;
        String line;
        StringBuilder builder;
        String result = null;

        input = openInputStream(caller, name, true);

        if (input != null) {

            try {
                reader = new BufferedReader(new InputStreamReader(input));

                builder = new StringBuilder(1024);

                try {
                    line = reader.readLine();

                    while (line != null) {
                        builder.append(line);
                        builder.append("\n");
                        line = reader.readLine();
                    }

                    result = builder.toString();
                } catch (Exception e) {
                    logException("load file 4", name, e);
                }

                close(reader, name);
            } catch (Exception e) {
                logException("load file 5", name, e);
            } finally {
                close(input, name);
            }
        }

        return result;
    }

    /**
     * Loads a text file, storing the resulting lines of text in a <code>String</code> array.
     *
     * @param   caller  the class of the object making the call, so that relative resource paths
     *                  are based on the caller's position in the source tree
     * @param   name    the name of the file to read
     * @return  the loaded file contents, or <code>null</code> if unable to load
     */
    public static String[] loadFileLines(final Class<?> caller, final String name) {

        InputStream input;
        BufferedReader reader;
        String line;
        ArrayList<String> lines;
        String[] result;

        input = getInputStream(caller, name);

        if (input == null) {
            result = null;
        } else {

            try {
                reader = new BufferedReader(new InputStreamReader(input));

                lines = new ArrayList<String>(100);

                try {
                    line = reader.readLine();

                    while (line != null) {
                        lines.add(line);
                        line = reader.readLine();
                    }

                    result = new String[lines.size()];
                    lines.toArray(result);
                } catch (Exception e) {
                    logException("read from", name, e);
                    result = null;
                } finally {
                    close(reader, name);
                }
            } catch (Exception e) {
                logException("create reader for", name, e);
                result = null;
            } finally {
                close(input, name);
            }
        }

        return result;
    }

    /**
     * Loads a binary file, storing the resulting data in as<code>byte</code> array.
     *
     * @param   caller  the class of the object making the call, so that relative resource paths
     *                  are based on the caller's position in the source tree
     * @param   name    the name of the file to read
     * @return  the loaded file contents, or <code>null</code> if unable to load
     */
    public static byte[] loadFileBytes(final Class<?> caller, final String name) {

        InputStream input;
        ByteArrayOutputStream baos;
        byte[] buffer;
        int count;
        byte[] result;

        input = getInputStream(caller, name);

        if (input == null) {
            result = null;
        } else {
            buffer = new byte[1024];
            baos = new ByteArrayOutputStream();

            try {
                count = input.read(buffer);

                while (count != -1) {
                    baos.write(buffer, 0, count);
                    count = input.read(buffer);
                }

                baos.close();
                result = baos.toByteArray();
            } catch (Exception e) {
                logException("read from", name, e);
                result = null;
            }

            close(input, name);
        }

        return result;
    }

    /**
     * Reads a single image file into a buffered image.
     *
     * @param   caller  the class of the object making the call, so that relative resource paths
     *                  are based on the caller's position in the source tree
     * @param   path    the resource path of the image file
     * @return  the loaded buffered image
     */
    public static BufferedImage loadImage(final Class<?> caller, final String path) {

        InputStream input;
        BufferedImage img = null;

        input = openInputStream(caller, path, true);

        if (input != null) {

            try {
                img = ImageIO.read(input);
            } catch (Exception e) {
                logException("load image", path, e);
            } finally {
                close(input, path);
            }
        }

        return img;
    }

    /**
     * Finds and opens the appropriate resource bundle for a given locale. This locale may change
     * if the user selects different languages from the interface, in which case the GUI will be
     * rebuilt with the new settings.
     *
     * <p>If the resource bundle cannot be loaded for any reason, a set of default settings will be
     * generated and returned.
     *
     * @param   dir   the directory in which to find the properties file
     * @param   base  the base name (without language extension) of the properties file
     * @return  the opened resources, as a <code>Properties</code> object
     */
    public static Properties loadProperties(final File dir, final String base) {

        Locale locale;
        String path;
        InputStream input;
        Properties res = null;

        locale = Locale.getDefault();

        // Now, look for a file qualified with the locale name, then for a file
        // with no qualifications.
        path = base + "_" + locale.getLanguage() + PROPS_EXTENSION;
        input = openInputStream(dir, path, false);

        if (input == null) {
            path = base + PROPS_EXTENSION;
            input = openInputStream(dir, path, true);
        }

        if (input != null) {
            res = new Properties();

            try {
                res.load(input);
            } catch (IOException e) {
                logException("load properties", path, e);
            } finally {
                close(input, path);
            }
        }

        return res;
    }

    /**
     * Finds and opens the appropriate resource bundle for a given locale. This locale may change
     * if the user selects different languages from the interface, in which case the GUI will be
     * rebuilt with the new settings.
     *
     * <p>If the resource bundle cannot be loaded for any reason, a set of default settings will be
     * generated and returned.
     *
     * @param   caller  the class of the object making the call, so that relative resource paths
     *                  are based on the caller's position in the source tree
     * @param   base    the base name (without language extension) of the resource bundle
     * @return  the opened resources, as a <code>Properties</code> object
     */
    public static Properties loadProperties(final Class<?> caller, final String base) {

        Locale locale;
        String path;
        InputStream input;
        Properties res = null;

        locale = Locale.getDefault();

        // Now, look for a file qualified with the locale name, then for a file
        // with no qualifications.
        path = base + "_" + locale.getLanguage() + PROPS_EXTENSION;
        input = openInputStream(caller, path, false);

        if (input == null) {
            path = base + PROPS_EXTENSION;
            input = openInputStream(caller, path, true);
        }

        if (input != null) {
            res = new Properties();

            try {
                res.load(input);
            } catch (IOException e) {
                logException("load properties", path, e);
            } finally {
                close(input, path);
            }
        }

        return res;
    }

    /**
     * Finds and opens the appropriate resource bundle for a given locale. This locale may change
     * if the user selects different languages from the interface, in which case the GUI will be
     * rebuilt with the new settings.
     *
     * <p>If the resource bundle cannot be loaded for any reason, a set of default settings will be
     * generated and returned.
     *
     * @param   caller  the class of the object making the call, so that relative resource paths
     *                  are based on the caller's position in the source tree
     * @param   base    the base name (without language extension) of the resource bundle
     * @param   def     a set of defaults in case the resources could not be found
     * @return  the opened resources, as a <code>Properties</code> object
     */
    public static Properties loadProperties(final Class<?> caller, final String base,
        final Properties def) {

        Locale locale;
        String path;
        InputStream input;
        Properties res = null;

        locale = Locale.getDefault();

        // Now, look for a file qualified with the locale name, then for a file
        // with no qualifications.
        path = base + "_" + locale.getLanguage() + PROPS_EXTENSION;
        input = openInputStream(caller, path, false);

        if (input == null) {
            path = base + PROPS_EXTENSION;
            input = openInputStream(caller, path, true);
        }

        if (input != null) {
            res = new Properties(def);

            try {
                res.load(input);
            } catch (IOException e) {
                logException("load properties", path, e);
            } finally {
                close(input, path);
            }
        }

        if (res == null) {
            res = def; // Use defaults if loading failed
        }

        return res;
    }

    /**
     * Obtains an input stream for a particular resource.
     *
     * @param   caller  the class of the object making the call, so that relative resource paths
     *                  are based on the caller's position in the source tree
     * @param   name    the name of the resource to read
     * @return  the input stream, or null if unable to open
     */
    public static InputStream getInputStream(final Class<?> caller, final String name) {

        return openInputStream(caller, name, true);
    }

    /**
     * Obtains an input stream for a particular resource.
     *
     * @param   caller     the class of the object making the call, so that relative resource paths
     *                     are based on the caller's position in the source tree
     * @param   name       the name of the resource to read
     * @param   logErrors  true to log errors; false otherwise
     * @return  the input stream, or null if unable to open
     */
    public static InputStream getInputStream(final Class<?> caller, final String name,
        final boolean logErrors) {

        return openInputStream(caller, name, logErrors);
    }

    /**
     * Obtains an input stream for a particular resource.
     *
     * @param   caller     the class of the object making the call, so that relative resource paths
     *                     are based on the caller's position in the source tree
     * @param   name       the name of the resource to read
     * @param   logErrors  true to log errors; false otherwise
     * @return  the input stream, or null if unable to open
     */
    private static InputStream openInputStream(final Class<?> caller, final String name,
        final boolean logErrors) {

        InputStream input;
        File file;

        input = caller.getResourceAsStream(name);

        if (input == null) {
            input = Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
        }

        if (input == null) {
            input = ClassLoader.getSystemResourceAsStream(name);
        }

        if (input == null) {
            file = new File(System.getProperty("user.dir"));

            try {
                input = new FileInputStream(new File(file, name));
            } catch (FileNotFoundException e) {
                input = null;
            }
        }

        if ((input == null) && logErrors) {
            LOG.log(Level.WARNING, "Failed to load: {0}", name);
        }

        return input;
    }

    /**
     * Obtains an input stream for a particular resource.
     *
     * @param   dir        the directory in which to locate the file to be opened
     * @param   name       the name of the resource to read
     * @param   logErrors  true to log errors; false otherwise
     * @return  the input stream, or null if unable to open
     */
    private static InputStream openInputStream(final File dir, final String name,
        final boolean logErrors) {

        File file;
        InputStream input = null;

        file = new File(dir, name);

        try {
            input = new FileInputStream(file);
        } catch (FileNotFoundException e) {

            if (logErrors) {
                LOG.log(Level.WARNING, "Failed to load: {0}", file.getAbsolutePath());
            }
        }

        return input;
    }

    /**
     * Closes an input stream.
     *
     * @param  input  the stream to close
     * @param  name   the name of the resource being closed
     */
    private static void close(final Closeable input, final String name) {

        try {
            input.close();
        } catch (IOException e) {
            logException("close", name, e);
        }
    }

    /**
     * Logs an exception encountered by the <code>ResourceLoader</code>.
     *
     * @param  operation  the operation that was being attempted
     * @param  name       the name of the resource
     * @param  exc        the exception
     */
    private static void logException(final String operation, final String name,
        final Exception exc) {

        LOG.log(Level.WARNING, "ResourceLoader failed to {0} ''{1}'': {2}",
            new Object[] { operation, name, exc.getLocalizedMessage() });
    }
}
