/*
 * Copyright 2016. the original author or authors.
 *
 * 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 org.springframework.data.mongodb.core.aggregation;

import java.util.LinkedHashMap;

import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators.ArithmeticOperatorFactory;
import org.springframework.util.Assert;

/**
 * Gateway to {@literal Date} aggregation operations.
 *
 * @author Christoph Strobl
 * @since 1.10
 */
public class DateOperators {

	/**
	 * Take the date referenced by given {@literal fieldReference}.
	 *
	 * @param fieldReference must not be {@literal null}.
	 * @return
	 */
	public static DateOperatorFactory dateOf(String fieldReference) {

		Assert.notNull(fieldReference, "FieldReference must not be null!");
		return new DateOperatorFactory(fieldReference);
	}

	/**
	 * Take the date resulting from the given {@link AggregationExpression}.
	 *
	 * @param expression must not be {@literal null}.
	 * @return
	 */
	public static DateOperatorFactory dateOf(AggregationExpression expression) {

		Assert.notNull(expression, "Expression must not be null!");
		return new DateOperatorFactory(expression);
	}

	/**
	 * @author Christoph Strobl
	 */
	public static class DateOperatorFactory {

		private final String fieldReference;
		private final AggregationExpression expression;

		/**
		 * Creates new {@link ArithmeticOperatorFactory} for given {@literal fieldReference}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 */
		public DateOperatorFactory(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			this.fieldReference = fieldReference;
			this.expression = null;
		}

		/**
		 * Creates new {@link ArithmeticOperatorFactory} for given {@link AggregationExpression}.
		 *
		 * @param expression must not be {@literal null}.
		 */
		public DateOperatorFactory(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			this.fieldReference = null;
			this.expression = expression;
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and
		 * 366.
		 *
		 * @return
		 */
		public DayOfYear dayOfYear() {
			return usesFieldRef() ? DayOfYear.dayOfYear(fieldReference) : DayOfYear.dayOfYear(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the day of the month for a date as a number between 1 and
		 * 31.
		 *
		 * @return
		 */
		public DayOfMonth dayOfMonth() {
			return usesFieldRef() ? DayOfMonth.dayOfMonth(fieldReference) : DayOfMonth.dayOfMonth(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the day of the week for a date as a number between 1
		 * (Sunday) and 7 (Saturday).
		 *
		 * @return
		 */
		public DayOfWeek dayOfWeek() {
			return usesFieldRef() ? DayOfWeek.dayOfWeek(fieldReference) : DayOfWeek.dayOfWeek(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the year portion of a date.
		 *
		 * @return
		 */
		public Year year() {
			return usesFieldRef() ? Year.yearOf(fieldReference) : Year.yearOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the month of a date as a number between 1 and 12.
		 *
		 * @return
		 */
		public Month month() {
			return usesFieldRef() ? Month.monthOf(fieldReference) : Month.monthOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the week of the year for a date as a number between 0 and
		 * 53.
		 *
		 * @return
		 */
		public Week week() {
			return usesFieldRef() ? Week.weekOf(fieldReference) : Week.weekOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the hour portion of a date as a number between 0 and 23.
		 *
		 * @return
		 */
		public Hour hour() {
			return usesFieldRef() ? Hour.hourOf(fieldReference) : Hour.hourOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the minute portion of a date as a number between 0 and 59.
		 *
		 * @return
		 */
		public Minute minute() {
			return usesFieldRef() ? Minute.minuteOf(fieldReference) : Minute.minuteOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the second portion of a date as a number between 0 and 59,
		 * but can be 60 to account for leap seconds.
		 *
		 * @return
		 */
		public Second second() {
			return usesFieldRef() ? Second.secondOf(fieldReference) : Second.secondOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the millisecond portion of a date as an integer between 0
		 * and 999.
		 *
		 * @return
		 */
		public Millisecond millisecond() {
			return usesFieldRef() ? Millisecond.millisecondOf(fieldReference) : Millisecond.millisecondOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that converts a date object to a string according to a user-specified
		 * {@literal format}.
		 *
		 * @param format must not be {@literal null}.
		 * @return
		 */
		public DateToString toString(String format) {
			return (usesFieldRef() ? DateToString.dateOf(fieldReference) : DateToString.dateOf(expression)).toString(format);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the weekday number in ISO 8601 format, ranging from 1 (for
		 * Monday) to 7 (for Sunday).
		 *
		 * @return
		 */
		public IsoDayOfWeek isoDayOfWeek() {
			return usesFieldRef() ? IsoDayOfWeek.isoDayOfWeek(fieldReference) : IsoDayOfWeek.isoDayOfWeek(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the week number in ISO 8601 format, ranging from 1 to 53.
		 *
		 * @return
		 */
		public IsoWeek isoWeek() {
			return usesFieldRef() ? IsoWeek.isoWeekOf(fieldReference) : IsoWeek.isoWeekOf(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns the year number in ISO 8601 format.
		 *
		 * @return
		 */
		public IsoWeekYear isoWeekYear() {
			return usesFieldRef() ? IsoWeekYear.isoWeekYearOf(fieldReference) : IsoWeekYear.isoWeekYearOf(expression);
		}

		private boolean usesFieldRef() {
			return fieldReference != null;
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $dayOfYear}.
	 *
	 * @author Christoph Strobl
	 */
	public static class DayOfYear extends AbstractAggregationExpression {

		private DayOfYear(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$dayOfYear";
		}

		/**
		 * Creates new {@link DayOfYear}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static DayOfYear dayOfYear(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new DayOfYear(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link DayOfYear}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static DayOfYear dayOfYear(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new DayOfYear(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $dayOfMonth}.
	 *
	 * @author Christoph Strobl
	 */
	public static class DayOfMonth extends AbstractAggregationExpression {

		private DayOfMonth(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$dayOfMonth";
		}

		/**
		 * Creates new {@link DayOfMonth}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static DayOfMonth dayOfMonth(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new DayOfMonth(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link DayOfMonth}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static DayOfMonth dayOfMonth(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new DayOfMonth(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $dayOfWeek}.
	 *
	 * @author Christoph Strobl
	 */
	public static class DayOfWeek extends AbstractAggregationExpression {

		private DayOfWeek(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$dayOfWeek";
		}

		/**
		 * Creates new {@link DayOfWeek}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static DayOfWeek dayOfWeek(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new DayOfWeek(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link DayOfWeek}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static DayOfWeek dayOfWeek(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new DayOfWeek(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $year}.
	 *
	 * @author Christoph Strobl
	 */
	public static class Year extends AbstractAggregationExpression {

		private Year(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$year";
		}

		/**
		 * Creates new {@link Year}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Year yearOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new Year(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Year}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Year yearOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new Year(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $month}.
	 *
	 * @author Christoph Strobl
	 */
	public static class Month extends AbstractAggregationExpression {

		private Month(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$month";
		}

		/**
		 * Creates new {@link Month}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Month monthOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new Month(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Month}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Month monthOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new Month(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $week}.
	 *
	 * @author Christoph Strobl
	 */
	public static class Week extends AbstractAggregationExpression {

		private Week(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$week";
		}

		/**
		 * Creates new {@link Week}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Week weekOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new Week(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Week}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Week weekOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new Week(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $hour}.
	 *
	 * @author Christoph Strobl
	 */
	public static class Hour extends AbstractAggregationExpression {

		private Hour(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$hour";
		}

		/**
		 * Creates new {@link Hour}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Hour hourOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new Hour(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Hour}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Hour hourOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new Hour(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $minute}.
	 *
	 * @author Christoph Strobl
	 */
	public static class Minute extends AbstractAggregationExpression {

		private Minute(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$minute";
		}

		/**
		 * Creates new {@link Minute}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Minute minuteOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new Minute(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Minute}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Minute minuteOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new Minute(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $second}.
	 *
	 * @author Christoph Strobl
	 */
	public static class Second extends AbstractAggregationExpression {

		private Second(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$second";
		}

		/**
		 * Creates new {@link Second}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Second secondOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new Second(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Second}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Second secondOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new Second(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $millisecond}.
	 *
	 * @author Christoph Strobl
	 */
	public static class Millisecond extends AbstractAggregationExpression {

		private Millisecond(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$millisecond";
		}

		/**
		 * Creates new {@link Millisecond}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Millisecond millisecondOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new Millisecond(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Millisecond}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Millisecond millisecondOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new Millisecond(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $dateToString}.
	 *
	 * @author Christoph Strobl
	 */
	public static class DateToString extends AbstractAggregationExpression {

		private DateToString(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$dateToString";
		}

		/**
		 * Creates new {@link FormatBuilder} allowing to define the date format to apply.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static FormatBuilder dateOf(final String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");

			return new FormatBuilder() {

				@Override
				public DateToString toString(String format) {

					Assert.notNull(format, "Format must not be null!");
					return new DateToString(argumentMap(Fields.field(fieldReference), format));
				}
			};
		}

		/**
		 * Creates new {@link FormatBuilder} allowing to define the date format to apply.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static FormatBuilder dateOf(final AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");

			return new FormatBuilder() {

				@Override
				public DateToString toString(String format) {

					Assert.notNull(format, "Format must not be null!");
					return new DateToString(argumentMap(expression, format));
				}
			};
		}

		private static java.util.Map<String, Object> argumentMap(Object date, String format) {

			java.util.Map<String, Object> args = new LinkedHashMap<String, Object>(2);
			args.put("format", format);
			args.put("date", date);
			return args;
		}

		public interface FormatBuilder {

			/**
			 * Creates new {@link DateToString} with all previously added arguments appending the given one.
			 *
			 * @param format must not be {@literal null}.
			 * @return
			 */
			DateToString toString(String format);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $isoDayOfWeek}.
	 *
	 * @author Christoph Strobl
	 */
	public static class IsoDayOfWeek extends AbstractAggregationExpression {

		private IsoDayOfWeek(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$isoDayOfWeek";
		}

		/**
		 * Creates new {@link IsoDayOfWeek}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static IsoDayOfWeek isoDayOfWeek(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new IsoDayOfWeek(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link IsoDayOfWeek}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new IsoDayOfWeek(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $isoWeek}.
	 *
	 * @author Christoph Strobl
	 */
	public static class IsoWeek extends AbstractAggregationExpression {

		private IsoWeek(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$isoWeek";
		}

		/**
		 * Creates new {@link IsoWeek}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static IsoWeek isoWeekOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new IsoWeek(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link IsoWeek}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static IsoWeek isoWeekOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new IsoWeek(expression);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $isoWeekYear}.
	 *
	 * @author Christoph Strobl
	 */
	public static class IsoWeekYear extends AbstractAggregationExpression {

		private IsoWeekYear(Object value) {
			super(value);
		}

		@Override
		protected String getMongoMethod() {
			return "$isoWeekYear";
		}

		/**
		 * Creates new {@link IsoWeekYear}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static IsoWeekYear isoWeekYearOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null!");
			return new IsoWeekYear(Fields.field(fieldReference));
		}

		/**
		 * Creates new {@link Millisecond}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static IsoWeekYear isoWeekYearOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null!");
			return new IsoWeekYear(expression);
		}
	}
}
