/*
 * 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 org.apache.nifi.util.file.monitor;

import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Allows the user to configure a {@link java.nio.file.Path Path} to watch for modifications and periodically poll to check if the file has been modified
 */
public class SynchronousFileWatcher {

    private final Path path;
    private final long checkUpdateMillis;
    private final UpdateMonitor monitor;
    private final AtomicReference<StateWrapper> lastState;
    private final Lock resourceLock = new ReentrantLock();

    public SynchronousFileWatcher(final Path path, final UpdateMonitor monitor) {
        this(path, monitor, 0L);
    }

    public SynchronousFileWatcher(final Path path, final UpdateMonitor monitor, final long checkMillis) {
        if (checkMillis < 0) {
            throw new IllegalArgumentException();
        }

        this.path = path;
        checkUpdateMillis = checkMillis;
        this.monitor = monitor;

        Object currentState;
        try {
            currentState = monitor.getCurrentState(path);
        } catch (final IOException e) {
            currentState = null;
        }

        this.lastState = new AtomicReference<>(new StateWrapper(currentState));
    }

    /**
     * Checks if the file has been updated according to the configured {@link UpdateMonitor} and resets the state
     *
     * @return true if updated; false otherwise
     * @throws IOException if failure occurs checking for changes
     */
    public boolean checkAndReset() throws IOException {
        if (checkUpdateMillis <= 0) { // if checkUpdateMillis <= 0, always check
            return checkForUpdate();
        } else {
            final StateWrapper stateWrapper = lastState.get();
            if (stateWrapper.getTimestamp() < System.currentTimeMillis() - checkUpdateMillis) {
                return checkForUpdate();
            }
            return false;
        }
    }

    private boolean checkForUpdate() throws IOException {
        if (resourceLock.tryLock()) {
            try {
                final StateWrapper wrapper = lastState.get();
                final Object newState = monitor.getCurrentState(path);
                if (newState == null && wrapper.getState() == null) {
                    return false;
                }
                if (newState == null || wrapper.getState() == null) {
                    lastState.set(new StateWrapper(newState));
                    return true;
                }

                final boolean unmodified = newState.equals(wrapper.getState());
                if (!unmodified) {
                    lastState.set(new StateWrapper(newState));
                }
                return !unmodified;
            } finally {
                resourceLock.unlock();
            }
        } else {
            return false;
        }
    }

    private static class StateWrapper {

        private final Object state;
        private final long timestamp;

        public StateWrapper(final Object state) {
            this.state = state;
            this.timestamp = System.currentTimeMillis();
        }

        public Object getState() {
            return state;
        }

        public long getTimestamp() {
            return timestamp;
        }
    }
}
