package com.pastdev.jsch.nio.file;


import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchEvent.Modifier;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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


public class UnixSshFileSystemWatchService implements WatchService {
    private static Logger logger = LoggerFactory.getLogger( UnixSshFileSystemWatchService.class );
    private static final long DEFAULT_POLLING_INTERVAL = 10;
    private static final TimeUnit DEFAULT_POLLING_INTERVAL_TIME_UNIT = TimeUnit.MINUTES;

    private long pollingInterval;
    private TimeUnit pollingIntervalTimeUnit;
    private volatile boolean closed;
    private final ExecutorService executorService;
    private final LinkedBlockingDeque<WatchKey> pendingKeys;
    private boolean useINotifyWatchKey = false;
    private final Map<UnixSshPath, Future<?>> watchKeyFutures;
    private final Map<UnixSshPath, UnixSshPathWatchKey> watchKeys;
    private final Lock watchKeysLock;

    private UnixSshFileSystemWatchService() {
        logger.debug( "creating new watch service polling every {} {}", pollingInterval, pollingIntervalTimeUnit );
        this.pendingKeys = new LinkedBlockingDeque<>();
        this.executorService = Executors.newCachedThreadPool();
        this.watchKeys = new HashMap<>();
        this.watchKeyFutures = new HashMap<>();
        this.watchKeysLock = new ReentrantLock();
    }

    @Override
    public void close() throws IOException {
        if ( closed ) return;
        closed = true;
    }

    boolean closed() {
        return closed;
    }

    void enqueue( WatchKey watchKey ) {
        ensureOpen();
        pendingKeys.add( watchKey );
    }

    void ensureOpen() {
        if ( closed ) throw new ClosedWatchServiceException();
    }

    public static WatchService inotifyWatchService() {
        UnixSshFileSystemWatchService service = new UnixSshFileSystemWatchService();
        service.useINotifyWatchKey = true;
        return service;
    }
    
    private UnixSshPathWatchKey newWatchKey( UnixSshPath path, Kind<?>[] events ) {
        return useINotifyWatchKey 
                ? new UnixSshPathINotifyWatchKey( this, path, events )
                : new UnixSshPathWatchKey( this, path, events, pollingInterval, pollingIntervalTimeUnit );
    }

    @Override
    public WatchKey poll() {
        ensureOpen();
        return pendingKeys.poll();
    }

    @Override
    public WatchKey poll( long timeout, TimeUnit unit ) throws InterruptedException {
        ensureOpen();
        return pendingKeys.poll( timeout, unit );
    }
    
    public static UnixSshFileSystemWatchService pollingWatchService( Long pollingInterval, TimeUnit pollingIntervalTimeUnit ) {
        UnixSshFileSystemWatchService service = new UnixSshFileSystemWatchService();
        service.pollingInterval = pollingInterval == null ? DEFAULT_POLLING_INTERVAL : pollingInterval;
        service.pollingIntervalTimeUnit = pollingIntervalTimeUnit == null
                ? DEFAULT_POLLING_INTERVAL_TIME_UNIT : pollingIntervalTimeUnit;
        return service;
    }

    UnixSshPathWatchKey register( UnixSshPath path, Kind<?>[] events, Modifier... modifiers ) {
        try {
            watchKeysLock.lock();
            if ( watchKeys.containsKey( path ) ) {
                return watchKeys.get( path );
            }

            UnixSshPathWatchKey watchKey = newWatchKey( path, events );
            watchKeys.put( path, watchKey );
            watchKeyFutures.put( path, executorService.submit( watchKey ) );
            return watchKey;
        }
        finally {
            watchKeysLock.unlock();
        }
    }

    void unregister( UnixSshPathWatchKey watchKey ) {
        try {
            watchKeysLock.lock();
            UnixSshPath path = watchKey.watchable();
            if ( !watchKeys.containsKey( path ) ) {
                return;
            }

            watchKeyFutures.remove( path ).cancel( true );
            watchKeys.remove( path );
        }
        finally {
            watchKeysLock.unlock();
        }
    }

    @Override
    public WatchKey take() throws InterruptedException {
        ensureOpen();
        return pendingKeys.take();
    }
}