/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.internal.logging;

import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.i18n.LogWriterI18n;
import org.apache.geode.internal.OSProcess;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.io.MainWithChildrenRollingFileHandler;
import org.apache.geode.internal.io.RollingFileHandler;
import org.apache.geode.internal.logging.log4j.AlertAppender;
import org.apache.geode.internal.util.LogFileUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Date;

/**
 * Implementation of {@link LogWriterI18n} for distributed system members. It's just like
 * {@link LocalLogWriter} except it has support for rolling and alerts.
 *
 * @since Geode 1.0
 */
public class ManagerLogWriter extends LocalLogWriter {

  public static final String TEST_FILE_SIZE_LIMIT_IN_KB_PROPERTY =
      DistributionConfig.GEMFIRE_PREFIX + "logging.test.fileSizeLimitInKB";

  private final boolean fileSizeLimitInKB;

  private final RollingFileHandler rollingFileHandler;

  private LogConfig cfg = null;

  private LocalLogWriter mainLogger = this;

  private File logDir = null;
  private int mainLogId = -1;
  private int childId = 0;
  private boolean useChildLogging = false;

  /**
   * Set to true when roll is in progress
   */
  private boolean rolling = false;
  private boolean mainLog = true;

  private File activeLogFile = null;

  private boolean started = false;

  /**
   * Creates a writer that logs to <code>System.out</code>.
   * 
   * @param level only messages greater than or equal to this value will be logged.
   * @throws IllegalArgumentException if level is not in legal range
   */
  public ManagerLogWriter(int level, PrintStream out) {
    this(level, out, null);
  }

  /**
   * Creates a writer that logs to <code>System.out</code>.
   *
   * @param level only messages greater than or equal to this value will be logged.
   * @param connectionName Name of the connection associated with this logger
   *
   * @throws IllegalArgumentException if level is not in legal range
   *
   * @since GemFire 3.5
   */
  public ManagerLogWriter(int level, PrintStream out, String connectionName) {
    super(level, out, connectionName);
    this.fileSizeLimitInKB = Boolean.getBoolean(TEST_FILE_SIZE_LIMIT_IN_KB_PROPERTY);
    this.rollingFileHandler = new MainWithChildrenRollingFileHandler();
  }

  /**
   * Sets the config that should be used by this manager to decide how to manage its logging.
   */
  public void setConfig(LogConfig cfg) {
    this.cfg = cfg;
    configChanged();
  }

  /**
   * Call when the config changes at runtime.
   */
  public void configChanged() {
    setLevel(cfg.getLogLevel());
    useChildLogging = cfg.getLogFile() != null && !cfg.getLogFile().equals(new File(""))
        && cfg.getLogFileSizeLimit() != 0;

    if (useChildLogging()) {
      logDir = rollingFileHandler.getParentFile(this.cfg.getLogFile());
      // let archive id always follow main log id, not the vice versa
      // e.g. in getArchiveName(), it's using mainArchiveId = calcNextMainId(archiveDir);
      // This is the only place we assign mainLogId.
      // mainLogId is only referenced when useChildLogging==true
      mainLogId = rollingFileHandler.calcNextMainId(logDir, true);
    }
    if (started) {
      if (useChildLogging()) {
        if (mainLog) {
          rollLog();
        }
      } else {
        switchLogs(this.cfg.getLogFile(), true);
      }
    }
  }

  public File getChildLogFile() {
    return this.activeLogFile;
  }

  public File getLogDir() {
    return this.logDir;
  }

  public int getMainLogId() {
    return this.mainLogId;
  }

  private File getNextChildLogFile() {
    String path = this.cfg.getLogFile().getPath();
    int extIdx = path.lastIndexOf('.');
    String ext = "";
    if (extIdx != -1) {
      ext = path.substring(extIdx);
      path = path.substring(0, extIdx);
    }
    path = path + rollingFileHandler.formatId(mainLogId) + rollingFileHandler.formatId(this.childId)
        + ext;
    this.childId++;
    File result = new File(path);
    if (result.exists()) {
      // try again until a unique name is found
      return getNextChildLogFile();
    } else {
      return result;
    }
  }

  public boolean useChildLogging() {
    return this.useChildLogging;
  }

  private long getLogFileSizeLimit() {
    if (rolling || mainLog) {
      return Long.MAX_VALUE;
    } else {
      long result = cfg.getLogFileSizeLimit();
      if (result == 0) {
        return Long.MAX_VALUE;
      }
      if (this.fileSizeLimitInKB) {
        // use KB instead of MB to speed up log rolling for test purpose
        return result * (1024);
      } else {
        return result * (1024 * 1024);
      }
    }
  }

  private long getLogDiskSpaceLimit() {
    long result = cfg.getLogDiskSpaceLimit();
    return result * (1024 * 1024);
  }

  private String getMetaLogFileName(String baseLogFileName, int mainLogId) {
    String metaLogFile = null;
    int extIdx = baseLogFileName.lastIndexOf('.');
    String ext = "";
    if (extIdx != -1) {
      ext = baseLogFileName.substring(extIdx);
      metaLogFile = baseLogFileName.substring(0, extIdx);
    }
    String fileName = new File(metaLogFile).getName();
    String parent = new File(metaLogFile).getParent();

    metaLogFile = "meta-" + fileName + rollingFileHandler.formatId(mainLogId) + ext;
    if (parent != null) {
      metaLogFile = parent + File.separator + metaLogFile;
    }
    return metaLogFile;
  }

  private synchronized void switchLogs(File newLog, boolean newIsMain) {
    rolling = true;
    try {
      try {
        if (!newIsMain) {
          if (mainLog && mainLogger == this && this.cfg.getLogFile() != null) {
            // this is the first child. Save the current output stream
            // so we can log special messages to the main log instead
            // of the current child log.
            String metaLogFile =
                getMetaLogFileName(this.cfg.getLogFile().getPath(), this.mainLogId);
            mainLogger = new LocalLogWriter(INFO_LEVEL,
                new PrintStream(new FileOutputStream(metaLogFile, true), true));
            if (activeLogFile == null) {
              mainLogger.info(LocalizedStrings.ManagerLogWriter_SWITCHING_TO_LOG__0,
                  this.cfg.getLogFile());
            }
          } else {
            mainLogger.info(LocalizedStrings.ManagerLogWriter_ROLLING_CURRENT_LOG_TO_0, newLog);
          }
        }
        boolean renameOK = true;
        String oldName = this.cfg.getLogFile().getAbsolutePath();
        File tmpFile = null;
        if (this.activeLogFile != null) {
          // @todo gregp: is a bug that we get here and try to rename the activeLogFile
          // to a newLog of the same name? This works ok on Unix but on windows fails.
          if (!this.activeLogFile.getAbsolutePath().equals(newLog.getAbsolutePath())) {
            boolean isWindows = false;
            String os = System.getProperty("os.name");
            if (os != null) {
              if (os.indexOf("Windows") != -1) {
                isWindows = true;
              }
            }
            if (isWindows) {
              // For windows to work we need to redirect everything
              // to a temporary file so we can get oldFile closed down
              // so we can rename it. We don't actually write to this tmp file
              File tmpLogDir = rollingFileHandler.getParentFile(this.cfg.getLogFile());
              tmpFile = File.createTempFile("mlw", null, tmpLogDir);
              // close the old guy down before we do the rename
              PrintStream tmpps = OSProcess.redirectOutput(tmpFile,
                  AlertAppender.getInstance().isAlertingDisabled()/* See #49492 */);
              PrintWriter oldPW = this.setTarget(new PrintWriter(tmpps, true));
              if (oldPW != null) {
                oldPW.close();
              }
            }
            File oldFile = this.activeLogFile;
            renameOK = LogFileUtils.renameAggressively(oldFile, newLog.getAbsoluteFile());
            if (!renameOK) {
              mainLogger
                  .warning("Could not delete original file '" + oldFile + "' after copying to '"
                      + newLog.getAbsoluteFile() + "'. Continuing without rolling.");
            } else {
              renameOK = true;
            }
          }
        }
        this.activeLogFile = new File(oldName);
        // Don't redirect sysouts/syserrs to client log file. See #49492.
        // IMPORTANT: This assumes that only a loner would have sendAlert set to false.
        PrintStream ps = OSProcess.redirectOutput(activeLogFile,
            AlertAppender.getInstance().isAlertingDisabled());
        PrintWriter oldPW = this.setTarget(new PrintWriter(ps, true), this.activeLogFile.length());
        if (oldPW != null) {
          oldPW.close();
        }
        if (tmpFile != null) {
          tmpFile.delete();
        }
        mainLog = newIsMain;
        if (mainLogger == null) {
          mainLogger = this;
        }
        if (!renameOK) {
          mainLogger.warning("Could not rename \"" + this.activeLogFile + "\" to \"" + newLog
              + "\". Continuing without rolling.");
        }
      } catch (IOException ex) {
        mainLogger.warning("Could not open log \"" + newLog + "\" because " + ex);
      }
      checkDiskSpace(this.activeLogFile);
    } finally {
      rolling = false;
    }
  }

  /** notification from manager that the output file is being closed */
  public void closingLogFile() {
    OutputStream out = new OutputStream() {
      @Override
      public void write(int b) throws IOException {
        // --> /dev/null
      }
    };
    close();
    if (mainLogger != null) {
      mainLogger.close();
    }
    PrintWriter pw = this.setTarget(new PrintWriter(out, true));
    if (pw != null) {
      pw.close();
    }
  }

  public static File getLogNameForOldMainLog(File log, boolean useOldFile) {
    /*
     * this is just searching for the existing logfile name we need to search for meta log file name
     * 
     */
    RollingFileHandler rollingFileHandler = new MainWithChildrenRollingFileHandler();
    File dir = rollingFileHandler.getParentFile(log.getAbsoluteFile());
    int previousMainId = rollingFileHandler.calcNextMainId(dir, true);
    if (useOldFile) {
      if (previousMainId > 0) {
        previousMainId--;
      }
    }
    if (previousMainId == 0) {
      previousMainId = 1;
    }

    File result = null;
    int childId = rollingFileHandler.calcNextChildId(log, previousMainId > 0 ? previousMainId : 0);
    StringBuffer buf = new StringBuffer(log.getPath());
    int insertIdx = buf.lastIndexOf(".");
    if (insertIdx == -1) {
      buf.append(rollingFileHandler.formatId(previousMainId))
          .append(rollingFileHandler.formatId(childId));
    } else {
      buf.insert(insertIdx, rollingFileHandler.formatId(childId));
      buf.insert(insertIdx, rollingFileHandler.formatId(previousMainId));
    }
    result = new File(buf.toString());
    return result;
  }

  private void checkDiskSpace(File newLog) {
    rollingFileHandler.checkDiskSpace("log", newLog, getLogDiskSpaceLimit(), logDir, mainLogger);
  }

  public void rollLog() {
    rollLog(false);
  }

  private void rollLogIfFull() {
    rollLog(true);
  }

  private void rollLog(boolean ifFull) {
    if (!useChildLogging()) {
      return;
    }
    synchronized (this) {
      // need to do the activeLogFull call while synchronized
      if (ifFull && !activeLogFull()) {
        return;
      }
      switchLogs(getNextChildLogFile(), false);
    }
  }

  /**
   * Called when manager is done starting up. This is when a child log will be started if rolling is
   * configured.
   */
  public void startupComplete() {
    started = true;
    rollLog();
  }

  /**
   * Called when manager starts shutting down. This is when any current child log needs to be closed
   * and the rest of the logging reverted to the main.
   */
  public void shuttingDown() {
    if (useChildLogging()) {
      switchLogs(this.cfg.getLogFile(), true);
    }
  }

  private boolean activeLogFull() {
    long limit = getLogFileSizeLimit();
    if (limit == Long.MAX_VALUE) {
      return false;
    } else {
      return getBytesLogged() >= limit;
    }
  }

  @Override
  public String put(int msgLevel, Date msgDate, String connectionName, String threadName, long tid,
      String msg, String exceptionText) {

    String result = null; // This seems to workaround a javac bug
    result = super.put(msgLevel, msgDate, connectionName, threadName, tid, msg, exceptionText);
    return result;
  }

  @Override
  public void writeFormattedMessage(String s) {
    rollLogIfFull();
    super.writeFormattedMessage(s);
  }
}
