/*
 * Copyright (C) 2021 - 2024 ScalAgent Distributed Technologies
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA.
 *
 * Original developer: ScalAgent Distributed Technologies
 */
package org.objectweb.util.monolog.jul;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Properties;
import java.util.Vector;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;

import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.api.LoggingFactory;

public class SimpleLoggingFactory implements LoggingFactory {
  private static boolean debug = Boolean.getBoolean("org.objectweb.util.monolog.debug");
  
  // ================================================================================
  // LoggingFactory implementation
  // ================================================================================

  /**
   * Default constructor (required).
   */
  public SimpleLoggingFactory() {
    super();
  }
  
  @Override
  public Logger getLogger(String topic) {
    return new SimpleLogger(topic); 
  }

  @Override
  public org.objectweb.util.monolog.api.Level getFatalLevel() {
    return new SimpleLevel("FATAL", java.util.logging.Level.SEVERE.intValue());
  }
  

  @Override
  public org.objectweb.util.monolog.api.Level getErrorLevel() {
    return new SimpleLevel("ERROR",   950);
  }
  

  @Override
  public org.objectweb.util.monolog.api.Level getWarnLevel() {
    return new SimpleLevel("WARN",   java.util.logging.Level.WARNING.intValue());
  }
  

  @Override
  public org.objectweb.util.monolog.api.Level getInfoLevel() {
    return new SimpleLevel("INFO",   java.util.logging.Level.INFO.intValue());
  }
  

  @Override
  public org.objectweb.util.monolog.api.Level getDebugLevel() {
    return new SimpleLevel("DEBUG",   java.util.logging.Level.FINEST.intValue());
  }

  @Override
  public org.objectweb.util.monolog.api.Level parse(String name) {
    return new SimpleLevel(name, Level.parse(name).intValue());
  }

  /**
   * The configuration file is first searched in the debugDir
   * If not defined, the configuration file is then searched from the search path used to load classes.
   */
  @Override
  public void initialize(String debugDir, String debugFileName) {
    if (debug) System.err.println("LoggerFactory.initialize: " + debugDir + "/" + debugFileName);
    
    File debugFile = null;
    InputStream configStream = null;
    if (debugDir != null) {
      debugFile = new File(debugDir, debugFileName);
    } else {
      debugFile = new File(debugFileName);
    }
    
    try {
      if (debugFile.exists() && debugFile.isFile() && (debugFile.length() != 0)) {
        configStream = new FileInputStream(debugFile);
      } else {
        throw new IOException();
      }
    } catch (IOException exc) {
      // Debug configuration file seems not exist, search it from the search path used to load classes.
      if (debug)
        System.err.println("Unable to find \"" + debugFile.getPath() + "\": " + exc.getMessage());
    }

    if (configStream == null) {
      configStream = SimpleLoggingFactory.class.getClassLoader().getResourceAsStream(debugFileName);
    }

    if (configStream != null) {
      // Try to configure with given configuration file.
      try {
        configure(configStream);
        if (debug)
          System.err.println("Monolog initialized");
        return;
      } catch(Throwable exc) {
        System.err.println("Unable to configure Monolog from file: " + exc.getMessage());
        if (debug) exc.printStackTrace();
      } finally {
        try {
          configStream.close();
        } catch (IOException exc) {}
      }
    } else {
      // try to use classic configuration.
      try {
        LogManager.getLogManager().readConfiguration();
      } catch(Throwable exc) {
        System.err.println("Unable to configure Monolog: " + exc.getMessage());
        if (debug) exc.printStackTrace();
      }
    }
    
    // Cannot configure logging, try to limit the volume of traces 
    java.util.logging.Logger.getLogger("").setLevel(java.util.logging.Level.SEVERE);
  }
  
  // ================================================================================
  
  private final static String HANDLERS = "handlers";
  private final static String LEVEL = ".level";
  private final static String LOGGER = "logger.";
  private final static String FORMAT = ".format";

  private static void setFilter(Handler handler, String classname) {
    try {
      if (debug) System.err.println("Set handler filter:" + handler + " -> " + classname);
//      Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(classname);
      Class<?> clz = Class.forName(classname);
      Filter filter = (Filter) clz.getDeclaredConstructor().newInstance();
      handler.setFilter(filter);
    } catch (ClassNotFoundException exc) {
      System.err.println("Error setting filter for " + handler.getClass().getName() + ", class not found: " + classname);
    } catch (InstantiationException | IllegalAccessException
        | IllegalArgumentException | InvocationTargetException
        | NoSuchMethodException | SecurityException exc) {
      System.err.println("Error setting filter for " + handler.getClass().getName() + ": " + exc.getMessage());  
    }
  }
  
  private static String OldLogFormatter = "org.objectweb.util.monolog.api.LogFormatter";
  
  private static void setFormatter(Handler handler, String classname) {
    //      Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(classname);
    if (OldLogFormatter.equals(classname)) {
      System.err.println("Classname \"" + OldLogFormatter + "\" is deprecated, you should use \"" + LogFormatter.class.getName() + "\" instead.\n" +
          "This class will be removed in future.");
      classname = LogFormatter.class.getName();
    }
    
    try {
      if (debug) System.err.println("Set handler formatter:" + handler + " -> " + classname);
      Class<?>clz = Class.forName(classname);
      Formatter formatter = (Formatter) clz.getDeclaredConstructor().newInstance();
      if (formatter instanceof ExtendedFormatter) {
        String format = props.getProperty(handler.getClass().getName() + ".format");
        if (format != null) {
          if (debug) System.err.println("Use specific format: " + format);
          try {
          ((ExtendedFormatter) formatter).setFormat(format);
          } catch (Exception exc) {
            // illegal syntax; fall back to the default format
            System.err.println("Bad format property, use default: " + exc.getMessage());
            if (debug) exc.printStackTrace();
          }
        }
      }
      handler.setFormatter(formatter);
    } catch (ClassNotFoundException exc) {
      System.err.println("Error setting formatter for " + handler.getClass().getName() + ", class not found: " + classname);
      if (debug) exc.printStackTrace();
    } catch (InstantiationException | IllegalAccessException
        | IllegalArgumentException | InvocationTargetException
        | NoSuchMethodException | SecurityException exc) {
      System.err.println("Error setting formatter for " + handler.getClass().getName() + ": " + exc.getMessage());
      if (debug) exc.printStackTrace();
    }
  }
  
  private static final String CONSOLE_HANDLER = "java.util.logging.ConsoleHandler";
  private static final String FILE_HANDLER = "java.util.logging.FileHandler";
  private static final String MEMORY_HANDLER = "java.util.logging.MemoryHandler";
  private static final String SOCKET_HANDLER = "java.util.logging.SocketHandler";

  private static Integer getInteger(String name, String dflt) {
    String value = props.getProperty(name, dflt);
    if (value == null) return null;

    try {
      return Integer.valueOf(value);
    } catch (Exception exc) {
      return null;
    }
  }
  
  @SuppressWarnings("deprecation")
  private static Handler createHandler(String classname, Properties props) throws Exception {
    Handler handler = null;
    Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(classname);
    
    if (CONSOLE_HANDLER.equals(classname)) {
      handler = (Handler) clz.getDeclaredConstructor().newInstance();
    } else if (FILE_HANDLER.equals(classname)) {
      // <handler-name>.limit specifies an approximate maximum amount to write (in bytes) to any one file. If this is zero, then there is no limit. (Defaults to no limit).
      Integer limit = getInteger(classname + ".limit", "0");
      // <handler-name>.count specifies how many output files to cycle through (defaults to 1).
      Integer count = getInteger(classname + ".count", "1");
      // <handler-name>.pattern specifies a pattern for generating the output file name. See below for details. (Defaults to "%h/java%u.log").
      String pattern = props.getProperty(classname + ".pattern", "\"%h/java%u.log\"");
      // <handler-name>.append
      Boolean append = Boolean.valueOf(props.getProperty(classname + ".append", "false"));
      
      handler = (Handler) clz.getDeclaredConstructor(String.class, int.class, int.class, boolean.class).newInstance(pattern, limit, count, append);
    } else if (MEMORY_HANDLER.equals(classname)) {
      // <handler-name>.size defines the buffer size (defaults to 1000).
      Integer size = getInteger(classname + ".size", "1000");
      // <handler-name>.push defines the pushLevel (defaults to level.SEVERE).
      String push = props.getProperty(classname + ".push", "SEVERE");
      // <handler-name>.target specifies the name of the target Handler class. (no default).    
      String target = props.getProperty(classname + ".target");
      if (target == null)
        throw new Exception("Cannot instantiate MemoryHandler without target.");
      
      handler = (Handler) clz.getDeclaredConstructor(String.class, int.class, Level.class).newInstance(target, size, Level.parse(push));      
    } else if (SOCKET_HANDLER.equals(classname)) {
      // <handler-name>.host specifies the target host name to connect to (no default).
      String host = props.getProperty(classname + ".host");
      // <handler-name>.port specifies the target TCP port to use (no default). 
      Integer port = getInteger(classname + "port", null);
      if ((host == null) || (port == null))
        throw new Exception("Cannot instantiate SocketHandler, host and/or port should be defined.");
      
      handler = (Handler) clz.getDeclaredConstructor(String.class, int.class).newInstance(host, port);      
    } else {
      handler = (Handler) clz.newInstance();
      System.err.println("Warning: " + classname + " not handled, use default properties.");
    }

    // <handler-name>.level specifies the default level for the Handler (defaults to Level.ALL).
    String value = props.getProperty(classname + ".level");
    if (value != null) {
      if (debug) System.err.println("Set handler level:" + classname + " -> " + value);
      handler.setLevel(Level.parse(value));
    }

    // <handler-name>.filter specifies the name of a Filter class to use (defaults to no Filter).
    value = props.getProperty(classname + ".filter");
    if (value != null) setFilter(handler, value);

    // <handler-name>.formatter specifies the name of a Formatter class to use (defaults to java.util.logging.XMLFormatter)
    value = props.getProperty(classname + ".formatter");
    if (value != null) setFormatter(handler, value);

    // <handler-name>.encoding the name of the character set encoding to use (defaults to the default platform encoding).
    value = props.getProperty(classname + ".encoding");
    if (value != null) {
      if (debug) System.err.println("Set handler encoding:" + classname + " -> " + value);
      try {
        handler.setEncoding(value);
      } catch (Exception exc) {
        System.err.println("Error setting encoding for " + classname + ": " + exc.getMessage());
      }
    }
    
    return handler;
  }
  
  private static boolean isHandlerName(String name, Vector<String> handlers) {
    for (String handler : handlers) {
      if (name.equals(handler)) return true;
    }
    return false;
  }
  
  private static Properties props = new Properties();
  private static HashMap<String, java.util.logging.Logger> sloggers = new HashMap<>();

  private static void configure(InputStream is) throws Exception {
    BufferedReader reader = new BufferedReader(new InputStreamReader(is));

    props.clear();
    Vector<String> handlers = null;
    Vector<String> loggers = new Vector<>();
    Vector<String> levels = new Vector<>();
    // Minimal configuration used to initialize JUL: Formatter classes can only be initialized from the properties
    // of LogManager, but there is no easy way to add properties in the LogManager singleton. To work around this
    // problem we initialize JUL with a minimal configuration containing the properties "<Formatter class>.format".
    // In this way, formatter find these properties when they are initialized.
    Properties config = new Properties();
    
    if (debug) System.err.println("Monolog.configure");

    int linenb = 1;
    while (true) {
      try {
        String line = reader.readLine();
        if (line == null) break; // EOF
        line = line.trim();
        if (line.isEmpty() || line.charAt(0) == '#') continue;

        int idx = line.indexOf('=');
        if (idx == -1)
          throw new Exception("Cannot parse line " + linenb);
        String name = line.substring(0, idx).trim();
        String value = line.substring(idx+1).trim();
        props.setProperty(name, value);
        if (debug) System.err.println("line: " + linenb + "read: ##" + name + "##=##" + value + "##");

        if (name.equals(HANDLERS)) {
          if (handlers == null)
            handlers = new Vector<>();
          String[] classes = value.split(",");
          for (String cn : classes) {
            if (debug) System.err.println("handler: " + cn);
            handlers.add(cn.trim());
          }
          continue;
        }
        // TODO (AF): Is it useful?
        if (handlers == null)
          throw new Exception("Handlers definition should be first [line #" + linenb + "].");

        if (name.endsWith(LEVEL)) {
          if (name.startsWith(LOGGER))
            name = name.substring(7);
          name = name.substring(0, name.length() -6);

          if (name.isEmpty()) {
            // Default level
            if (debug) System.err.println("Set root logger: " + value);
            java.util.logging.Logger.getLogger("").setLevel(Level.parse(value));
          } else {
            if (debug) System.err.println("Set logger: " + name + "=" + value);
            loggers.add(name);
            levels.add(value);
          }
          continue;
        }
        
        if (name.endsWith(FORMAT)) {
          config.put(name, value);
          continue;
        }
      } catch (IOException exc) {
        throw new Exception("Error reading configuration: " + exc.getMessage());
      } finally {
        linenb += 1;
      }
    }

    // Initializes LogManager with the minimal configuration (see comment above).
    LogManager.getLogManager().reset();
    try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
        config.store(out, "");
        LogManager.getLogManager().readConfiguration(new ByteArrayInputStream(out.toByteArray()));
    } catch (IOException ioe) {
        throw new AssertionError(ioe);
    }
    
    // Configure each handler
    for (String name : handlers) {
      if (debug) System.err.println("Creates handler: " + name);
      Handler handler = createHandler(name, props);
      java.util.logging.Logger.getLogger("").addHandler(handler);
    }

    for (int i=0; i<loggers.size(); i++) {
      String name = loggers.get(i);
      if (isHandlerName(name, handlers)) continue;

      if (debug) System.err.println("Configure logger: " + name + "=" + levels.get(i));

      Level level = Level.parse(levels.get(i));
      java.util.logging.Logger logger = java.util.logging.Logger.getLogger(name);
      sloggers.put(name, logger);
      logger.setLevel(level);

      if (debug) System.err.println("Configure logger: " + logger + " || " + logger.getParent() + " / " + logger.getUseParentHandlers());
    }
    
    for (String name : loggers) {
      if (debug) System.err.println("##############################");
      java.util.logging.Logger logger = java.util.logging.Logger.getLogger(name);
      if (logger == null) {
        if (debug) System.err.println("## No logger found for " + name);
        continue;
      }
      if (debug) System.err.println("## " + logger + " || " + sloggers.get(name));
      Level level = logger.getLevel();
      if (level == null) {
        if (debug) System.err.println("## No level found for " + name);
        continue;
      }
      if (debug) System.err.println("## " + name + " -> " + level);
    }
  }
}
