/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2013, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.core.rolling.helper;

import java.io.File;
import java.util.Date;

import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.pattern.Converter;
import ch.qos.logback.core.pattern.LiteralConverter;
import ch.qos.logback.core.spi.ContextAwareBase;

abstract public class DefaultArchiveRemover extends ContextAwareBase implements
        ArchiveRemover {

  static protected final long UNINITIALIZED = -1;
  // aim for 64 days, except in case of hourly rollover
  static protected final long INACTIVITY_TOLERANCE_IN_MILLIS = 64L * (long) CoreConstants.MILLIS_IN_ONE_DAY;
  static final int MAX_VALUE_FOR_INACTIVITY_PERIODS = 14 * 24; // 14 days in case of hourly rollover

  final FileNamePattern fileNamePattern;
  final RollingCalendar rc;
  int periodOffsetForDeletionTarget;
  final boolean parentClean;
  long lastHeartBeat = UNINITIALIZED;

  public DefaultArchiveRemover(FileNamePattern fileNamePattern,
                               RollingCalendar rc) {
    this.fileNamePattern = fileNamePattern;
    this.rc = rc;
    this.parentClean = computeParentCleaningFlag(fileNamePattern);
  }


  int computeElapsedPeriodsSinceLastClean(long nowInMillis) {
    long periodsElapsed = 0;
    if (lastHeartBeat == UNINITIALIZED) {
      addInfo("first clean up after appender initialization");
      periodsElapsed = rc.periodsElapsed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS);
      if (periodsElapsed > MAX_VALUE_FOR_INACTIVITY_PERIODS)
        periodsElapsed = MAX_VALUE_FOR_INACTIVITY_PERIODS;
    } else {
      periodsElapsed = rc.periodsElapsed(lastHeartBeat, nowInMillis);
      if (periodsElapsed < 1) {
        addWarn("Unexpected periodsElapsed value " + periodsElapsed);
        periodsElapsed = 1;
      }
    }
    return (int) periodsElapsed;
  }

  public void clean(Date now) {
    long nowInMillis = now.getTime();
    int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis);
    lastHeartBeat = nowInMillis;
    if (periodsElapsed > 1) {
      addInfo("periodsElapsed = " + periodsElapsed);
    }
    for (int i = 0; i < periodsElapsed; i++) {
      cleanByPeriodOffset(now, periodOffsetForDeletionTarget - i);
    }
  }

  abstract void cleanByPeriodOffset(Date now, int periodOffset);

  boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) {
    DateTokenConverter dtc = fileNamePattern.getPrimaryDateTokenConverter();
    // if the date pattern has a /, then we need parent cleaning
    if (dtc.getDatePattern().indexOf('/') != -1) {
      return true;
    }
    // if the literal string subsequent to the dtc contains a /, we also
    // need parent cleaning

    Converter<Object> p = fileNamePattern.headTokenConverter;

    // find the date converter
    while (p != null) {
      if (p instanceof DateTokenConverter) {
        break;
      }
      p = p.getNext();
    }

    while (p != null) {
      if (p instanceof LiteralConverter) {
        String s = p.convert(null);
        if (s.indexOf('/') != -1) {
          return true;
        }
      }
      p = p.getNext();
    }

    // no /, so we don't need parent cleaning
    return false;
  }

  void removeFolderIfEmpty(File dir) {
    removeFolderIfEmpty(dir, 0);
  }

  /**
   * Will remove the directory passed as parameter if empty. After that, if the
   * parent is also becomes empty, remove the parent dir as well but at most 3
   * times.
   *
   * @param dir
   * @param depth
   */
  private void removeFolderIfEmpty(File dir, int depth) {
    // we should never go more than 3 levels higher
    if (depth >= 3) {
      return;
    }
    if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) {
      addInfo("deleting folder [" + dir + "]");
      dir.delete();
      removeFolderIfEmpty(dir.getParentFile(), depth + 1);
    }
  }

  public void setMaxHistory(int maxHistory) {
    this.periodOffsetForDeletionTarget = -maxHistory - 1;
  }

}
