/*
 * 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.ode.utils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This class is based on {@link org.apache.log4j.helpers.FileWatchdog}.<p/>
 * Modifications have been made to support additional abstract ressource and more events (creation, deletion and updates), and to allow "manual"
 * invocations of {@link #check()} (i.e wihtout having to use a thread) while preserving time checking.<p/>
 * Now two use cases coexist:
 * <ol>
 * <li>Pass an instance of {@link WatchDog} to a new thread ({@link WatchDog} is a {@link Runnable}).
 * So that {@link WatchDog# check ()} will be called automatically every {@code delay} milliseconds.</li>
 * <li>Invoke {@link WatchDog# check ()} only when you feel like it. If the expiration date previously set is lower than NOW then event
 * callback methods will be invoked accordingly.</li>
 * </ol>
 *
 * @author <a href="mailto:midon@intalio.com">Alexis Midon</a>
 */
public class WatchDog<T> implements Runnable {
    static final public long DEFAULT_DELAY = 30000;
    final Log log = LogFactory.getLog(getClass());

    private long expire;
    private T lastModif;
    private long delay;
    private boolean existedBefore, warnedAlready, interrupted;
    protected final Mutable<T> mutable;

    /**
     * @param mutable the object to watch closely
     * @param delay   between two checks
     */
    public WatchDog(Mutable<T> mutable, long delay) {
        this(mutable);
        this.delay = delay;
    }

    /**
     * @see #WatchDog(org.apache.ode.utils.WatchDog.Mutable, long)
     */
    public WatchDog(Mutable<T> mutable) {
        this.mutable = mutable;
        this.delay = DEFAULT_DELAY;
    }

    protected boolean isInitialized() {
        return true;
    }

    /**
     * Called by {@link #check()} if the object is not {@link #isInitialized initialized} and the {@link WatchDog.Mutable#exists()}  resource does not exist}.
     * <br/> This method might called to reset the object.
     *
     * @throws Exception
     */
    protected void init() {
    }

    /**
     * Called only if the resource previously existed and now does not exist.
     * <br/>The default implementation invokes {@link #init()} .
     *
     * @throws Exception
     */
    protected void doOnDelete() {
        init();
    }

    /**
     * Called only if the resource previously existed but the {@link WatchDog.Mutable#lastModified()} timestamp has changed (greater than the previous value).
     * <br/>The default implementation invokes {@link #init()} .
     *
     * @throws Exception
     */
    protected void doOnUpdate() {
        init();
    }

    public long getDelay() {
        return delay;
    }

    public void setDelay(long delay) {
        this.delay = delay;
    }

    public void run() {
        try {
            while (!interrupted) {
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException e) {
                    // no interruption expected
                }
                check();
            }
        } catch (Exception e) {
            log.warn("Exception occured. Thread will stop", e);
        }
    }

    public final void check() {
        long now = System.currentTimeMillis();
        if (expire <= now) {
            expire = now + delay;

            if (mutable.exists()) {
                existedBefore = true;
                if (lastModif==null || mutable.hasChangedSince(lastModif)) {
                    lastModif = mutable.lastModified();
                    if (log.isDebugEnabled())
                        log.debug(mutable + " has been modified");
                    doOnUpdate();
                    warnedAlready = false;
                }
            } else if (!isInitialized()) {
                // no resource and first time
                init();
            } else {
                if (existedBefore) {
                    existedBefore = false;
                    lastModif = null;
                    doOnDelete();
                }
                if (!warnedAlready) {
                    warnedAlready = true;
                    if (log.isDebugEnabled()) log.debug(mutable + "] does not exist.");
                }
            }
        }
    }

    /**
     * have you said that duck typing would be nice?
     */
    public interface Mutable<T> {
        boolean exists();

        boolean hasChangedSince(T since);

        T lastModified();
    }

}
