/**
 * The MIT License
 * Copyright (c) 2014-2016 Ilkka Seppälä
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.iluwatar.reader.writer.lock;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

/**
 * Class responsible for control the access for reader or writer
 * 
 * Allows multiple readers to hold the lock at same time, but if any writer holds the lock then
 * readers wait. If reader holds the lock then writer waits. This lock is not fair.
 */
public class ReaderWriterLock implements ReadWriteLock {


  private Object readerMutex = new Object();

  private int currentReaderCount;

  /**
   * Global mutex is used to indicate that whether reader or writer gets the lock in the moment.
   * <p>
   * 1. When it contains the reference of {@link readerLock}, it means that the lock is acquired by
   * the reader, another reader can also do the read operation concurrently. <br>
   * 2. When it contains the reference of reference of {@link writerLock}, it means that the lock is
   * acquired by the writer exclusively, no more reader or writer can get the lock.
   * <p>
   * This is the most important field in this class to control the access for reader/writer.
   */
  private Set<Object> globalMutex = new HashSet<>();

  private ReadLock readerLock = new ReadLock();
  private WriteLock writerLock = new WriteLock();

  @Override
  public Lock readLock() {
    return readerLock;
  }

  @Override
  public Lock writeLock() {
    return writerLock;
  }

  /**
   * return true when globalMutex hold the reference of writerLock
   */
  private boolean doesWriterOwnThisLock() {
    return globalMutex.contains(writerLock);
  }

  /**
   * return true when globalMutex hold the reference of readerLock
   */
  private boolean doesReaderOwnThisLock() {
    return globalMutex.contains(readerLock);
  }

  /**
   * Nobody get the lock when globalMutex contains nothing
   * 
   */
  private boolean isLockFree() {
    return globalMutex.isEmpty();
  }

  private static void waitUninterruptibly(Object o) {
    try {
      o.wait();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * Reader Lock, can be access for more than one reader concurrently if no writer get the lock
   */
  private class ReadLock implements Lock {

    @Override
    public void lock() {

      synchronized (readerMutex) {

        currentReaderCount++;
        if (currentReaderCount == 1) {
          // Try to get the globalMutex lock for the first reader
          synchronized (globalMutex) {
            while (true) {
              // If the no one get the lock or the lock is locked by reader, just set the reference
              // to the globalMutex to indicate that the lock is locked by Reader.
              if (isLockFree() || doesReaderOwnThisLock()) {
                globalMutex.add(this);
                break;
              } else {
                // If lock is acquired by the write, let the thread wait until the writer release
                // the lock
                waitUninterruptibly(globalMutex);
              }
            }
          }

        }
      }
    }

    @Override
    public void unlock() {

      synchronized (readerMutex) {
        currentReaderCount--;
        // Release the lock only when it is the last reader, it is ensure that the lock is released
        // when all reader is completely.
        if (currentReaderCount == 0) {
          synchronized (globalMutex) {
            // Notify the waiter, mostly the writer
            globalMutex.remove(this);
            globalMutex.notifyAll();
          }
        }
      }

    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock() {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
      throw new UnsupportedOperationException();
    }

    @Override
    public Condition newCondition() {
      throw new UnsupportedOperationException();
    }

  }

  /**
   * Writer Lock, can only be accessed by one writer concurrently
   */
  private class WriteLock implements Lock {

    @Override
    public void lock() {

      synchronized (globalMutex) {

        while (true) {
          // When there is no one acquired the lock, just put the writeLock reference to the
          // globalMutex to indicate that the lock is acquired by one writer.
          // It is ensure that writer can only get the lock when no reader/writer acquired the lock.
          if (isLockFree()) {
            globalMutex.add(this);
            break;
          } else if (doesWriterOwnThisLock()) {
            // Wait when other writer get the lock
            waitUninterruptibly(globalMutex);
          } else if (doesReaderOwnThisLock()) {
            // Wait when other reader get the lock
            waitUninterruptibly(globalMutex);
          } else {
            throw new AssertionError("it should never reach here");
          }
        }
      }
    }

    @Override
    public void unlock() {

      synchronized (globalMutex) {
        globalMutex.remove(this);
        // Notify the waiter, other writer or reader
        globalMutex.notifyAll();
      }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock() {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
      throw new UnsupportedOperationException();
    }

    @Override
    public Condition newCondition() {
      throw new UnsupportedOperationException();
    }
  }

}
