/**
 * Copyright (c) 2012-2017, jcabi.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 1) Redistributions of source code must retain the above
 * copyright notice, this list of conditions and the following
 * disclaimer. 2) Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution. 3) Neither the name of the jcabi.com nor
 * the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jcabi.aspects.aj;

import com.jcabi.aspects.Loggable;
import com.jcabi.log.VerboseRunnable;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * For updating cache and cleaning expired cache.
 * @author Peng Chuanjiang (pengchuanjiang@souyidai.com)
 * @version $Id: 4c18b638735db6e83ba1da68a4d685c6e12aa222 $
 */
@SuppressWarnings("PMD.DoNotUseThreads")
public final class UpdateMethodCacher {

    /**
     * Calling tunnels.
     */
    private final transient
        ConcurrentMap<MethodCacher.Key, MethodCacher.Tunnel> tunnels;

    /**
     * Service that cleans cache.
     */
    private final transient ScheduledExecutorService cleaner;

    /**
     * Save the keys of caches which need update.
     */
    private final transient BlockingQueue<MethodCacher.Key> updatekeys;

    /**
     * Service that update cache.
     */
    private final transient ScheduledExecutorService updater;

    /**
     * Public ctor.
     * @param tls ConcurrentMap
     * @param ukeys BlockingQueue
     */
    public UpdateMethodCacher(
        final ConcurrentMap<MethodCacher.Key, MethodCacher.Tunnel> tls,
        final BlockingQueue<MethodCacher.Key> ukeys) {
        this.tunnels = tls;
        this.updatekeys = ukeys;
        this.cleaner = Executors.newSingleThreadScheduledExecutor(
            new NamedThreads(
                "cacheable-clean",
                "automated cleaning of expired @Cacheable values"
            )
        );
        this.updater = Executors.newSingleThreadScheduledExecutor(
            new NamedThreads(
                "cacheable-update",
                "async update of expired @Cacheable values"
            )
        );
    }

    /**
     * Staring two threads(cheaner and updater) are used for
     * updating cache and cleaning expired cache.
     */
    public void start() {
        this.cleaner.scheduleWithFixedDelay(
            new VerboseRunnable(
                new Runnable() {
                    @Override
                    public void run() {
                        UpdateMethodCacher.this.clean();
                    }
                }
            ),
            1L, 1L, TimeUnit.SECONDS
        );
        this.updater.schedule(
            new VerboseRunnable(
                new Runnable() {
                    @Override
                    public void run() {
                        UpdateMethodCacher.this.update();
                    }
                }
            ),
            0L, TimeUnit.SECONDS
        );
    }

    /**
     * Clean cache.
     */
    private void clean() {
        synchronized (this.tunnels) {
            for (final MethodCacher.Key key : this.tunnels.keySet()) {
                if (this.tunnels.get(key).expired()
                    && !this.tunnels.get(key).asyncUpdate()) {
                    final MethodCacher.Tunnel tunnel = this.tunnels.remove(key);
                    LogHelper.log(
                        key.getLevel(),
                        this,
                        "%s:%s expired in cache",
                        key,
                        tunnel
                    );
                }
            }
        }
    }

    /**
     * Update cache.
     */
    @SuppressWarnings("PMD.AvoidCatchingThrowable")
    private void update() {
        while (true) {
            try {
                final MethodCacher.Key key = this.updatekeys.take();
                final MethodCacher.Tunnel tunnel = this.tunnels.get(key);
                if (tunnel != null && tunnel.expired()) {
                    final MethodCacher.Tunnel newTunnel = tunnel.copy();
                    newTunnel.through();
                    this.tunnels.put(key, newTunnel);
                }
            } catch (final InterruptedException ex) {
                LogHelper.log(
                    Loggable.ERROR,
                    this,
                    "%s:%s",
                    ex.getMessage(),
                    ex
                );
                // @checkstyle IllegalCatch (1 line)
            } catch (final Throwable ex) {
                LogHelper.log(
                    Loggable.ERROR,
                    this,
                    "Exception message is %s, Exception is %s",
                    ex.getMessage(),
                    ex
                );
            }
        }
    }

}
