/*
 * 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.Arrays;
import java.util.Collections;
import java.util.List;

import org.springframework.util.Assert;

/**
 * Gateway to {@literal boolean expressions} that evaluate their argument expressions as booleans and return a boolean
 * as the result.
 *
 * @author Christoph Strobl
 * @since 1.10
 */
public class BooleanOperators {

	/**
	 * Take the array referenced by given {@literal fieldReference}.
	 *
	 * @param fieldReference must not be {@literal null}.
	 * @return
	 */
	public static BooleanOperatorFactory valueOf(String fieldReference) {
		return new BooleanOperatorFactory(fieldReference);
	}

	/**
	 * Take the value resulting of the given {@link AggregationExpression}.
	 *
	 * @param fieldReference must not be {@literal null}.
	 * @return
	 */
	public static BooleanOperatorFactory valueOf(AggregationExpression fieldReference) {
		return new BooleanOperatorFactory(fieldReference);
	}

	/**
	 * Creates new {@link AggregationExpression} that evaluates the boolean value of the referenced field and returns the
	 * opposite boolean value.
	 *
	 * @param fieldReference must not be {@literal null}.
	 * @return
	 */
	public static Not not(String fieldReference) {
		return Not.not(fieldReference);
	}

	/**
	 * Creates new {@link AggregationExpression} that evaluates the boolean value of {@link AggregationExpression} result
	 * and returns the opposite boolean value.
	 *
	 * @param expression must not be {@literal null}.
	 * @return
	 */
	public static Not not(AggregationExpression expression) {
		return Not.not(expression);
	}

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

		private final String fieldReference;
		private final AggregationExpression expression;

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

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

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

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

		/**
		 * Creates new {@link AggregationExpression} that evaluates one or more expressions and returns {@literal true} if
		 * all of the expressions are {@literal true}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public And and(AggregationExpression expression) {

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

		/**
		 * Creates new {@link AggregationExpression} that evaluates one or more expressions and returns {@literal true} if
		 * all of the expressions are {@literal true}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public And and(String fieldReference) {

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

		private And createAnd() {
			return usesFieldRef() ? And.and(Fields.field(fieldReference)) : And.and(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that evaluates one or more expressions and returns {@literal true} if
		 * any of the expressions are {@literal true}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public Or or(AggregationExpression expression) {

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

		/**
		 * Creates new {@link AggregationExpression} that evaluates one or more expressions and returns {@literal true} if
		 * any of the expressions are {@literal true}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public Or or(String fieldReference) {

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

		private Or createOr() {
			return usesFieldRef() ? Or.or(Fields.field(fieldReference)) : Or.or(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that evaluates a boolean and returns the opposite boolean value.
		 *
		 * @return
		 */
		public Not not() {
			return usesFieldRef() ? Not.not(fieldReference) : Not.not(expression);
		}

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

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

		private And(List<?> values) {
			super(values);
		}

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

		/**
		 * Creates new {@link And} that evaluates one or more expressions and returns {@literal true} if all of the
		 * expressions are {@literal true}.
		 *
		 * @param expressions
		 * @return
		 */
		public static And and(Object... expressions) {
			return new And(Arrays.asList(expressions));
		}

		/**
		 * Creates new {@link And} with all previously added arguments appending the given one.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public And andExpression(AggregationExpression expression) {

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

		/**
		 * Creates new {@link And} with all previously added arguments appending the given one.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public And andField(String fieldReference) {

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

		/**
		 * Creates new {@link And} with all previously added arguments appending the given one.
		 *
		 * @param value must not be {@literal null}.
		 * @return
		 */
		public And andValue(Object value) {

			Assert.notNull(value, "Value must not be null!");
			return new And(append(value));
		}
	}

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

		private Or(List<?> values) {
			super(values);
		}

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

		/**
		 * Creates new {@link Or} that evaluates one or more expressions and returns {@literal true} if any of the
		 * expressions are {@literal true}.
		 *
		 * @param expressions must not be {@literal null}.
		 * @return
		 */
		public static Or or(Object... expressions) {

			Assert.notNull(expressions, "Expressions must not be null!");
			return new Or(Arrays.asList(expressions));
		}

		/**
		 * Creates new {@link Or} with all previously added arguments appending the given one.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public Or orExpression(AggregationExpression expression) {

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

		/**
		 * Creates new {@link Or} with all previously added arguments appending the given one.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public Or orField(String fieldReference) {

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

		/**
		 * Creates new {@link Or} with all previously added arguments appending the given one.
		 *
		 * @param value must not be {@literal null}.
		 * @return
		 */
		public Or orValue(Object value) {

			Assert.notNull(value, "Value must not be null!");
			return new Or(append(value));
		}
	}

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

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

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

		/**
		 * Creates new {@link Not} that evaluates the boolean value of the referenced field and returns the opposite boolean
		 * value.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return
		 */
		public static Not not(String fieldReference) {

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

		/**
		 * Creates new {@link Not} that evaluates the resulting boolean value of the given {@link AggregationExpression} and
		 * returns the opposite boolean value.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static Not not(AggregationExpression expression) {

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