package de.lmu.ifi.dbs.elki.gui.util;

/*
 This file is part of ELKI:
 Environment for Developing KDD-Applications Supported by Index-Structures

 Copyright (C) 2015
 Ludwig-Maximilians-Universität München
 Lehr- und Forschungseinheit für Datenbanksysteme
 ELKI Development Team

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program 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 Affero General Public License for more details.

 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import java.awt.Color;
import java.util.logging.ErrorManager;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;

import de.lmu.ifi.dbs.elki.logging.ErrorFormatter;
import de.lmu.ifi.dbs.elki.logging.LoggingConfiguration;
import de.lmu.ifi.dbs.elki.logging.MessageFormatter;
import de.lmu.ifi.dbs.elki.logging.OutputStreamLogger;
import de.lmu.ifi.dbs.elki.logging.progress.ProgressLogRecord;

/**
 * A Swing object to receive ELKI logging output. Call
 * {@link #becomeDefaultLogger()} to register as default logger in ELKI.
 * 
 * @author Erich Schubert
 * @since 0.3
 * 
 * @apiviz.uses LoggingConfiguration
 * @apiviz.uses de.lmu.ifi.dbs.elki.logging.ELKILogRecord
 * @apiviz.uses ProgressLogRecord
 */
public class LogPane extends JTextPane {
  /**
   * Serialization version number
   */
  private static final long serialVersionUID = 1L;

  /**
   * Base (default) style
   */
  protected Style baseStyle;

  /**
   * Regular message style
   */
  protected Style msgStyle;

  /**
   * Debug message style
   */
  protected Style dbgStyle;

  /**
   * Error message style
   */
  protected Style errStyle;

  /**
   * Formatter for regular messages (informational records)
   */
  private Formatter msgformat = new MessageFormatter();

  /**
   * Formatter for debugging messages
   */
  private Formatter debugformat = new ErrorFormatter();

  /**
   * Formatter for error messages
   */
  private Formatter errformat = new ErrorFormatter();

  /**
   * Last newline position
   */
  private int lastNewlinePos = 0;

  /**
   * Constructor
   */
  public LogPane() {
    super();
    // setup styles
    baseStyle = getStyledDocument().addStyle(null, null);
    msgStyle = getStyledDocument().addStyle("msg", baseStyle);
    errStyle = getStyledDocument().addStyle("err", baseStyle);
    errStyle.addAttribute(StyleConstants.Foreground, Color.RED);
    dbgStyle = getStyledDocument().addStyle("err", baseStyle);
    dbgStyle.addAttribute(StyleConstants.Foreground, Color.BLUE);
  }

  /**
   * Print a message as if it were logged, without going through the full
   * logger.
   * 
   * @param message Message text
   * @param level Message level
   */
  public void publish(String message, Level level) {
    try {
      publish(new LogRecord(level, message));
    }
    catch(BadLocationException e) {
      throw new RuntimeException("Error writing a log-like message.", e);
    }
  }

  /**
   * Publish a log record to the logging pane.
   * 
   * @param record Log record
   * @throws Exception
   */
  protected synchronized void publish(LogRecord record) throws BadLocationException {
    // choose an appropriate formatter
    final Formatter fmt;
    final Style style;
    // always format progress messages using the progress formatter.
    if(record.getLevel().intValue() >= Level.WARNING.intValue()) {
      // format errors using the error formatter
      fmt = errformat;
      style = errStyle;
    }
    else if(record.getLevel().intValue() <= Level.FINE.intValue()) {
      // format debug statements using the debug formatter.
      fmt = debugformat;
      style = dbgStyle;
    }
    else {
      // default to the message formatter.
      fmt = msgformat;
      style = msgStyle;
    }
    // format
    final String m;
    m = fmt.format(record);
    if(record instanceof ProgressLogRecord) {
      if(lastNewlinePos < getStyledDocument().getLength()) {
        getStyledDocument().remove(lastNewlinePos, getStyledDocument().getLength() - lastNewlinePos);
      }
    }
    else {
      // insert a newline, if we didn't see one yet.
      if(lastNewlinePos < getStyledDocument().getLength()) {
        getStyledDocument().insertString(getStyledDocument().getLength(), "\n", style);
        lastNewlinePos = getStyledDocument().getLength();
      }
    }
    int tail = tailingNonNewline(m, 0, m.length());
    int headlen = m.length() - tail;
    if(headlen > 0) {
      String pre = m.substring(0, headlen);
      getStyledDocument().insertString(getStyledDocument().getLength(), pre, style);
    }
    lastNewlinePos = getStyledDocument().getLength();
    if(tail > 0) {
      String post = m.substring(m.length() - tail);
      getStyledDocument().insertString(lastNewlinePos, post, style);
    }
  }

  /**
   * Count the tailing non-newline characters.
   * 
   * @param str String
   * @param off Offset
   * @param len Range
   * @return number of tailing non-newline character
   */
  private int tailingNonNewline(String str, int off, int len) {
    for(int cnt = 0; cnt < len; cnt++) {
      final int pos = off + (len - 1) - cnt;
      if(str.charAt(pos) == OutputStreamLogger.UNIX_NEWLINE) {
        return cnt;
      }
      // TODO: need to compare to NEWLINE, too?
    }
    return len;
  }

  /**
   * Clear the current contents.
   */
  public void clear() {
    setText("");
    lastNewlinePos = 0;
  }

  /**
   * Become the default logger.
   */
  public void becomeDefaultLogger() {
    LoggingConfiguration.replaceDefaultHandler(new LogPaneHandler());
  }

  /**
   * Internal {@link java.util.logging.Handler}
   * 
   * @author Erich Schubert
   * 
   * @apiviz.exclude
   */
  private class LogPaneHandler extends Handler {
    /**
     * Constructor.
     */
    protected LogPaneHandler() {
      super();
    }

    @Override
    public void close() throws SecurityException {
      // do nothing
    }

    @Override
    public void flush() {
      // do nothing
    }

    @Override
    public void publish(LogRecord record) {
      try {
        LogPane.this.publish(record);
      }
      catch(Exception e) {
        reportError("Error printing output log message.", e, ErrorManager.GENERIC_FAILURE);
      }
    }
  }
}
