/*
 * 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.ignite.spi.checkpoint.sharedfs;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.marshaller.Marshaller;
import org.apache.ignite.spi.IgniteSpiThread;
import org.apache.ignite.spi.checkpoint.CheckpointListener;

/**
 * Implementation of {@link org.apache.ignite.spi.IgniteSpiThread} that takes care about outdated files.
 * Every checkpoint has expiration date after which it makes no sense to
 * keep it. This class periodically compares files last access time with given
 * expiration time.
 * <p>
 * If this file was not accessed then it is deleted. If file access time is
 * different from modification date new expiration date is set.
 */
class SharedFsTimeoutTask extends IgniteSpiThread {
    /** Map of files to their access and expiration date. */
    private Map<File, SharedFsTimeData> files = new HashMap<>();

    /** Messages logger. */
    private IgniteLogger log;

    /** Messages marshaller. */
    private Marshaller marshaller;

    /** */
    private final Object mux = new Object();

    /** Timeout listener. */
    private CheckpointListener lsnr;

    /**
     * Creates new instance of task that looks after files.
     *
     * @param igniteInstanceName Ignite instance name.
     * @param marshaller Messages marshaller.
     * @param log Messages logger.
     */
    SharedFsTimeoutTask(String igniteInstanceName, Marshaller marshaller, IgniteLogger log) {
        super(igniteInstanceName, "grid-sharedfs-timeout-worker", log);

        assert marshaller != null;
        assert log != null;

        this.marshaller = marshaller;
        this.log = log.getLogger(getClass());
    }

    /** {@inheritDoc} */
    @Override public void body() throws InterruptedException {
        long nextTime = 0;

        Collection<String> rmvKeys = new HashSet<>();

        while (!isInterrupted()) {
            rmvKeys.clear();

            synchronized (mux) {
                // nextTime is 0 only on first iteration.
                if (nextTime != 0) {
                    long delay;

                    if (nextTime == -1)
                        delay = 5000;
                    else {
                        assert nextTime > 0;

                        delay = nextTime - U.currentTimeMillis();
                    }

                    while (delay > 0) {
                        mux.wait(delay);

                        delay = nextTime - U.currentTimeMillis();
                    }
                }

                Map<File, SharedFsTimeData> snapshot = new HashMap<>(files);

                long now = U.currentTimeMillis();

                nextTime = -1;

                // Check files one by one and physically remove
                // if (now - last modification date) > expiration time
                for (Map.Entry<File, SharedFsTimeData> entry : snapshot.entrySet()) {
                    File file = entry.getKey();

                    SharedFsTimeData timeData = entry.getValue();

                    try {
                        if (timeData.getLastAccessTime() != file.lastModified())
                            timeData.setExpireTime(SharedFsUtils.read(file, marshaller, log).getExpireTime());
                    }
                    catch (IgniteCheckedException e) {
                        U.error(log, "Failed to marshal/unmarshal in checkpoint file: " + file.getAbsolutePath(), e);

                        continue;
                    }
                    catch (IOException e) {
                        if (!file.exists()) {
                            files.remove(file);

                            rmvKeys.add(timeData.getKey());
                        }
                        else
                            U.error(log, "Failed to read checkpoint file: " + file.getAbsolutePath(), e);

                        continue;
                    }

                    if (timeData.getExpireTime() > 0)
                        if (timeData.getExpireTime() <= now)
                            if (!file.delete() && file.exists())
                                U.error(log, "Failed to delete check point file by timeout: " + file.getAbsolutePath());
                            else {
                                files.remove(file);

                                rmvKeys.add(timeData.getKey());

                                if (log.isDebugEnabled())
                                    log.debug("File was deleted by timeout: " + file.getAbsolutePath());
                            }
                        else
                            if (timeData.getExpireTime() < nextTime || nextTime == -1)
                                nextTime = timeData.getExpireTime();
                }
            }

            CheckpointListener lsnr = this.lsnr;

            if (lsnr != null)
                for (String key : rmvKeys)
                    lsnr.onCheckpointRemoved(key);
        }

        synchronized (mux) {
            files.clear();
        }
    }

    /**
     * Adds file to a list of files this task should look after.
     *
     * @param file File being watched.
     * @param timeData File expiration and access information.
     */
    void add(File file, SharedFsTimeData timeData) {
        assert file != null;
        assert timeData != null;

        synchronized (mux) {
            files.put(file, timeData);

            mux.notifyAll();
        }
    }

    /**
     * Adds list of files this task should looks after.
     *
     * @param newFiles List of files.
     */
    void add(Map<File, SharedFsTimeData> newFiles) {
        assert newFiles != null;

        synchronized (mux) {
            files.putAll(newFiles);

            mux.notifyAll();
        }
    }

    /**
     * Stops watching file.
     *
     * @param file File that task should not look after anymore.
     */
    void remove(File file) {
        assert file != null;

        synchronized (mux) {
            files.remove(file);
        }
    }

    /**
     * Stops watching file list.
     *
     * @param delFiles List of files this task should not look after anymore.
     */
    void remove(Iterable<File> delFiles) {
        assert delFiles != null;

        synchronized (mux) {
            for (File file : delFiles)
                files.remove(file);
        }
    }

    /**
     * Sets listener.
     *
     * @param lsnr Listener.
     */
    void setCheckpointListener(CheckpointListener lsnr) {
        this.lsnr = lsnr;
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return S.toString(SharedFsTimeoutTask.class, this);
    }
}