/*
 * Copyright 2010-2019 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
 *
 *      https://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.gemfire.client;

import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.geode.cache.InterestResultPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Constants;
import org.springframework.util.Assert;

/**
 * The Interest class holds details for registering a client interest.
 *
 * @author Costin Leau
 * @author John Blum
 * @see java.util.regex.Pattern
 * @see org.apache.geode.cache.InterestResultPolicy
 * @see org.springframework.beans.factory.InitializingBean
 * @see org.springframework.core.Constants
 * @since 1.0.0
 */
@SuppressWarnings("unused")
public class Interest<K> implements InitializingBean {

	public static final String ALL_KEYS = "ALL_KEYS";

	protected static final boolean DEFAULT_DURABLE = false;
	protected static final boolean DEFAULT_RECEIVE_VALUES = true;

	private static final Constants constants = new Constants(InterestResultPolicy.class);

	protected final Logger logger = LoggerFactory.getLogger(getClass());

	private boolean durable = false;
	private boolean receiveValues = true;

	private InterestResultPolicy policy = InterestResultPolicy.DEFAULT;

	private K key;

	private Type type;

	/**
	 * Factory method to construct a new instance of {@link Interest} initialized with the given key.
	 *
	 * @param <K> {@link Class} type of the key.
	 * @param key key of interest.
	 * @return a new instance of {@link Interest} initialized with the given key.
	 * @see #Interest(Object)
	 */
	public static <K> Interest newInterest(K key) {
		return new Interest<>(key);
	}

	/**
	 * Constructs an instance of non-durable {@link Interest} initialized with the given key to register interest in,
	 * using the {@link InterestResultPolicy#DEFAULT} to initialize the client cache and receiving values by default.
	 *
	 * @param key key(s) of interest.
	 * @see #Interest(Object, InterestResultPolicy, boolean, boolean)
	 */
	public Interest(K key) {
		this(key, InterestResultPolicy.DEFAULT, DEFAULT_DURABLE, DEFAULT_RECEIVE_VALUES);
	}

	/**
	 * Constructs an instance of non-durable {@link Interest} initialized with the given key to register interest in,
	 * the given {@link InterestResultPolicy} used to initialize the client cache, receiving values by default.
	 *
	 * @param key key(s) of interest.
	 * @param policy initial {@link InterestResultPolicy} used to initialize the client cache.
	 * @see #Interest(Object, InterestResultPolicy, boolean, boolean)
	 */
	public Interest(K key, InterestResultPolicy policy) {
		this(key, policy, DEFAULT_DURABLE, DEFAULT_RECEIVE_VALUES);
	}

	/**
	 * Constructs an instance of {@link Interest} initialized with the given key to register interest in,
	 * the given {@link InterestResultPolicy} used to initialize the client cache, the given boolean value
	 * to indicate whether interest registration should be durable, receiving values by default.
	 *
	 * @param key key(s) of interest.
	 * @param policy initial {@link InterestResultPolicy} used to initialize the client cache.
	 * @param durable boolean value to indicate whether the interest registration should be durable.
	 * @see #Interest(Object, InterestResultPolicy, boolean, boolean)
	 */
	public Interest(K key, InterestResultPolicy policy, boolean durable) {
		this(key, policy, durable, DEFAULT_RECEIVE_VALUES);
	}

	/**
	 * Constructs an instance of {@link Interest} initialized with the given key to register interest in,
	 * the given {@link InterestResultPolicy} used to initialize the client cache and the given boolean values
	 * indicating whether interest registration should be durable and whether to receive values during notifications.
	 *
	 * @param key key(s) of interest.
	 * @param policy initial {@link InterestResultPolicy} used to initialize the client cache.
	 * @param durable boolean value to indicate whether the interest registration should be durable.
	 * @param receiveValues boolean value to indicate whether to receive value in notifications.
	 * @see #Interest(Object, InterestResultPolicy, boolean, boolean)
	 * @see #afterPropertiesSet()
	 */
	public Interest(K key, InterestResultPolicy policy, boolean durable, boolean receiveValues) {

		this.key = key;
		this.policy = policy;
		this.durable = durable;
		this.receiveValues = receiveValues;

		afterPropertiesSet();
	}

	/**
	 * @inheritDoc
	 */
	public void afterPropertiesSet() {

		Assert.notNull(this.key, "Key is required");

		setType(resolveType(getType()));
	}

	/**
	 * Attempts to resolve the {@link Interest.Type} based on the configured {@link #getKey()}.
	 *
	 * @param type provided {@link Interest.Type} used if {@literal non-null}.
	 * @return the resolved {@link Interest.Type}.
	 * @see #isRegularExpression(Object)
	 */
	protected Type resolveType(Type type) {
		return (type != null ? type : (isRegularExpression(getKey()) ? Type.REGEX : Type.KEY));
	}

	/**
	 * Determines whether the given {@code key} is a Regular Expression (Regex).
	 *
	 * If the given {@code key} is {@literal "ALL_KEYS"}, a {@link List} or only contains letters, numbers and spaces,
	 * then the {@code key} is not considered a Regular Expression by GemFire, and can be handled with normal
	 * interest registration using {@link org.apache.geode.cache.Region#registerInterest(Object)}.
	 *
	 * @param key {@link Object} to evaluate.
	 * @return a boolean value indicating whether the given {@link Object} {@code key} is a Regular Expression.
	 * @see #isRegularExpression(String)
	 */
	protected boolean isRegularExpression(Object key) {
		return (!(ALL_KEYS.equals(key) || key instanceof List) && isRegularExpression(String.valueOf(key)));
	}

	/**
	 * Determines whether the given {@code key} is a Regular Expression (Regex).
	 *
	 * If the given {@code value} contains at least 1 special character (e.g. *) and can be compiled
	 * using {@link Pattern#compile(String)}, then the {@code key} is considered a Regular Expression
	 * and interest will be registered using {@link org.apache.geode.cache.Region#registerInterestRegex(String)}.
	 *
	 * @param value {@link String} to evaluate.
	 * @return a boolean value indicating whether the given {@link String} {@code value} is a Regular Expression.
	 * @see #containsNonAlphaNumericWhitespace(String)
	 * @see java.util.regex.Pattern#compile(String)
	 */
	@SuppressWarnings("all")
	protected boolean isRegularExpression(String value) {
		try {
			return (containsNonAlphaNumericWhitespace(value) && Pattern.compile(value) != null);
		}
		catch (PatternSyntaxException ignore) {
			return false;
		}
	}

	/**
	 * Determines whether the given {@link String} value contains at least 1 special character.
	 *
	 * @param value {@link String} to evaluate.
	 * @return a boolean value indicating whether the given {@link String} contains at least 1 special character,
	 * a non-alphanumeric, non-whitespace character.
	 * @see #isAlphaNumericWhitespace(char)
	 * @see #isNotAlphaNumericWhitespace(char)
	 */
	protected boolean containsNonAlphaNumericWhitespace(String value) {
		for (char character : String.valueOf(value).toCharArray()) {
			if (isNotAlphaNumericWhitespace(character)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Determines whether the given {@code character} is a special character (non-alphanumeric, non-whitespace).
	 *
	 * @param character {@link Character} to evaluate.
	 * @return a boolean value indicating whether the given {@code character} is a special character.
	 * @see #isAlphaNumericWhitespace(char)
	 */
	protected boolean isNotAlphaNumericWhitespace(char character) {
		return !isAlphaNumericWhitespace(character);
	}

	/**
	 * Determines whether the given {@code character} is an alphanumeric or whitespace character.
	 *
	 * @param character {@link Character} to evaluate.
	 * @return a boolean value indicating whether the given {@code character} is an alphanumeric
	 * or whitespace character.
	 * @see java.lang.Character#isDigit(char)
	 * @see java.lang.Character#isLetter(char)
	 * @see java.lang.Character#isWhitespace(char)
	 */
	protected boolean isAlphaNumericWhitespace(char character) {
		return (Character.isDigit(character) || Character.isLetter(character)  || Character.isWhitespace(character));
	}

	/**
	 * Determines whether the interest registration is durable and persists between cache client sessions.
	 *
	 * @return a boolean value indicating whether this interest registration is durable.
	 */
	public boolean isDurable() {
		return this.durable;
	}

	/**
	 * Sets whether interest registration is durable and persists between cache client sessions.
	 *
	 * @param durable boolean value to indicate whether this interest registration is durable.
	 */
	public void setDurable(boolean durable) {
		this.durable = durable;
	}

	/**
	 * Returns the key on which interest is registered.
	 *
	 * @return the key of interest.
	 */
	public K getKey() {
		return this.key;
	}

	/**
	 * Sets the key on which interest is registered.
	 *
	 * @param key the key of interest.
	 */
	public void setKey(K key) {
		this.key = key;
	}

	/**
	 * Returns the {@link InterestResultPolicy} used when interest is registered and determines whether KEYS,
	 * KEYS_VALUES or nothing (NONE) is initially fetched on initial registration.
	 *
	 * @return the policy
	 */
	public InterestResultPolicy getPolicy() {
		return this.policy;
	}

	/**
	 * Sets the initial {@link InterestResultPolicy} used when interest is first registered and determines whether KEYS,
	 * KEYS_VALUE or nothing (NONE) is initially fetched.
	 *
	 * The argument is set as an {@link Object} to be able to accept both {@link InterestResultPolicy}
	 * and {@link String Strings}, used in XML configuration meta-data.
	 *
	 * @param policy initial {@link InterestResultPolicy} to set.
	 * @throws IllegalArgumentException if the given {@code policy} is not a valid type.
	 * @see org.apache.geode.cache.InterestResultPolicy
	 */
	public void setPolicy(Object policy) {

		if (policy instanceof InterestResultPolicy) {
			this.policy = (InterestResultPolicy) policy;
		}
		else if (policy instanceof String) {
			this.policy = (InterestResultPolicy) constants.asObject(String.valueOf(policy));
		}
		else {
			throw new IllegalArgumentException(String.format("Unknown argument type [%s] for property policy", policy));
		}
	}

	/**
	 * Returns the type of values received by the listener.
	 *
	 * @return the receiveValues
	 */
	public boolean isReceiveValues() {
		return this.receiveValues;
	}

	/**
	 * Switches between the different entities received by the listener.
	 *
	 * @param receiveValues the receiveValues to set
	 */
	public void setReceiveValues(boolean receiveValues) {
		this.receiveValues = receiveValues;
	}

	/**
	 * Returns the type of interest registration (e.g. based on KEY or Regex).
	 *
	 * @return a {@link Interest.Type} determining the type of interest.
	 * @see Interest.Type
	 */
	public Type getType() {
		return this.type;
	}

	/**
	 * Set the type of interest registration (e.g. based on KEY or Regex).
	 *
	 * @param type {@link Interest.Type} qualifying the type of interest.
	 * @see Interest.Type
	 */
	public void setType(Type type) {
		this.type = type;
	}

	/**
	 * Determines whether this {@link Interest} is a KEY interest registration.
	 *
	 * @return a boolean value indicating whether this is KEY interest.
	 * @see Interest.Type#KEY
	 * @see #getType()
	 */
	public boolean isKeyType() {
		return Type.KEY.equals(getType());
	}

	/**
	 * Determines whether this {@link Interest} is a REGEX interest registration.
	 *
	 * @return a boolean value indicating whether this is REGEX interest.
	 * @see Interest.Type#REGEX
	 * @see #getType()
	 */
	public boolean isRegexType() {
		return Type.REGEX.equals(getType());
	}

	/**
	 * @inheritDoc
	 */
	@Override
	public String toString() {
		return String.format("{ @type = %1$s, key = %2$s, durable = %3$s, policy = %4$s, receiveValues = %5$s, type = %6$s }",
			getClass().getName(), getKey(), isDurable(), getPolicy(), isReceiveValues(), getType());
	}

	/**
	 * Builder method to specify the type of interest registration.
	 *
	 * @param type {@link Interest.Type} of interest registration.
	 * @return this {@link Interest}.
	 * @see Interest.Type
	 * @see #resolveType(Type)
	 * @see #setType(Type)
	 */
	public Interest asType(Type type) {
		setType(resolveType(type));
		return this;
	}

	/**
	 * Builder method to mark this {@link Interest} as durable.
	 *
	 * @return this {@link Interest}.
	 * @see #setDurable(boolean)
	 */
	public Interest makeDurable() {
		setDurable(true);
		return this;
	}

	/**
	 * Builder method to set whether the interest event notifications will receive values along with keys.
	 *
	 * @param receiveValues boolean to indicate that value should be sent along with keys
	 * on interest event notifications.
	 * @return this {@link Interest}.
	 * @see #setReceiveValues(boolean)
	 */
	public Interest receivesValues(boolean receiveValues) {
		setReceiveValues(receiveValues);
		return this;
	}

	/**
	 * Builder method to set the {@link InterestResultPolicy} used to initialize the cache.
	 *
	 * @param policy {@link InterestResultPolicy}.
	 * @return this {@link Interest}.
	 * @see org.apache.geode.cache.InterestResultPolicy
	 * @see #setPolicy(Object)
	 */
	public Interest usingPolicy(InterestResultPolicy policy) {
		setPolicy(policy);
		return this;
	}

	/**
	 * Builder method to express the key of interest.
	 *
	 * @param key key of interests.
	 * @return this {@link Interest}.
	 * @see #setKey(Object)
	 * @see #getType()
	 * @see #resolveType(Type)
	 * @see #setType(Type)
	 */
	public Interest withKey(K key) {
		setKey(key);
		setType(resolveType(getType()));
		return this;
	}

	/**
	 * Type of interest registration.
	 */
	public enum Type {
		KEY, REGEX
	}
}
