/*
 * 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.carbondata.common.logging.impl;

import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.TreeMap;

import org.apache.log4j.Level;
import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.helpers.CountingQuietWriter;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;

/**
 * Copied from log4j to remove the hard coding for the file name from it Copied
 * form log4j and modified for renaming files
 */
public class ExtendedRollingFileAppender extends RollingFileAppender {

  private static final String DATE_FORMAT_FOR_TRANSFER = "yyyy-MM-dd'_'HH-mm-ss";
  protected int currentLevel = Level.FATAL_INT;
  /**
   * Now in log file rolling(after file size
   * exceeded the threshold) and deletion (after file count exceeded the file
   * count threshold) it will print log message
   */

  private long nextRollover = 0;
  private volatile boolean cleanupInProgress = false;

  /**
   * Total number of files at any point of time should be Backup number of
   * files + current file
   */
  private static void cleanLogs(final String startName, final String folderPath,
      int maxBackupIndex) {
    final String fileStartName = startName.toLowerCase(Locale.US);
    // Delete the oldest file, to keep Windows happy.
    File file = new File(folderPath);

    if (file.exists()) {
      File[] files = file.listFiles(f ->
          !f.isDirectory() && f.getName().toLowerCase(Locale.US).startsWith(fileStartName));

      if (null == files) {
        return;
      }

      int backupFiles = files.length - 1;

      if (backupFiles <= maxBackupIndex) {
        return;
      }

      // Sort the file based on its name.
      TreeMap<String, File> sortedMap = new TreeMap<>();
      for (File file1 : files) {
        sortedMap.put(file1.getName(), file1);
      }

      // Remove the first log file from map. it will be <startName>.log
      // itself which will be backed up in rollover
      sortedMap.remove(sortedMap.firstKey());

      Iterator<Entry<String, File>> it = sortedMap.entrySet().iterator();
      Entry<String, File> temp;

      // After clean up the files should be maxBackupIndex -1 number of
      // files. Because one more backup file
      // will be created after this method call is over
      while (it.hasNext() && backupFiles > maxBackupIndex) {
        temp = it.next();
        File deleteFile = temp.getValue();
        // Delete the file
        // After deletion of log file it
        // will print the log message in ReportService.log
        if (deleteFile.delete()) {
          backupFiles--;
        } else {
          LogLog.error("Couldn't delete file :: " + deleteFile.getPath());
        }
      }
    }
  }

  /**
   * Copied from log4j to remove hardcoding of file name
   */
  public void rollOver() {
    File target;
    File file = new File(fileName);

    String fileStartName = file.getName();
    int dotIndex = fileStartName.indexOf('.');

    if (dotIndex != -1) {
      fileStartName = fileStartName.substring(0, dotIndex);
    }
    final String startName = fileStartName;
    final String folderPath = file.getParent();

    if (qw != null) {
      long size = ((CountingQuietWriter) qw).getCount();
      LogLog.debug("rolling over count=" + size);
      // if operation fails, do not roll again until
      // maxFileSize more bytes are written
      nextRollover = size + maxFileSize;
    }

    LogLog.debug("maxBackupIndex=" + maxBackupIndex);

    boolean renameSucceeded = true;

    // If maxBackups <= 0, then there is no file renaming to be done.
    if (maxBackupIndex > 0) {
      DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_FOR_TRANSFER);

      StringBuilder buffer = new StringBuilder();
      String extension = "";
      if (fileName.contains(".")) {
        extension = fileName.substring(fileName.lastIndexOf("."));
        buffer.append(fileName, 0, fileName.lastIndexOf("."));
      } else {
        buffer.append(fileName);
      }
      buffer.append("_").append(dateFormat.format(new Date())).append(extension);
      // Rename fileName to fileName.1
      target = new File(buffer.toString());

      this.closeFile(); // keep windows happy.

      LogLog.debug("Renaming file " + file + " to " + target);
      renameSucceeded = file.renameTo(target);

      //
      // if file rename failed, reopen file with append = true
      //
      if (!renameSucceeded) {
        try {
          this.setFile(fileName, true, bufferedIO, bufferSize);
        } catch (InterruptedIOException e) {
          Thread.currentThread().interrupt();
        } catch (IOException e) {
          LogLog.error("setFile(" + fileName + ", true) call failed.", e);
        }
      }
    }

    //
    // if all renames were successful, then
    //
    if (renameSucceeded) {
      try {
        // This will also close the file. This is OK since multiple
        // close operations are safe.
        this.setFile(fileName, false, bufferedIO, bufferSize);
        nextRollover = 0;
      } catch (InterruptedIOException e) {
        Thread.currentThread().interrupt();
      } catch (IOException e) {
        LogLog.error("setFile(" + fileName + ", false) call failed.", e);
      }
    }

    // Do clean up finally
    if (!cleanupInProgress) {
      cleanUpLogs(startName, folderPath);
    }
  }

  private void cleanUpLogs(final String startName, final String folderPath) {
    if (maxBackupIndex > 0) {
      // Clean the logs files
      Runnable r = new Runnable() {

        public void run() {
          synchronized (ExtendedRollingFileAppender.class) {
            cleanupInProgress = true;
            try {
              cleanLogs(startName, folderPath, maxBackupIndex);
            } catch (Throwable e) {
              // ignore any error
              LogLog.error("Cleaning logs failed", e);
            } finally {
              cleanupInProgress = false;
            }
          }
        }
      };

      Thread t = new Thread(r);
      t.start();
    }
  }

  protected void subAppend(LoggingEvent event) {
    if (event.getLevel().toInt() <= currentLevel) {
      super.subAppend(event);
      if (fileName != null && qw != null) {
        long size = ((CountingQuietWriter) qw).getCount();
        if (size >= maxFileSize && size >= nextRollover) {
          rollOver();
        }
      }
    }
  }

}
