/*
 * Copyright 2008-2019 by Emeric Vernat
 *
 *     This file is part of Java Melody.
 *
 * Licensed 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 net.bull.javamelody.internal.model;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

import net.bull.javamelody.internal.common.LOG;

/**
 * Factory pour les compteurs par jour, par semaine, par mois et par année.
 * @author Emeric Vernat
 */
class PeriodCounterFactory {
	// Note d'implémentation : Calendar.getInstance() crée à chaque appel une nouvelle instance
	// de Calendar à la date et à l'heure courante (cette date-heure peut être modifiée)

	private final Counter currentDayCounter;

	PeriodCounterFactory(Counter currentDayCounter) {
		super();
		assert currentDayCounter != null;
		this.currentDayCounter = currentDayCounter;
	}

	Counter buildNewDayCounter() throws IOException {
		final Calendar start = Calendar.getInstance();
		start.setTime(currentDayCounter.getStartDate());
		if (start.get(Calendar.MONTH) != Calendar.getInstance().get(Calendar.MONTH)) {
			// le mois a changé, on crée un compteur vide qui sera enregistré dans un nouveau fichier;
			// ce compteur agrégé pour le mois est utilisé pour de meilleurs performances sur le compteur de l'année
			// on calcule le monthCounter et on l'enregistre (optimisation pour getYearCounter)
			getMonthCounterAtDate(currentDayCounter.getStartDate());
		}

		return createDayCounterAtDate(new Date());
	}

	// compteur d'un jour donné
	private Counter getDayCounterAtDate(Date day) {
		final Counter dayCounter = createDayCounterAtDate(day);
		try {
			dayCounter.readFromFile();
		} catch (final IOException e) {
			// lecture échouée, tant pis
			// (on n'interrompt pas tout un rapport juste pour un des fichiers illisible)
			LOG.info("read of a counter file failed: " + dayCounter.getName(), e);
		}
		return dayCounter;
	}

	// compteur custom
	Counter getCustomCounter(Range range) {
		assert range.getPeriod() == null;
		final Counter customCounter = createPeriodCounter("yyyy-MM-dd", range.getStartDate());
		addRequestsAndErrorsForRange(customCounter, range);
		return customCounter;
	}

	// compteur du jour courant
	Counter getDayCounter() {
		return currentDayCounter;
	}

	// compteur des 7 derniers jours
	Counter getWeekCounter() {
		final Counter weekCounter = createPeriodCounter("yyyyWW", currentDayCounter.getStartDate());
		addRequestsAndErrorsForRange(weekCounter, Period.SEMAINE.getRange());
		return weekCounter;
	}

	// compteur des 31 derniers jours,
	// ici c'est un mois flottant (ie une durée), et pas un mois entier
	Counter getMonthCounter() {
		final Counter monthCounter = createMonthCounterAtDate(currentDayCounter.getStartDate());
		addRequestsAndErrorsForRange(monthCounter, Period.MOIS.getRange());
		return monthCounter;
	}

	private void addRequestsAndErrorsForRange(Counter counter, Range range) {
		final Calendar dayCalendar = Calendar.getInstance();
		if (range.getPeriod() == null) {
			counter.addRequestsAndErrors(getDayCounterAtDate(range.getEndDate()));
			dayCalendar.setTime(range.getEndDate());
			// issue 122: attention endDate contient un jour jusqu'à 23h59m59s selon Range.parse
			dayCalendar.set(Calendar.HOUR_OF_DAY, 0);
			dayCalendar.set(Calendar.MINUTE, 0);
			dayCalendar.set(Calendar.SECOND, 0);
		} else {
			counter.addRequestsAndErrors(currentDayCounter);
			dayCalendar.setTime(currentDayCounter.getStartDate());
		}
		final int durationDays = range.getDurationDays();
		for (int i = 1; i < durationDays; i++) {
			// TODO optimisation avec getMonthCounterAtDate comme getYearCounter() ?
			dayCalendar.add(Calendar.DAY_OF_YEAR, -1);
			counter.addRequestsAndErrors(getDayCounterAtDate(dayCalendar.getTime()));
		}
		counter.setStartDate(dayCalendar.getTime());
	}

	// compteur des 366 derniers jours
	Counter getYearCounter() throws IOException {
		final Counter yearCounter = createPeriodCounter("yyyy", currentDayCounter.getStartDate());
		yearCounter.addRequestsAndErrors(currentDayCounter);
		final Calendar dayCalendar = Calendar.getInstance();
		final int currentMonth = dayCalendar.get(Calendar.MONTH);
		dayCalendar.setTime(currentDayCounter.getStartDate());
		dayCalendar.add(Calendar.DAY_OF_YEAR, -Period.ANNEE.getDurationDays() + 1);
		yearCounter.setStartDate(dayCalendar.getTime());
		for (int i = 1; i < Period.ANNEE.getDurationDays(); i++) {
			if (dayCalendar.get(Calendar.DAY_OF_MONTH) == 1
					&& dayCalendar.get(Calendar.MONTH) != currentMonth) {
				// optimisation : on récupère les statistiques précédemment calculées pour ce mois entier
				// au lieu de parcourir à chaque fois les statistiques de chaque jour du mois
				yearCounter.addRequestsAndErrors(getMonthCounterAtDate(dayCalendar.getTime()));
				final int nbDaysInMonth = dayCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
				// nbDaysInMonth - 1 puisque l'itération va ajouter 1 à i et à dayCalendar
				dayCalendar.add(Calendar.DAY_OF_YEAR, nbDaysInMonth - 1);
				i += nbDaysInMonth - 1;
			} else {
				yearCounter.addRequestsAndErrors(getDayCounterAtDate(dayCalendar.getTime()));
			}
			dayCalendar.add(Calendar.DAY_OF_YEAR, 1);
		}
		return yearCounter;
	}

	private Counter getMonthCounterAtDate(Date day) throws IOException {
		final Counter monthCounter = createMonthCounterAtDate(day);
		try {
			final Counter readCounter = new CounterStorage(monthCounter).readFromFile();
			if (readCounter != null) {
				// monthCounter déjà calculé et enregistré
				return readCounter;
			}
		} catch (final IOException e) {
			// lecture échouée, tant pis
			// (on n'interrompt pas tout un rapport juste pour un des fichiers illisible)
			LOG.info("read of a counter file failed: " + monthCounter.getName(), e);
		}
		// monthCounter n'est pas encore calculé (il est calculé à la fin de chaque mois,
		// mais le serveur a pu aussi être arrêté ce jour là),
		// alors on le calcule et on l'enregistre (optimisation pour getYearCounter)
		final Calendar dayCalendar = Calendar.getInstance();
		dayCalendar.setTime(day);
		final int nbDaysInMonth = dayCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
		for (int i = 1; i <= nbDaysInMonth; i++) {
			dayCalendar.set(Calendar.DAY_OF_MONTH, i);
			monthCounter.addRequestsAndErrors(getDayCounterAtDate(dayCalendar.getTime()));
		}
		monthCounter.writeToFile();
		return monthCounter;
	}

	Counter createDayCounterAtDate(Date day) {
		// le nom du compteur par jour est celui du compteur initial
		// auquel on ajoute la date en suffixe pour que son enregistrement soit unique
		return createPeriodCounter("yyyyMMdd", day);
	}

	private Counter createMonthCounterAtDate(Date day) {
		// le nom du compteur par mois est celui du compteur initial
		// auquel on ajoute le mois en suffixe pour que son enregistrement soit unique
		return createPeriodCounter("yyyyMM", day);
	}

	private Counter createPeriodCounter(String dateFormatPattern, Date date) {
		final String storageName = currentDayCounter.getName() + '_'
				+ new SimpleDateFormat(dateFormatPattern, Locale.getDefault()).format(date);
		// ceci crée une nouvelle instance sans requêtes avec startDate à la date courante
		final Counter result = new Counter(currentDayCounter.getName(), storageName,
				currentDayCounter.getIconName(), currentDayCounter.getChildCounterName());
		result.setApplication(currentDayCounter.getApplication());
		result.setDisplayed(currentDayCounter.isDisplayed());
		result.setRequestTransformPattern(currentDayCounter.getRequestTransformPattern());
		result.setMaxRequestsCount(currentDayCounter.getMaxRequestsCount());
		return result;
	}
}
