/*
 * 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.data.mongodb.core.aggregation.AccumulatorOperators.Sum;
import org.springframework.util.Assert;

/**
 * Gateway to {@literal Set expressions} which perform {@literal set} operation on arrays, treating arrays as sets.
 *
 * @author Christoph Strobl
 * @since 1.10
 */
public class SetOperators {

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

	/**
	 * Take the array resulting from the given {@link AggregationExpression}.
	 *
	 * @param expression must not be {@literal null}.
	 * @return
	 */
	public static SetOperatorFactory arrayAsSet(AggregationExpression expression) {
		return new SetOperatorFactory(expression);
	}

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

		private final String fieldReference;
		private final AggregationExpression expression;

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

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

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

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

		/**
		 * Creates new {@link AggregationExpression} that compares the previously mentioned field to one or more arrays and
		 * returns {@literal true} if they have the same distinct elements and {@literal false} otherwise.
		 *
		 * @param arrayReferences must not be {@literal null}.
		 * @return
		 */
		public SetEquals isEqualTo(String... arrayReferences) {
			return createSetEquals().isEqualTo(arrayReferences);
		}

		/**
		 * Creates new {@link AggregationExpression} that compares the previously mentioned field to one or more arrays and
		 * returns {@literal true} if they have the same distinct elements and {@literal false} otherwise.
		 *
		 * @param expressions must not be {@literal null}.
		 * @return
		 */
		public SetEquals isEqualTo(AggregationExpression... expressions) {
			return createSetEquals().isEqualTo(expressions);
		}

		private SetEquals createSetEquals() {
			return usesFieldRef() ? SetEquals.arrayAsSet(fieldReference) : SetEquals.arrayAsSet(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and one or more
		 * arrays and returns an array that contains the elements that appear in every of those.
		 *
		 * @param arrayReferences must not be {@literal null}.
		 * @return
		 */
		public SetIntersection intersects(String... arrayReferences) {
			return createSetIntersection().intersects(arrayReferences);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and one or more
		 * arrays and returns an array that contains the elements that appear in every of those.
		 *
		 * @param expressions must not be {@literal null}.
		 * @return
		 */
		public SetIntersection intersects(AggregationExpression... expressions) {
			return createSetIntersection().intersects(expressions);
		}

		private SetIntersection createSetIntersection() {
			return usesFieldRef() ? SetIntersection.arrayAsSet(fieldReference) : SetIntersection.arrayAsSet(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and one or more
		 * arrays and returns an array that contains the elements that appear in any of those.
		 *
		 * @param arrayReferences must not be {@literal null}.
		 * @return
		 */
		public SetUnion union(String... arrayReferences) {
			return createSetUnion().union(arrayReferences);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and one or more
		 * arrays and returns an array that contains the elements that appear in any of those.
		 *
		 * @param expressions must not be {@literal null}.
		 * @return
		 */
		public SetUnion union(AggregationExpression... expressions) {
			return createSetUnion().union(expressions);
		}

		private SetUnion createSetUnion() {
			return usesFieldRef() ? SetUnion.arrayAsSet(fieldReference) : SetUnion.arrayAsSet(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and returns an array
		 * containing the elements that do not exist in the given {@literal arrayReference}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public SetDifference differenceTo(String arrayReference) {
			return createSetDifference().differenceTo(arrayReference);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and returns an array
		 * containing the elements that do not exist in the given {@link AggregationExpression}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public SetDifference differenceTo(AggregationExpression expression) {
			return createSetDifference().differenceTo(expression);
		}

		private SetDifference createSetDifference() {
			return usesFieldRef() ? SetDifference.arrayAsSet(fieldReference) : SetDifference.arrayAsSet(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and returns
		 * {@literal true} if it is a subset of the given {@literal arrayReference}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public SetIsSubset isSubsetOf(String arrayReference) {
			return createSetIsSubset().isSubsetOf(arrayReference);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and returns
		 * {@literal true} if it is a subset of the given {@link AggregationExpression}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public SetIsSubset isSubsetOf(AggregationExpression expression) {
			return createSetIsSubset().isSubsetOf(expression);
		}

		private SetIsSubset createSetIsSubset() {
			return usesFieldRef() ? SetIsSubset.arrayAsSet(fieldReference) : SetIsSubset.arrayAsSet(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes array of the previously mentioned field and returns
		 * {@literal true} if any of the elements are {@literal true} and {@literal false} otherwise.
		 *
		 * @return
		 */
		public AnyElementTrue anyElementTrue() {
			return usesFieldRef() ? AnyElementTrue.arrayAsSet(fieldReference) : AnyElementTrue.arrayAsSet(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that tkes array of the previously mentioned field and returns
		 * {@literal true} if no elements is {@literal false}.
		 *
		 * @return
		 */
		public AllElementsTrue allElementsTrue() {
			return usesFieldRef() ? AllElementsTrue.arrayAsSet(fieldReference) : AllElementsTrue.arrayAsSet(expression);
		}

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

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

		private SetEquals(List<?> arrays) {
			super(arrays);
		}

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

		/**
		 * Create new {@link SetEquals}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public static SetEquals arrayAsSet(String arrayReference) {

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new SetEquals(asFields(arrayReference));
		}

		/**
		 * Create new {@link SetEquals}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return
		 */
		public static SetEquals arrayAsSet(AggregationExpression expression) {

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

		/**
		 * Creates new {@link java.util.Set} with all previously added arguments appending the given one.
		 *
		 * @param arrayReferences must not be {@literal null}.
		 * @return
		 */
		public SetEquals isEqualTo(String... arrayReferences) {

			Assert.notNull(arrayReferences, "ArrayReferences must not be null!");
			return new SetEquals(append(Fields.fields(arrayReferences).asList()));
		}

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

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

		/**
		 * Creates new {@link Sum} with all previously added arguments appending the given one.
		 *
		 * @param array must not be {@literal null}.
		 * @return
		 */
		public SetEquals isEqualTo(Object[] array) {

			Assert.notNull(array, "Array must not be null!");
			return new SetEquals(append(array));
		}
	}

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

		private SetIntersection(List<?> arrays) {
			super(arrays);
		}

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

		/**
		 * Creates new {@link SetIntersection}
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public static SetIntersection arrayAsSet(String arrayReference) {

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new SetIntersection(asFields(arrayReference));
		}

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

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

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

			Assert.notNull(arrayReferences, "ArrayReferences must not be null!");
			return new SetIntersection(append(asFields(arrayReferences)));
		}

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

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

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

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

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

		/**
		 * Creates new {@link SetUnion}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public static SetUnion arrayAsSet(String arrayReference) {

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new SetUnion(asFields(arrayReference));
		}

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

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

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

			Assert.notNull(arrayReferences, "ArrayReferences must not be null!");
			return new SetUnion(append(asFields(arrayReferences)));
		}

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

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

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

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

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

		/**
		 * Creates new {@link SetDifference}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public static SetDifference arrayAsSet(String arrayReference) {

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new SetDifference(asFields(arrayReference));
		}

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

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

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

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new SetDifference(append(Fields.field(arrayReference)));
		}

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

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

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

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

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

		/**
		 * Creates new {@link SetIsSubset}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public static SetIsSubset arrayAsSet(String arrayReference) {

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new SetIsSubset(asFields(arrayReference));
		}

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

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

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

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new SetIsSubset(append(Fields.field(arrayReference)));
		}

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

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

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

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

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

		/**
		 * Creates new {@link AnyElementTrue}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public static AnyElementTrue arrayAsSet(String arrayReference) {

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new AnyElementTrue(asFields(arrayReference));
		}

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

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

		public AnyElementTrue anyElementTrue() {
			return this;
		}
	}

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

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

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

		/**
		 * Creates new {@link AllElementsTrue}.
		 *
		 * @param arrayReference must not be {@literal null}.
		 * @return
		 */
		public static AllElementsTrue arrayAsSet(String arrayReference) {

			Assert.notNull(arrayReference, "ArrayReference must not be null!");
			return new AllElementsTrue(asFields(arrayReference));
		}

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

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

		public AllElementsTrue allElementsTrue() {
			return this;
		}
	}
}
