/*
 * Copyright 2011-2023 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.redis.support.collections;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisListCommands;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Default implementation for {@link RedisList}. Suitable for not just lists, but also queues (FIFO ordering) or stacks
 * (LIFO ordering) and deques (or double ended queues). Allows the maximum size (or the cap) to be specified to prevent
 * the list from over growing. Note that all write operations will execute immediately, whether a cap is specified or
 * not - the list will always accept new items (trimming the tail after each insert in case of capped collections).
 *
 * @author Costin Leau
 * @author Christoph Strobl
 * @author Mark Paluch
 */
public class DefaultRedisList<E> extends AbstractRedisCollection<E> implements RedisList<E> {

	private final BoundListOperations<String, E> listOps;

	private volatile int maxSize = 0;
	private volatile boolean capped = false;

	private class DefaultRedisListIterator extends RedisIterator<E> {

		public DefaultRedisListIterator(Iterator<E> delegate) {
			super(delegate);
		}

		@Override
		protected void removeFromRedisStorage(E item) {
			DefaultRedisList.this.remove(item);
		}
	}

	/**
	 * Constructs a new, uncapped {@link DefaultRedisList} instance.
	 *
	 * @param key Redis key of this list.
	 * @param operations {@link RedisOperations} for the value type of this list.
	 */
	public DefaultRedisList(String key, RedisOperations<String, E> operations) {
		this(operations.boundListOps(key));
	}

	/**
	 * Constructs a new {@link DefaultRedisList} instance.
	 *
	 * @param key Redis key of this list.
	 * @param operations {@link RedisOperations} for the value type of this list.
	 * @param maxSize
	 * @since 2.6
	 */
	public DefaultRedisList(String key, RedisOperations<String, E> operations, int maxSize) {
		this(operations.boundListOps(key), maxSize);
	}

	/**
	 * Constructs a new, uncapped {@link DefaultRedisList} instance.
	 *
	 * @param boundOps {@link BoundListOperations} for the value type of this list.
	 */
	public DefaultRedisList(BoundListOperations<String, E> boundOps) {
		this(boundOps, 0);
	}

	/**
	 * Constructs a new {@link DefaultRedisList} instance.
	 *
	 * @param boundOps {@link BoundListOperations} for the value type of this list.
	 * @param maxSize
	 */
	public DefaultRedisList(BoundListOperations<String, E> boundOps, int maxSize) {

		super(boundOps.getKey(), boundOps.getOperations());

		listOps = boundOps;
		setMaxSize(maxSize);
	}

	/**
	 * Sets the maximum size of the (capped) list. A value of 0 means unlimited.
	 *
	 * @param maxSize list maximum size
	 */
	public void setMaxSize(int maxSize) {

		this.maxSize = maxSize;
		capped = (maxSize > 0);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.support.collections.RedisList#moveFirstTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction)
	 */
	@Override
	public E moveFirstTo(RedisList<E> destination, RedisListCommands.Direction destinationPosition) {

		Assert.notNull(destination, "Destination must not be null");
		Assert.notNull(destinationPosition, "Destination position must not be null");

		E result = listOps.move(RedisListCommands.Direction.first(), destination.getKey(), destinationPosition);
		potentiallyCap(destination);
		return result;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.support.collections.RedisList#moveFirstTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction, long, java.util.concurrent.TimeUnit)
	 */
	@Override
	public E moveFirstTo(RedisList<E> destination, RedisListCommands.Direction destinationPosition, long timeout,
			TimeUnit unit) {

		Assert.notNull(destination, "Destination must not be null");
		Assert.notNull(destinationPosition, "Destination position must not be null");
		Assert.notNull(unit, "TimeUnit must not be null");

		E result = listOps.move(RedisListCommands.Direction.first(), destination.getKey(), destinationPosition, timeout,
				unit);
		potentiallyCap(destination);
		return result;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.support.collections.RedisList#moveLastTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction)
	 */
	@Override
	public E moveLastTo(RedisList<E> destination, RedisListCommands.Direction destinationPosition) {

		Assert.notNull(destination, "Destination must not be null");
		Assert.notNull(destinationPosition, "Destination position must not be null");

		E result = listOps.move(RedisListCommands.Direction.last(), destination.getKey(), destinationPosition);
		potentiallyCap(destination);
		return result;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.support.collections.RedisList#moveLastTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction, long, java.util.concurrent.TimeUnit)
	 */
	@Override
	public E moveLastTo(RedisList<E> destination, RedisListCommands.Direction destinationPosition, long timeout,
			TimeUnit unit) {

		Assert.notNull(destination, "Destination must not be null");
		Assert.notNull(destinationPosition, "Destination position must not be null");
		Assert.notNull(unit, "TimeUnit must not be null");

		E result = listOps.move(RedisListCommands.Direction.last(), destination.getKey(), destinationPosition, timeout,
				unit);
		potentiallyCap(destination);
		return result;
	}

	private void potentiallyCap(RedisList<E> destination) {
		if (destination instanceof DefaultRedisList) {
			((DefaultRedisList<Object>) destination).cap();
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.support.collections.RedisList#range(long, long)
	 */
	@Override
	public List<E> range(long start, long end) {
		return listOps.range(start, end);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.support.collections.RedisList#trim(int, int)
	 */
	@Override
	public RedisList<E> trim(int start, int end) {
		listOps.trim(start, end);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.support.collections.RedisList#trim(long, long)
	 */
	@Override
	public RedisList<E> trim(long start, long end) {
		listOps.trim(start, end);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.AbstractCollection#iterator()
	 */
	@Override
	public Iterator<E> iterator() {
		List<E> list = content();
		checkResult(list);
		return new DefaultRedisListIterator(list.iterator());
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.AbstractCollection#size()
	 */
	@Override
	public int size() {
		Long size = listOps.size();
		checkResult(size);
		return size.intValue();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.AbstractCollection#add(java.lang.Object)
	 */
	@Override
	public boolean add(E value) {
		listOps.rightPush(value);
		cap();
		return true;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.AbstractCollection#clear()
	 */
	@Override
	public void clear() {
		listOps.trim(size() + 1, 0);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.AbstractCollection#remove(java.lang.Object)
	 */
	@Override
	public boolean remove(Object o) {
		Long result = listOps.remove(1, o);
		return (result != null && result.longValue() > 0);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#add(int, java.lang.Object)
	 */
	@Override
	public void add(int index, E element) {
		if (index == 0) {
			listOps.leftPush(element);
			cap();
			return;
		}

		int size = size();

		if (index == size()) {
			listOps.rightPush(element);
			cap();
			return;
		}

		if (index < 0 || index > size) {
			throw new IndexOutOfBoundsException();
		}

		throw new IllegalArgumentException("Redis supports insertion only at the beginning or the end of the list");
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#addAll(int, java.util.Collection)
	 */
	@Override
	public boolean addAll(int index, Collection<? extends E> c) {
		// insert collection in reverse
		if (index == 0) {
			Collection<? extends E> reverseC = CollectionUtils.reverse(c);

			for (E e : reverseC) {
				listOps.leftPush(e);
				cap();
			}
			return true;
		}

		int size = size();

		if (index == size()) {
			for (E e : c) {
				listOps.rightPush(e);
				cap();
			}
			return true;
		}

		if (index < 0 || index > size) {
			throw new IndexOutOfBoundsException();
		}

		throw new IllegalArgumentException("Redis supports insertion only at the beginning or the end of the list");
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#get(int)
	 */
	@Override
	public E get(int index) {
		if (index < 0 || index > size()) {
			throw new IndexOutOfBoundsException();
		}
		return listOps.index(index);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#indexOf(java.lang.Object)
	 */
	@Override
	public int indexOf(Object o) {

		Long index = listOps.indexOf((E) o);
		return index != null ? index.intValue() : -1;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#lastIndexOf(java.lang.Object)
	 */
	@Override
	public int lastIndexOf(Object o) {

		Long index = listOps.lastIndexOf((E) o);
		return index != null ? index.intValue() : -1;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#listIterator()
	 */
	@Override
	public ListIterator<E> listIterator() {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#listIterator(int)
	 */
	@Override
	public ListIterator<E> listIterator(int index) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#remove(int)
	 */
	@Override
	public E remove(int index) {
		throw new UnsupportedOperationException();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#set(int, java.lang.Object)
	 */
	@Override
	public E set(int index, E e) {

		E object = get(index);
		listOps.set(index, e);
		return object;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.List#subList(int, int)
	 */
	@Override
	public List<E> subList(int fromIndex, int toIndex) {
		throw new UnsupportedOperationException();
	}

	//
	// Queue methods
	//

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#element()
	 */
	@Override
	public E element() {
		E value = peek();
		if (value == null) {
			throw new NoSuchElementException();
		}

		return value;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#offer(java.lang.Object)
	 */
	@Override
	public boolean offer(E e) {
		listOps.rightPush(e);
		cap();
		return true;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#peek()
	 */
	@Override
	@Nullable
	public E peek() {
		return listOps.index(0);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#poll()
	 */
	@Override
	@Nullable
	public E poll() {
		return listOps.leftPop();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#remove()
	 */
	@Override
	public E remove() {

		E value = poll();
		if (value == null) {
			throw new NoSuchElementException();
		}

		return value;
	}

	//
	// Dequeue
	//

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#addFirst(java.lang.Object)
	 */
	@Override
	public void addFirst(E e) {
		listOps.leftPush(e);
		cap();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#addLast(java.lang.Object)
	 */
	@Override
	public void addLast(E e) {
		add(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#descendingIterator()
	 */
	@Override
	public Iterator<E> descendingIterator() {
		List<E> content = content();
		Collections.reverse(content);
		return new DefaultRedisListIterator(content.iterator());
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#getFirst()
	 */
	@Override
	public E getFirst() {
		return element();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#getLast()
	 */
	@Override
	public E getLast() {
		E e = peekLast();
		if (e == null) {
			throw new NoSuchElementException();
		}
		return e;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#offerFirst(java.lang.Object)
	 */
	@Override
	public boolean offerFirst(E e) {
		addFirst(e);
		return true;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#offerLast(java.lang.Object)
	 */
	@Override
	public boolean offerLast(E e) {
		addLast(e);
		return true;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#peekFirst()
	 */
	@Override
	@Nullable
	public E peekFirst() {
		return peek();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#peekLast()
	 */
	@Override
	@Nullable
	public E peekLast() {
		return listOps.index(-1);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#pollFirst()
	 */
	@Override
	@Nullable
	public E pollFirst() {
		return poll();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#pollLast()
	 */
	@Override
	@Nullable
	public E pollLast() {
		return listOps.rightPop();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#pop()
	 */
	@Override
	public E pop() {

		E e = poll();
		if (e == null) {
			throw new NoSuchElementException();
		}
		return e;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#push(java.lang.Object)
	 */
	@Override
	public void push(E e) {
		addFirst(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#removeFirst()
	 */
	@Override
	public E removeFirst() {
		return pop();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#removeFirstOccurrence(java.lang.Object)
	 */
	@Override
	public boolean removeFirstOccurrence(Object o) {
		return remove(o);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.Deque#removeLast()
	 */
	@Override
	public E removeLast() {
		E e = pollLast();
		if (e == null) {
			throw new NoSuchElementException();
		}
		return e;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#removeLastOccurrence(java.lang.Object)
	 */
	@Override
	public boolean removeLastOccurrence(Object o) {
		Long result = listOps.remove(-1, o);
		return (result != null && result.longValue() > 0);
	}

	//
	// BlockingQueue
	//
	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingQueue#drainTo(java.util.Collection, int)
	 */
	@Override
	public int drainTo(Collection<? super E> c, int maxElements) {
		if (this.equals(c)) {
			throw new IllegalArgumentException("Cannot drain a queue to itself");
		}

		int size = size();
		int loop = (size >= maxElements ? maxElements : size);

		for (int index = 0; index < loop; index++) {
			c.add(poll());
		}

		return loop;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingQueue#drainTo(java.util.Collection)
	 */
	@Override
	public int drainTo(Collection<? super E> c) {
		return drainTo(c, size());
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#offer(java.lang.Object, long, java.util.concurrent.TimeUnit)
	 */
	@Override
	public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
		return offer(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#poll(long, java.util.concurrent.TimeUnit)
	 */
	@Override
	@Nullable
	public E poll(long timeout, TimeUnit unit) throws InterruptedException {
		return listOps.leftPop(timeout, unit);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#put(java.lang.Object)
	 */
	@Override
	public void put(E e) throws InterruptedException {
		offer(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingQueue#remainingCapacity()
	 */
	@Override
	public int remainingCapacity() {
		return Integer.MAX_VALUE;
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#take()
	 */
	@Override
	@Nullable
	public E take() throws InterruptedException {
		return poll(0, TimeUnit.SECONDS);
	}

	//
	// BlockingDeque
	//

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#offerFirst(java.lang.Object, long, java.util.concurrent.TimeUnit)
	 */
	@Override
	public boolean offerFirst(E e, long timeout, TimeUnit unit) throws InterruptedException {
		return offerFirst(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#offerLast(java.lang.Object, long, java.util.concurrent.TimeUnit)
	 */
	@Override
	public boolean offerLast(E e, long timeout, TimeUnit unit) throws InterruptedException {
		return offerLast(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#pollFirst(long, java.util.concurrent.TimeUnit)
	 */
	@Override
	@Nullable
	public E pollFirst(long timeout, TimeUnit unit) throws InterruptedException {
		return poll(timeout, unit);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#pollLast(long, java.util.concurrent.TimeUnit)
	 */
	@Override
	@Nullable
	public E pollLast(long timeout, TimeUnit unit) throws InterruptedException {
		return listOps.rightPop(timeout, unit);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#putFirst(java.lang.Object)
	 */
	@Override
	public void putFirst(E e) throws InterruptedException {
		add(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#putLast(java.lang.Object)
	 */
	@Override
	public void putLast(E e) throws InterruptedException {
		put(e);
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#takeFirst()
	 */
	@Override
	@Nullable
	public E takeFirst() throws InterruptedException {
		return take();
	}

	/*
	 * (non-Javadoc)
	 * @see java.util.concurrent.BlockingDeque#takeLast()
	 */
	@Override
	@Nullable
	public E takeLast() throws InterruptedException {
		return pollLast(0, TimeUnit.SECONDS);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.core.BoundKeyOperations#getType()
	 */
	@Override
	public DataType getType() {
		return DataType.LIST;
	}

	private List<E> content() {
		return listOps.range(0, -1);
	}

	private void cap() {
		if (capped) {
			listOps.trim(0, maxSize - 1);
		}
	}
}
