/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.controller.audit;

import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.as.controller.services.path.PathManagerService;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
 *  All methods on this class should be called with {@link ManagedAuditLoggerImpl}'s lock taken.
 *
 * @author <a href="mailto:istudens@redhat.com">Ivo Studensky</a>
 */
public class PeriodicRotatingFileAuditLogHandler extends AbstractFileAuditLogHandler {
    private SimpleDateFormat format;
    private Period period = Period.NEVER;
    private volatile String nextSuffix;
    private volatile long nextRollover = Long.MAX_VALUE;

    private TimeZone timeZone = TimeZone.getDefault();
    private String suffix;

    public PeriodicRotatingFileAuditLogHandler(final String name, final String formatterName, final int maxFailureCount, final PathManagerService pathManager, final String path, final String relativeTo, final String suffix, final TimeZone timeZone) {
        super(name, formatterName, maxFailureCount, pathManager, path, relativeTo);
        this.suffix = suffix;       // remember the value just for the sake of the method isDifferent()
        if (timeZone != null)
            this.timeZone = timeZone;   // needed for setSuffix in the next step
        setSuffix(suffix);
    }

    @Override
    protected void initializeAtStartup(final File file) {
        final long lastModified = file.lastModified();
        calcNextRollover((lastModified > 0) ? lastModified : System.currentTimeMillis());
    }

    @Override
    protected void rotateLogFile(final File file) {
        final long now = System.currentTimeMillis();
        if (now >= nextRollover) {
            rollOver(file);
            calcNextRollover(now);
        }
    }

    /**
     * Set the suffix string.  The string is in a format which can be understood by {@link java.text.SimpleDateFormat}.
     * The period of the rotation is automatically calculated based on the suffix.
     *
     * @param suffix the suffix
     * @throws IllegalArgumentException if the suffix is not valid
     */
    private void setSuffix(String suffix) throws IllegalArgumentException {
        // method code stolen from the logging subsystem
        final SimpleDateFormat format = new SimpleDateFormat(suffix);
        format.setTimeZone(timeZone);
        final int len = suffix.length();
        Period period = Period.NEVER;
        for (int i = 0; i < len; i ++) {
            switch (suffix.charAt(i)) {
                case 'y': period = min(period, Period.YEAR); break;
                case 'M': period = min(period, Period.MONTH); break;
                case 'w':
                case 'W': period = min(period, Period.WEEK); break;
                case 'D':
                case 'd':
                case 'F':
                case 'E': period = min(period, Period.DAY); break;
                case 'a': period = min(period, Period.HALF_DAY); break;
                case 'H':
                case 'k':
                case 'K':
                case 'h': period = min(period, Period.HOUR); break;
                case 'm': period = min(period, Period.MINUTE); break;
                case '\'': while (suffix.charAt(++i) != '\''){} break;
                case 's':
                case 'S': throw new IllegalArgumentException("Rotating by second or millisecond is not supported");
            }
        }
        this.format = format;
        this.period = period;
    }

    private void rollOver(final File file) {
        final File backup = new File(file.getParentFile(), file.getName() + nextSuffix);
        try {
            rename(file, backup);
        } catch (IOException e) {
            throw ControllerLogger.ROOT_LOGGER.couldNotBackUp(e, file.getAbsolutePath(), backup.getAbsolutePath());
        }
        createNewFile(file);
    }

    private void calcNextRollover(final long fromTime) {
        // method code stolen from the logging subsystem
        if (period == Period.NEVER) {
            nextRollover = Long.MAX_VALUE;
            return;
        }
        nextSuffix = format.format(new Date(fromTime));
        final Calendar calendar = Calendar.getInstance(timeZone);
        calendar.setTimeInMillis(fromTime);
        final Period period = this.period;
        // clear out less-significant fields
        switch (period) {
            default:
            case YEAR:
                calendar.set(Calendar.MONTH, 0);
            case MONTH:
                calendar.set(Calendar.DAY_OF_MONTH, 0);
                calendar.clear(Calendar.WEEK_OF_MONTH);
            case WEEK:
                if (period == Period.WEEK) {
                    calendar.set(Calendar.DAY_OF_WEEK, 0);
                } else {
                    calendar.clear(Calendar.DAY_OF_WEEK);
                }
                calendar.clear(Calendar.DAY_OF_WEEK_IN_MONTH);
            case DAY:
                calendar.set(Calendar.HOUR_OF_DAY, 0);
            case HALF_DAY:
                calendar.set(Calendar.HOUR, 0);
            case HOUR:
                calendar.set(Calendar.MINUTE, 0);
            case MINUTE:
                calendar.set(Calendar.SECOND, 0);
                calendar.set(Calendar.MILLISECOND, 0);
        }
        // increment the relevant field
        switch (period) {
            case YEAR:
                calendar.add(Calendar.YEAR, 1);
                break;
            case MONTH:
                calendar.add(Calendar.MONTH, 1);
                break;
            case WEEK:
                calendar.add(Calendar.WEEK_OF_YEAR, 1);
                break;
            case DAY:
                calendar.add(Calendar.DAY_OF_MONTH, 1);
                break;
            case HALF_DAY:
                calendar.add(Calendar.AM_PM, 1);
                break;
            case HOUR:
                calendar.add(Calendar.HOUR_OF_DAY, 1);
                break;
            case MINUTE:
                calendar.add(Calendar.MINUTE, 1);
                break;
        }
        nextRollover = calendar.getTimeInMillis();
    }

    @Override
    boolean isDifferent(AuditLogHandler other) {
        if (other instanceof PeriodicRotatingFileAuditLogHandler == false){
            return true;
        }
        PeriodicRotatingFileAuditLogHandler otherHandler = (PeriodicRotatingFileAuditLogHandler)other;
        if (!suffix.equals(otherHandler.suffix)) {
            return true;
        }
        if (!timeZone.equals(otherHandler.timeZone)) {
            return true;
        }
        if (super.isDifferent(other)) {
            return true;
        }
        return false;
    }

    private static <T extends Comparable<? super T>> T min(T a, T b) {
        return a.compareTo(b) <= 0 ? a : b;
    }

    /**
     * Possible period values.  Keep in strictly ascending order of magnitude.
     */
    public enum Period {
        MINUTE,
        HOUR,
        HALF_DAY,
        DAY,
        WEEK,
        MONTH,
        YEAR,
        NEVER,
    }

}
