/*
 * Copyright 2013-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.integration.redis.metadata;

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.support.collections.RedisProperties;
import org.springframework.integration.metadata.ConcurrentMetadataStore;
import org.springframework.util.Assert;

/**
 * Redis implementation of {@link ConcurrentMetadataStore}. Use this
 * {@link ConcurrentMetadataStore} to achieve meta-data persistence across application
 * restarts.
 * <p>
 * This implementation is based on the {@link RedisProperties} and its
 * {@link #replace(String, String, String)}; it can't currently be used with a Redis
 * cluster because the {@code WATCH} command is not supported.
 *
 * @author Gunnar Hillert
 * @author Artem Bilan
 *
 * @since 3.0
 */
public class RedisMetadataStore implements ConcurrentMetadataStore {

	private static final String KEY_MUST_NOT_BE_NULL = "'key' must not be null.";

	public static final String KEY = "MetaData";

	private final RedisProperties properties;

	/**
	 * Specifies the {@link RedisProperties} backend for this {@link ConcurrentMetadataStore}.
	 *
	 * @param properties The properties.
	 */
	public RedisMetadataStore(RedisProperties properties) {
		Assert.notNull(properties, "'properties' must not be null.");
		this.properties = properties;
	}

	/**
	 * Initializes the {@link RedisProperties} by provided {@link RedisConnectionFactory}
	 * and default hash key - {@link #KEY}.
	 *
	 * @param connectionFactory The connection factory.
	 */
	public RedisMetadataStore(RedisConnectionFactory connectionFactory) {
		this(connectionFactory, KEY);
	}

	/**
	 * Initializes the {@link RedisProperties} by provided {@link RedisConnectionFactory} and key.
	 *
	 * @param connectionFactory The connection factory.
	 * @param key The key.
	 */
	public RedisMetadataStore(RedisConnectionFactory connectionFactory, String key) {
		Assert.notNull(connectionFactory, "'connectionFactory' must not be null.");
		Assert.hasText(key, "'key' must not be empty.");
		RedisOperations<String, String> redisTemplate = new StringRedisTemplate(connectionFactory);
		BoundHashOperations<String, String, String> hashOperations = redisTemplate.boundHashOps(key);
		this.properties = new RedisProperties(hashOperations);
	}

	/**
	 * Initializes the {@link RedisProperties} by provided {@link RedisConnectionFactory}
	 * and default hash key - {@link #KEY}.
	 *
	 * @param operations The Redis operations object.
	 */
	public RedisMetadataStore(RedisOperations<String, ?> operations) {
		this(operations, KEY);
	}

	/**
	 * Initializes the {@link RedisProperties} by provided {@link RedisConnectionFactory} and key.
	 *
	 * @param operations The Redis operations object.
	 * @param key The key.
	 */
	public RedisMetadataStore(RedisOperations<String, ?> operations, String key) {
		Assert.notNull(operations, "'operations' must not be null.");
		Assert.hasText(key, "'key' must not be empty.");
		BoundHashOperations<String, String, String> hashOperations = operations.boundHashOps(key);
		this.properties = new RedisProperties(hashOperations);
	}

	/**
	 * Persists the provided key and value to Redis.
	 *
	 * @param key Must not be null
	 * @param value Must not be null
	 */
	@Override
	public void put(String key, String value) {
		Assert.notNull(key, KEY_MUST_NOT_BE_NULL);
		Assert.notNull(value, "'value' must not be null.");
		this.properties.put(key, value);
	}

	/**
	 * Retrieve the persisted value for the provided key.
	 *
	 * @param key Must not be null
	 */
	@Override
	public String get(String key) {
		Assert.notNull(key, KEY_MUST_NOT_BE_NULL);
		Object value = this.properties.get(key);
		if (value != null) {
			Assert.isInstanceOf(String.class, value, "Invalid type in the store");
		}
		return (String) value;
	}

	@Override

	public String remove(String key) {
		Assert.notNull(key, KEY_MUST_NOT_BE_NULL);
		Object removed = this.properties.remove(key);
		if (removed != null) {
			Assert.isInstanceOf(String.class, removed, "The removed value was an invalid type");
		}
		return (String) removed;
	}

	@Override
	public String putIfAbsent(String key, String value) {
		Assert.notNull(key, KEY_MUST_NOT_BE_NULL);
		Assert.notNull(value, "'value' must not be null.");
		Object oldValue = this.properties.putIfAbsent(key, value);
		if (oldValue != null) {
			Assert.isInstanceOf(String.class, oldValue, "Invalid type in the store");
		}
		return (String) oldValue;
	}

	@Override
	public boolean replace(String key, String oldValue, String newValue) {
		Assert.notNull(key, KEY_MUST_NOT_BE_NULL);
		Assert.notNull(oldValue, "'oldValue' must not be null.");
		Assert.notNull(newValue, "'newValue' must not be null.");
		return this.properties.replace(key, oldValue, newValue);
	}

}
