
package uk.co.wingpath.util;

import java.io.*;
import uk.co.wingpath.event.*;

/**
* This class implements {@link Reporter} by writing to a file.
*/

public class FileReporter
    implements Reporter
{
    private final Variable<Reporter.Level> maxLevel;
    private final Variable<Integer> maxSize;
    private final Object lock;
    private Reporter reporter;
    private String filename;
    private File file;
    private boolean includeLevel;
    private boolean includeMillis;
    private PrintWriter out;
    private String lastDate;
    private boolean reporting;

    public FileReporter (final Variable<Reporter.Level> maxLevel,
        Variable<Integer> maxSize)
    {
        this.maxLevel = maxLevel;
        this.maxSize = maxSize;
        lock = new Object ();
        reporter = StderrReporter.getInstance ();
        filename = "";
        file = null;
        includeLevel = false;
        includeMillis = false;
        out = null;
        lastDate = "";
        reporting = false;

        maxLevel.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    synchronized (lock)
                    {
                        if (maxLevel.getValue () == Reporter.Level.NONE)
                        {
                            if (out != null)
                            {
                                out.close ();
                                out = null;
                            }
                        }
                        else if (out == null && !filename.isEmpty ())
                        {
                            try
                            {
                                FileOutputStream fos =
                                    new FileOutputStream (file, true);
                                out = new PrintWriter (fos);
                                lastDate = "";
                            }
                            catch (FileNotFoundException ex)
                            {
                                reporter.error (null,
                                    "Can't open log file " +
                                    Exceptions.getMessage (ex));
                                return;
                            }
                        }
                    }
                }
            });
    }

    /**
    * Sets the reporter to be used to report write errors on the log file.
    * By default, write errors are reported to stderr.
    * @param reporter the reporter.
    */
    public void setReporter (Reporter reporter)
    {
        if (reporter == null)
            throw new NullPointerException ("reporter must not be null");
        synchronized (lock)
        {
            this.reporter = reporter;
        }
    }

    /**
    * Sets the file to which logging should be sent.
    * <p>The file does not exist, it will be created. If it does exist,
    * logging output will be appended to it.
    * <p>If the filename is an empty string, no logging will be done.
    * @param filename name of the log file.
    * @throws FileNotFoundException if the file could not be created/opened.
    * In this case, the old log file will continue to be used.
    */
    public void setFile (String filename)
        throws FileNotFoundException
    {
        synchronized (lock)
        {
            if (filename.equals (this.filename))
                return;
            File file = new File (filename);
            if (filename.isEmpty ())
            {
                if (out != null)
                {
                    out.close ();
                    out = null;
                }
            }
            else if (maxLevel.getValue () != Reporter.Level.NONE)
            {
                FileOutputStream fos = new FileOutputStream (file, true);
                if (out != null)
                    out.close ();
                out = new PrintWriter (fos);
                lastDate = "";
            }
            this.filename = filename;
            this.file = file;
        }
    }

    /**
    * Gets the file to which logging is being sent.
    * @return name of log file.
    */
    public String getFile ()
    {
        return filename;
    }

    /**
    * Sets whether to include milliseconds in the output.
    * @param include whether to include milliseconds.
    */
    public void setIncludeMillis (boolean include)
    {
        synchronized (lock)
        {
            includeMillis = include;
        }
    }

    /**
    * Sets whether to include the log level in the output.
    * @param include whether to include the log level.
    */
    public void setIncludeLevel (boolean include)
    {
        synchronized (lock)
        {
            includeLevel = include;
        }
    }

    private void logDate (long ms)
    {
        String date = String.format ("%tF", ms);
        if (date.equals (lastDate))
            return;
        out.println ("------------- " + String.format ("%tc", ms));
        lastDate = date;
    }

    private boolean checkOk ()
    {
        if (out.checkError ())
        {
            reporter.error (null,
                "Write error on " + filename + ": Out of space?");
            out.close ();
            out = null;
            return false;
        }

        return true;
    }

    private boolean print (Reporter.Level level, String fmt, Object... args)
    {
        synchronized (lock)
        {
            if (out == null)
                return false;
            if (level.compareTo (maxLevel.getValue ()) > 0)
                return false;

            // Check for recursive call (from attempting to report a write
            // error).
            if (reporting)
                return false;
            reporting = true;
            String msg = String.format (fmt, args);
            try
            {

                int maxsize = maxSize.getValue ();
                if (maxsize > 0 && file.length () >= maxsize * 1024L * 1024L)
                {
                    // Size limit reached - backup the current file and start
                    // a new one.

                    out.close ();
                    out = null;
                    File bak = new File (file.getParentFile (),
                        file.getName () + ".bak");
                    bak.delete ();
                    if (!file.renameTo (bak))
                    {
                        reporter.error (null,
                            "Can't backup log file to " + bak);
                        return false;
                    }
                    try
                    {
                        FileOutputStream fos =
                            new FileOutputStream (file, true);
                        out = new PrintWriter (fos);
                        lastDate = "";
                    }
                    catch (FileNotFoundException e)
                    {
                        reporter.error (null,
                            "Can't open log file " + Exceptions.getMessage (e));
                        return false;
                    }
                }

                long ms = System.currentTimeMillis ();
                logDate (ms);

                String prefix = String.format ("%1$tH:%1$tM:%1$tS", ms);
                if (includeMillis)
                    prefix += String.format (".%1$tL", ms);
                prefix += ":";
                if (includeLevel)
                    prefix += " " + String.format ("%-5s", level);
                prefix += " ";

                String [] lines = msg.split ("\n");

                for (int i = 0 ; i < lines.length ; i++)
                {
                    out.println (prefix + lines [i]);
                }

                if (!checkOk ())
                    return false;
            }
            finally
            {
                reporting = false;
            }
        }

        return true;
    }

    @Override
    public boolean fatal (String fmt, Object... args)
    {
        return print (Reporter.Level.FATAL, fmt, args);
    }

    @Override
    public boolean fatal (Throwable t, String fmt, Object... args)
    {
        synchronized (lock)
        {
            if (out == null)
                return false;
            if (Reporter.Level.FATAL.compareTo (maxLevel.getValue ()) > 0)
                return false;
            if (!print (Reporter.Level.FATAL, fmt, args))
                return false;
            t.printStackTrace (out);
            return checkOk ();
        }
    }

    @Override
    public void error (String helpId, String fmt, Object... args)
    {
        print (Reporter.Level.ERROR, fmt, args);
    }

    @Override
    public void warn (String helpId, String fmt, Object... args)
    {
        print (Reporter.Level.WARN, fmt, args);
    }

    @Override
    public void info (String helpId, String fmt, Object... args)
    {
        print (Reporter.Level.INFO, fmt, args);
    }

    @Override
    public void trace (String helpId, String fmt, Object... args)
    {
        print (Reporter.Level.TRACE, fmt, args);
    }

    @Override
    public boolean debug (String fmt, Object... args)
    {
        print (Reporter.Level.DEBUG, fmt, args);
        return true;
    }

    @Override
    public void clear ()
    {
    }
}

