/*
 * 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.catalina.ha.deploy;

import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.res.StringManager;

/**
 * The <b>WarWatcher </b> watches the deployDir for changes made to the directory (adding new WAR files-&gt;deploy or
 * remove WAR files-&gt;undeploy) and notifies a listener of the changes made.
 */
public class WarWatcher {

    /*--Static Variables----------------------------------------*/
    private static final Log log = LogFactory.getLog(WarWatcher.class);
    private static final StringManager sm = StringManager.getManager(WarWatcher.class);

    /*--Instance Variables--------------------------------------*/
    /**
     * Directory to watch for war files
     */
    protected final File watchDir;

    /**
     * Parent to be notified of changes
     */
    protected final FileChangeListener listener;

    /**
     * Currently deployed files
     */
    protected final Map<String,WarInfo> currentStatus = new HashMap<>();

    /*--Constructor---------------------------------------------*/

    public WarWatcher(FileChangeListener listener, File watchDir) {
        this.listener = listener;
        this.watchDir = watchDir;
    }

    /*--Logic---------------------------------------------------*/

    /**
     * check for modification and send notification to listener
     */
    public void check() {
        if (log.isDebugEnabled()) {
            log.debug(sm.getString("warWatcher.checkingWars", watchDir));
        }
        File[] list = watchDir.listFiles(new WarFilter());
        if (list == null) {
            log.warn(sm.getString("warWatcher.cantListWatchDir", watchDir));

            list = new File[0];
        }
        // first make sure all the files are listed in our current status
        for (File file : list) {
            if (!file.exists()) {
                log.warn(sm.getString("warWatcher.listedFileDoesNotExist", file, watchDir));
            }

            addWarInfo(file);
        }

        // Check all the status codes and update the FarmDeployer
        for (Iterator<Map.Entry<String,WarInfo>> i = currentStatus.entrySet().iterator(); i.hasNext();) {
            Map.Entry<String,WarInfo> entry = i.next();
            WarInfo info = entry.getValue();
            if (log.isTraceEnabled()) {
                log.trace(sm.getString("warWatcher.checkingWar", info.getWar()));
            }
            int check = info.check();
            if (check == 1) {
                listener.fileModified(info.getWar());
            } else if (check == -1) {
                listener.fileRemoved(info.getWar());
                // no need to keep in memory
                i.remove();
            }
            if (log.isTraceEnabled()) {
                log.trace(sm.getString("warWatcher.checkWarResult", Integer.valueOf(check), info.getWar()));
            }
        }

    }

    /**
     * add cluster war to the watcher state
     *
     * @param warfile The WAR to add
     */
    protected void addWarInfo(File warfile) {
        WarInfo info = currentStatus.get(warfile.getAbsolutePath());
        if (info == null) {
            info = new WarInfo(warfile);
            info.setLastState(-1); // assume file is non existent
            currentStatus.put(warfile.getAbsolutePath(), info);
        }
    }

    /**
     * clear watcher state
     */
    public void clear() {
        currentStatus.clear();
    }


    /*--Inner classes-------------------------------------------*/

    /**
     * File name filter for war files
     */
    protected static class WarFilter implements java.io.FilenameFilter {
        @Override
        public boolean accept(File path, String name) {
            if (name == null) {
                return false;
            }
            return name.endsWith(".war");
        }
    }

    /**
     * File information on existing WAR files
     */
    protected static class WarInfo {
        protected final File war;

        protected long lastChecked;

        protected long lastState = 0;

        public WarInfo(File war) {
            this.war = war;
            this.lastChecked = war.lastModified();
            if (!war.exists()) {
                lastState = -1;
            }
        }

        public boolean modified() {
            return war.exists() && war.lastModified() > lastChecked;
        }

        public boolean exists() {
            return war.exists();
        }

        /**
         * Returns 1 if the file has been added/modified, 0 if the file is unchanged and -1 if the file has been removed
         *
         * @return int 1=file added; 0=unchanged; -1=file removed
         */
        public int check() {
            // file unchanged by default
            int result = 0;

            if (modified()) {
                // file has changed - timestamp
                result = 1;
                lastState = result;
            } else if ((!exists()) && (!(lastState == -1))) {
                // file was removed
                result = -1;
                lastState = result;
            } else if ((lastState == -1) && exists()) {
                // file was added
                result = 1;
                lastState = result;
            }
            this.lastChecked = System.currentTimeMillis();
            return result;
        }

        public File getWar() {
            return war;
        }

        @Override
        public int hashCode() {
            return war.getAbsolutePath().hashCode();
        }

        @Override
        public boolean equals(Object other) {
            if (other instanceof WarInfo) {
                WarInfo wo = (WarInfo) other;
                return wo.getWar().equals(getWar());
            } else {
                return false;
            }
        }

        protected void setLastState(int lastState) {
            this.lastState = lastState;
        }

    }

}