/*
 * This file is part of ELKI:
 * Environment for Developing KDD-Applications Supported by Index-Structures
 *
 * Copyright (C) 2022
 * 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/>.
 */
package elki.logging;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;

/**
 * Class to write to Output Streams, IGNORING {@link #close()}, with a special
 * newline handling and always flushing.
 * 
 * This is meant to wrap logging output to the console.
 * 
 * @author Erich Schubert
 * @since 0.2
 */
public class OutputStreamLogger extends OutputStreamWriter {
  /**
   * Flag to signal if we have had a newline recently.
   */
  private int charsSinceNewline = 0;

  /**
   * Carriage return character.
   */
  protected static final char CARRIAGE_RETURN = '\r';

  /**
   * Unix newline
   */
  public static final char UNIX_NEWLINE = '\n';

  /**
   * Newline string.
   */
  public static final String NEWLINE = System.getProperty("line.separator");

  /**
   * Newline as char array.
   */
  public static final char[] NEWLINEC = NEWLINE.toCharArray();

  /**
   * Whitespace.
   */
  public static final String WHITESPACE = "                                                                                ";

  /**
   * Constructor.
   * 
   * @param out Output stream
   */
  public OutputStreamLogger(OutputStream out) {
    super(out);
  }

  /**
   * Constructor.
   * 
   * @param out Output stream
   * @param charsetName Character set name
   * @throws UnsupportedEncodingException thrown on unknown character sets
   */
  public OutputStreamLogger(OutputStream out, String charsetName) throws UnsupportedEncodingException {
    super(out, charsetName);
  }

  /**
   * Constructor.
   * 
   * @param out Output Stream
   * @param cs Character set to use
   */
  public OutputStreamLogger(OutputStream out, Charset cs) {
    super(out, cs);
  }

  /**
   * Constructor.
   * 
   * @param out Output Stream
   * @param enc Charset encoder
   */
  public OutputStreamLogger(OutputStream out, CharsetEncoder enc) {
    super(out, enc);
  }

  /**
   * Close command - will be IGNORED.
   */
  @Override
  public void close() {
    // IGNORE any close command.
  }

  /**
   * Count the tailing non-newline characters.
   * 
   * @param cbuf Character buffer
   * @param off Offset
   * @param len Range
   * @return number of tailing non-newline character
   */
  private int tailingNonNewline(char[] cbuf, int off, int len) {
    for(int cnt = 0; cnt < len; cnt++) {
      final int pos = off + (len - 1) - cnt;
      if(cbuf[pos] == UNIX_NEWLINE) {
        return cnt;
      }
      if(cbuf[pos] == CARRIAGE_RETURN) {
        return cnt;
      }
      // TODO: need to compare to NEWLINEC, too?
    }
    return len;
  }

  /**
   * 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) == UNIX_NEWLINE) {
        return cnt;
      }
      if(str.charAt(pos) == CARRIAGE_RETURN) {
        return cnt;
      }
      // TODO: need to compare to NEWLINE, too?
    }
    return len;
  }

  /**
   * Count the number of non-newline characters before first newline in the string.
   * 
   * @param cbuf character buffer
   * @param off offset
   * @param len length
   * @return number of non-newline characters
   */
  private int countNonNewline(char[] cbuf, int off, int len) {
    for(int cnt = 0; cnt < len; cnt++) {
      final int pos = off + cnt;
      if(cbuf[pos] == UNIX_NEWLINE) {
        return cnt;
      }
      if(cbuf[pos] == CARRIAGE_RETURN) {
        return cnt;
      }
    }
    return len;
  }

  /**
   * Count the number of non-newline characters before first newline in the string.
   * 
   * @param str String
   * @param off offset
   * @param len length
   * @return number of non-newline characters
   */
  private int countNonNewline(String str, int off, int len) {
    for(int cnt = 0; cnt < len; cnt++) {
      final int pos = off + cnt;
      if(str.charAt(pos) == UNIX_NEWLINE) {
        return cnt;
      }
      if(str.charAt(pos) == CARRIAGE_RETURN) {
        return cnt;
      }
    }
    return len;
  }

  /**
   * Writer that keeps track of when it hasn't seen a newline yet, will
   * auto-insert newlines except when lines start with a carriage return.
   */
  @Override
  public void write(char[] cbuf, int off, int len) throws IOException {
    if(len <= 0) {
      return;
    }
    // if we havn't last seen a newline, and don't get a CR, insert a newline.
    if(charsSinceNewline > 0) {
      if(cbuf[off] != CARRIAGE_RETURN) {
        super.write(NEWLINEC, 0, NEWLINEC.length);
        charsSinceNewline = 0;
      }
      else {
        // length of this line:
        int nonnl = countNonNewline(cbuf, off + 1, len - 1);
        // clear the existing chars.
        if(nonnl < charsSinceNewline) {
          super.write(CARRIAGE_RETURN);
          while(charsSinceNewline > 0) {
            final int n = Math.min(charsSinceNewline, WHITESPACE.length());
            super.write(WHITESPACE, 0, n);
            charsSinceNewline -= n;
          }
        }
        else {
          charsSinceNewline = 0;
        }
      }
    }
    charsSinceNewline = tailingNonNewline(cbuf, off, len);
    super.write(cbuf, off, len);
    flush();
  }

  /**
   * Writer that keeps track of when it hasn't seen a newline yet, will
   * auto-insert newlines except when lines start with a carriage return.
   */
  @Override
  public void write(String str, int off, int len) throws IOException {
    if(len <= 0) {
      return;
    }
    // if we havn't last seen a newline, and don't get a CR, insert a newline.
    if(charsSinceNewline > 0) {
      if(str.charAt(off) != CARRIAGE_RETURN) {
        super.write(NEWLINEC, 0, NEWLINEC.length);
        charsSinceNewline = 0;
      }
      else {
        // length of this line:
        int nonnl = countNonNewline(str, off + 1, len - 1);
        // clear the existing chars.
        if(nonnl < charsSinceNewline) {
          super.write(CARRIAGE_RETURN);
          while(charsSinceNewline > 0) {
            final int n = Math.min(charsSinceNewline, WHITESPACE.length());
            super.write(WHITESPACE, 0, n);
            charsSinceNewline -= n;
          }
        }
        else {
          charsSinceNewline = 0;
        }
      }
    }
    charsSinceNewline = tailingNonNewline(str, off, len);
    super.write(str, off, len);
    flush();
  }
}