/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 com.datatorrent.stram.engine;

import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.jctools.queues.MessagePassingQueue;
import org.jctools.queues.SpscArrayQueue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.datatorrent.api.Sink;
import com.datatorrent.netlet.util.CircularBuffer;
import com.datatorrent.netlet.util.UnsafeBlockingQueue;
import com.datatorrent.stram.tuple.Tuple;

import static java.lang.Thread.sleep;

/**
 * Abstract Sweepable Reservoir implementation. Implements all methods of {@link SweepableReservoir} except
 * {@link SweepableReservoir#sweep}. Classes that extend {@link AbstractReservoir} must implement
 * {@link BlockingQueue} interface.
 *
 * @since 3.4.0
 */
public abstract class AbstractReservoir implements SweepableReservoir, BlockingQueue<Object>
{
  private static final Logger logger = LoggerFactory.getLogger(AbstractReservoir.class);
  static final String reservoirClassNameProperty = "com.datatorrent.stram.engine.Reservoir";
  private static final int SPSC_ARRAY_BLOCKING_QUEUE_CAPACITY_THRESHOLD = 64 * 1024;

  /**
   * Reservoir factory. Constructs concrete implementation of {@link AbstractReservoir} based on
   * {@link AbstractReservoir#reservoirClassNameProperty} property.
   * @param id reservoir identifier
   * @param capacity reservoir capacity
   * @return concrete implementation of {@link AbstractReservoir}
   */
  public static AbstractReservoir newReservoir(final String id, final int capacity)
  {
    String reservoirClassName = System.getProperty(reservoirClassNameProperty);
    if (reservoirClassName == null) {
      if (capacity >= SPSC_ARRAY_BLOCKING_QUEUE_CAPACITY_THRESHOLD) {
        return new SpscArrayQueueReservoir(id, capacity);
      } else {
        return new SpscArrayBlockingQueueReservoir(id, capacity);
      }
    } else if (reservoirClassName.equals(SpscArrayQueueReservoir.class.getName())) {
      return new SpscArrayQueueReservoir(id, capacity);
    } else if (reservoirClassName.equals(SpscArrayBlockingQueueReservoir.class.getName())) {
      return new SpscArrayBlockingQueueReservoir(id, capacity);
    } else if (reservoirClassName.equals(CircularBufferReservoir.class.getName())) {
      return new CircularBufferReservoir(id, capacity);
    } else if (reservoirClassName.equals(ArrayBlockingQueueReservoir.class.getName())) {
      return new ArrayBlockingQueueReservoir(id, capacity);
    } else {
      try {
        final Constructor<?> constructor = Class.forName(reservoirClassName).getConstructor(String.class, int.class);
        return (AbstractReservoir)constructor.newInstance(id, capacity);
      } catch (ReflectiveOperationException e) {
        logger.debug("Fail to construct reservoir {}", reservoirClassName, e);
        throw new RuntimeException("Fail to construct reservoir " + reservoirClassName, e);
      }
    }
  }

  private Sink<Object> sink;
  private String id;
  protected int count;

  protected AbstractReservoir(final String id)
  {
    this.id = id;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Sink<Object> setSink(Sink<Object> sink)
  {
    try {
      return this.sink;
    } finally {
      this.sink = sink;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int getCount(boolean reset)
  {
    try {
      return count;
    } finally {
      if (reset) {
        count = 0;
      }
    }
  }

  /**
   * @return allocated reservoir capacity
   */
  public abstract int capacity();

  /**
   * @return reservoir id
   */
  public String getId()
  {
    return id;
  }

  /**
   * @param id the id to set
   */
  public void setId(String id)
  {
    this.id = id;
  }

  protected Sink<Object> getSink()
  {
    return sink;
  }

  @Override
  public String toString()
  {
    return getClass().getName() + '@' + Integer.toHexString(hashCode()) +
      "{sink=" + sink + ", id=" + id + ", count=" + count + '}';
  }

  /**
   * <p>SpscArrayQueueReservoir</p>
   * {@link SweepableReservoir} implementation that extends AbstractReservoir and delegates {@link BlockingQueue}
   * implementation to {@see <a href=http://jctools.github.io/JCTools/>JCTools</a>} SpscArrayQueue.
   */
  private static class SpscArrayQueueReservoir extends AbstractReservoir
  {
    private final int maxSpinMillis = 10;
    private final SpscArrayQueue<Object> queue;

    private SpscArrayQueueReservoir(final String id, final int capacity)
    {
      super(id);
      queue = new SpscArrayQueue<>(capacity);
    }

    @Override
    public Tuple sweep()
    {
      Object o;
      final SpscArrayQueue<Object> queue = this.queue;
      final Sink<Object> sink = getSink();
      while ((o = queue.peek()) != null) {
        if (o instanceof Tuple) {
          return (Tuple)o;
        }
        count++;
        sink.put(queue.poll());
      }
      return null;
    }

    @Override
    public boolean add(Object o)
    {
      return queue.add(o);
    }

    @Override
    public Object remove()
    {
      return queue.remove();
    }

    @Override
    public Object peek()
    {
      return queue.peek();
    }

    @Override
    public int size(final boolean dataTupleAware)
    {
      return queue.size();
    }

    @Override
    public int capacity()
    {
      return queue.capacity();
    }

    @Override
    public int drainTo(final Collection<? super Object> container)
    {
      return queue.drain(new MessagePassingQueue.Consumer<Object>()
      {
        @Override
        public void accept(Object o)
        {
          container.add(o);
        }
      });
    }

    @Override
    public boolean offer(Object o)
    {
      return queue.offer(o);
    }

    @Override
    public void put(Object o) throws InterruptedException
    {
      long spinMillis = 0;
      final SpscArrayQueue<Object> queue = this.queue;
      while (!queue.offer(o)) {
        sleep(spinMillis);
        spinMillis = Math.min(maxSpinMillis, spinMillis + 1);
      }
    }

    @Override
    public boolean offer(Object o, long timeout, TimeUnit unit) throws InterruptedException
    {
      throw new UnsupportedOperationException();
    }

    @Override
    public Object take() throws InterruptedException
    {
      throw new UnsupportedOperationException();
    }

    @Override
    public Object poll(long timeout, TimeUnit unit) throws InterruptedException
    {
      throw new UnsupportedOperationException();
    }

    @Override
    public int remainingCapacity()
    {
      final SpscArrayQueue<Object> queue = this.queue;
      return queue.capacity() - queue.size();
    }

    @Override
    public boolean remove(Object o)
    {
      return queue.remove(o);
    }

    @Override
    public boolean contains(Object o)
    {
      return queue.contains(o);
    }

    @Override
    public int drainTo(final Collection<? super Object> collection, int maxElements)
    {
      return queue.drain(new MessagePassingQueue.Consumer<Object>()
      {
        @Override
        public void accept(Object o)
        {
          collection.add(o);
        }
      }, maxElements);
    }

    @Override
    public Object poll()
    {
      return queue.poll();
    }

    @Override
    public Object element()
    {
      return queue.element();
    }

    @Override
    public boolean isEmpty()
    {
      return queue.peek() == null;
    }

    @Override
    public Iterator<Object> iterator()
    {
      return queue.iterator();
    }

    @Override
    public Object[] toArray()
    {
      return queue.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a)
    {
      return queue.toArray(a);
    }

    @Override
    public boolean containsAll(Collection<?> c)
    {
      return queue.containsAll(c);
    }

    @Override
    public boolean addAll(Collection<?> c)
    {
      return queue.addAll(c);
    }

    @Override
    public boolean removeAll(Collection<?> c)
    {
      return queue.removeAll(c);
    }

    @Override
    public boolean retainAll(Collection<?> c)
    {
      return queue.retainAll(c);
    }

    @Override
    public int size()
    {
      return queue.size();
    }

    @Override
    public void clear()
    {
      queue.clear();
    }

    protected SpscArrayQueue<Object> getQueue()
    {
      return queue;
    }

  }

  /**
   * <p>SpscArrayBlockingQueueReservoir</p>
   * {@link SweepableReservoir} implementation that extends SpscArrayQueueReservoir and delegates {@link BlockingQueue}
   * implementation to {@see <a href=http://jctools.github.io/JCTools/>JCTools</a>} SpscArrayQueue.
   */
  private static class SpscArrayBlockingQueueReservoir extends SpscArrayQueueReservoir
  {
    private final ReentrantLock lock;
    private final Condition notFull;

    private SpscArrayBlockingQueueReservoir(final String id, final int capacity)
    {
      super(id, capacity);
      lock = new ReentrantLock();
      notFull = lock.newCondition();
    }

    @Override
    public Tuple sweep()
    {
      Object o;
      final ReentrantLock lock = this.lock;
      final SpscArrayQueue<Object> queue = getQueue();
      final Sink<Object> sink = getSink();
      lock.lock();
      try {
        while ((o = queue.peek()) != null) {
          if (o instanceof Tuple) {
            return (Tuple)o;
          }
          count++;
          sink.put(queue.poll());
          notFull.signal();
          if (lock.hasQueuedThreads()) {
            return null;
          }
        }
        return null;
      } finally {
        lock.unlock();
      }
    }

    @Override
    public void put(Object o) throws InterruptedException
    {
      final SpscArrayQueue<Object> queue = getQueue();
      if (!queue.offer(o)) {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
          while (!queue.offer(o)) {
            notFull.await();
          }
        } finally {
          lock.unlock();
        }
      }
    }

    @Override
    public Object remove()
    {
      final SpscArrayQueue<Object> queue = getQueue();
      final ReentrantLock lock = this.lock;
      lock.lock();
      try {
        Object o = queue.remove();
        if (o != null) {
          notFull.signal();
        }
        return o;
      } finally {
        lock.unlock();
      }
    }


  }

  /**
   * <p>ArrayBlockingQueueReservoir</p>
   * {@link SweepableReservoir} implementation that extends AbstractReservoir and delegates {@link BlockingQueue}
   * implementation to {@link ArrayBlockingQueue}.
   */
  private static class ArrayBlockingQueueReservoir extends AbstractReservoir
  {
    private final ArrayBlockingQueue<Object> queue;

    private ArrayBlockingQueueReservoir(final String id, final int capacity)
    {
      super(id);
      queue = new ArrayBlockingQueue<>(capacity);
    }

    @Override
    public Tuple sweep()
    {
      Object o;
      final ArrayBlockingQueue<Object> queue = this.queue;
      final Sink<Object> sink = getSink();
      while ((o = queue.peek()) != null) {
        if (o instanceof Tuple) {
          return (Tuple)o;
        }
        count++;
        sink.put(queue.poll());
      }
      return null;
    }

    @Override
    public boolean add(Object o)
    {
      return queue.add(o);
    }

    @Override
    public boolean offer(Object o)
    {
      return queue.offer(o);
    }

    @Override
    public void put(Object o) throws InterruptedException
    {
      queue.put(o);
    }

    @Override
    public boolean offer(Object o, long timeout, TimeUnit unit) throws InterruptedException
    {
      return queue.offer(o, timeout, unit);
    }

    @Override
    public Object poll()
    {
      return queue.poll();
    }

    @Override
    public Object take() throws InterruptedException
    {
      return queue.take();
    }

    @Override
    public Object poll(long timeout, TimeUnit unit) throws InterruptedException
    {
      return queue.poll(timeout, unit);
    }

    @Override
    public Object peek()
    {
      return queue.peek();
    }

    @Override
    public int size()
    {
      return queue.size();
    }

    @Override
    public int size(final boolean dataTupleAware)
    {
      return queue.size();
    }

    @Override
    public int capacity()
    {
      throw new UnsupportedOperationException();
    }

    @Override
    public int remainingCapacity()
    {
      return queue.remainingCapacity();
    }

    @Override
    public boolean remove(Object o)
    {
      return queue.remove(o);
    }

    @Override
    public boolean contains(Object o)
    {
      return queue.contains(o);
    }

    @Override
    public Object[] toArray()
    {
      return queue.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a)
    {
      return queue.toArray(a);
    }

    @Override
    public String toString()
    {
      return queue.toString();
    }

    @Override
    public void clear()
    {
      queue.clear();
    }

    @Override
    public int drainTo(Collection<? super Object> c)
    {
      return queue.drainTo(c);
    }

    @Override
    public int drainTo(Collection<? super Object> c, int maxElements)
    {
      return queue.drainTo(c, maxElements);
    }

    @Override
    public Iterator<Object> iterator()
    {
      return queue.iterator();
    }

    @Override
    public Object remove()
    {
      return queue.remove();
    }

    @Override
    public Object element()
    {
      return queue.element();
    }

    @Override
    public boolean addAll(Collection<?> c)
    {
      return queue.addAll(c);
    }

    @Override
    public boolean isEmpty()
    {
      return queue.isEmpty();
    }

    @Override
    public boolean containsAll(Collection<?> c)
    {
      return queue.containsAll(c);
    }

    @Override
    public boolean removeAll(Collection<?> c)
    {
      return queue.removeAll(c);
    }

    @Override
    public boolean retainAll(Collection<?> c)
    {
      return queue.retainAll(c);
    }
  }

  /**
   * <p>CircularBufferReservoir</p>
   * {@link SweepableReservoir} implementation that extends AbstractReservoir and delegates {@link BlockingQueue}
   * implementation to {@code CircularBuffer}. Replaces DefaultReservoir class since release 3.3}.
   *
   * @since 0.3.2
   */
  private static class CircularBufferReservoir extends AbstractReservoir implements UnsafeBlockingQueue<Object>
  {
    private final CircularBuffer<Object> circularBuffer;

    private CircularBufferReservoir(String id, int capacity)
    {
      super(id);
      circularBuffer = new CircularBuffer<>(capacity);
    }

    @Override
    public Tuple sweep()
    {
      final CircularBuffer<Object> circularBuffer = this.circularBuffer;
      final Sink<Object> sink = getSink();
      final int size = circularBuffer.size();
      for (int i = 0; i < size; i++) {
        if (circularBuffer.peekUnsafe() instanceof Tuple) {
          count += i;
          return (Tuple)peekUnsafe();
        }
        sink.put(pollUnsafe());
      }

      count += size;
      return null;
    }

    @Override
    public boolean add(Object o)
    {
      return circularBuffer.add(o);
    }

    @Override
    public Object remove()
    {
      return circularBuffer.remove();
    }

    @Override
    public Object peek()
    {
      return circularBuffer.peek();
    }

    @Override
    public int size(final boolean dataTupleAware)
    {
      int size = circularBuffer.size();
      if (dataTupleAware) {
        Iterator<Object> iterator = circularBuffer.getFrozenIterator();
        while (iterator.hasNext()) {
          if (iterator.next() instanceof Tuple) {
            size--;
          }
        }
      }
      return size;
    }

    @Override
    public int capacity()
    {
      return circularBuffer.capacity();
    }

    @Override
    public int drainTo(Collection<? super Object> container)
    {
      return circularBuffer.drainTo(container);
    }

    @Override
    public boolean offer(Object o)
    {
      return circularBuffer.offer(o);
    }

    @Override
    public void put(Object o) throws InterruptedException
    {
      circularBuffer.put(o);
    }

    @Override
    public boolean offer(Object o, long timeout, TimeUnit unit) throws InterruptedException
    {
      return circularBuffer.offer(o, timeout, unit);
    }

    @Override
    public Object take() throws InterruptedException
    {
      return circularBuffer.take();
    }

    @Override
    public Object poll(long timeout, TimeUnit unit) throws InterruptedException
    {
      return circularBuffer.poll(timeout, unit);
    }

    @Override
    public int remainingCapacity()
    {
      return circularBuffer.remainingCapacity();
    }

    @Override
    public boolean remove(Object o)
    {
      return circularBuffer.remove(o);
    }

    @Override
    public boolean contains(Object o)
    {
      return circularBuffer.contains(o);
    }

    @Override
    public int drainTo(Collection<? super Object> collection, int maxElements)
    {
      return circularBuffer.drainTo(collection, maxElements);
    }

    @Override
    public Object poll()
    {
      return circularBuffer.poll();
    }

    @Override
    public Object pollUnsafe()
    {
      return circularBuffer.pollUnsafe();
    }

    @Override
    public Object element()
    {
      return circularBuffer.element();
    }

    @Override
    public boolean isEmpty()
    {
      return circularBuffer.isEmpty();
    }

    public Iterator<Object> getFrozenIterator()
    {
      return circularBuffer.getFrozenIterator();
    }

    public Iterable<Object> getFrozenIterable()
    {
      return circularBuffer.getFrozenIterable();
    }

    @Override
    public Iterator<Object> iterator()
    {
      return circularBuffer.iterator();
    }

    @Override
    public Object[] toArray()
    {
      return circularBuffer.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a)
    {
      return circularBuffer.toArray(a);
    }

    @Override
    public boolean containsAll(Collection<?> c)
    {
      return circularBuffer.containsAll(c);
    }

    @Override
    public boolean addAll(Collection<?> c)
    {
      return circularBuffer.addAll(c);
    }

    @Override
    public boolean removeAll(Collection<?> c)
    {
      return circularBuffer.removeAll(c);
    }

    @Override
    public boolean retainAll(Collection<?> c)
    {
      return circularBuffer.retainAll(c);
    }

    @Override
    public int size()
    {
      return circularBuffer.size();
    }

    @Override
    public void clear()
    {
      circularBuffer.clear();
    }

    @Override
    public Object peekUnsafe()
    {
      return circularBuffer.peekUnsafe();
    }

    public CircularBuffer<Object> getWhitehole(String exceptionMessage)
    {
      return circularBuffer.getWhitehole(exceptionMessage);
    }
  }

}