/*
 * Copyright 2017 The RoboZonky Project
 *
 * Licensed 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 com.github.robozonky.app.configuration.daemon;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadFactory;
import java.util.stream.IntStream;

import com.github.robozonky.api.ReturnCode;
import com.github.robozonky.api.marketplaces.Marketplace;
import com.github.robozonky.app.authentication.Authenticated;
import com.github.robozonky.app.configuration.InvestmentMode;
import com.github.robozonky.app.investing.Investor;
import com.github.robozonky.app.runtime.RuntimeHandler;
import com.github.robozonky.util.RoboZonkyThreadFactory;
import com.github.robozonky.util.Scheduler;
import com.github.robozonky.util.Schedulers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DaemonInvestmentMode implements InvestmentMode {

    private static final Logger LOGGER = LoggerFactory.getLogger(DaemonInvestmentMode.class);
    private static final ThreadFactory THREAD_FACTORY = new RoboZonkyThreadFactory(newThreadGroup("rzDaemon"));
    private final Marketplace marketplace;
    private final List<DaemonOperation> daemons;
    private final PortfolioUpdater portfolioUpdater;
    private final Runnable blockedAmountsUpdater;

    public DaemonInvestmentMode(final Authenticated auth, final PortfolioUpdater p, final Investor.Builder builder,
                                final Marketplace marketplace,
                                final StrategyProvider strategyProvider, final Runnable blockedAmountsUpdater,
                                final Duration maximumSleepPeriod, final Duration primaryMarketplaceCheckPeriod,
                                final Duration secondaryMarketplaceCheckPeriod) {
        this.marketplace = marketplace;
        this.portfolioUpdater = p;
        this.blockedAmountsUpdater = blockedAmountsUpdater;
        this.daemons = Arrays.asList(new InvestingDaemon(auth, builder, marketplace, strategyProvider::getToInvest, p,
                                                         maximumSleepPeriod, primaryMarketplaceCheckPeriod),
                                     new PurchasingDaemon(auth, strategyProvider::getToPurchase, p, maximumSleepPeriod,
                                                          secondaryMarketplaceCheckPeriod, builder.isDryRun()));
    }

    private static ThreadGroup newThreadGroup(final String name) {
        final ThreadGroup threadGroup = new ThreadGroup(name);
        threadGroup.setMaxPriority(Thread.NORM_PRIORITY + 1); // these threads should be a bit more important
        threadGroup.setDaemon(true); // no thread from this group shall block shutdown
        return threadGroup;
    }

    private static LocalDateTime getNextFourAM(final LocalDateTime now) {
        final LocalDateTime fourAM = LocalTime.of(4, 0).atDate(now.toLocalDate());
        if (fourAM.isAfter(now)) {
            return fourAM;
        }
        return fourAM.plusDays(1);
    }

    private static Duration timeUntil4am(final LocalDateTime now) {
        final LocalDateTime nextFourAm = getNextFourAM(now);
        return Duration.between(now, nextFourAm);
    }

    private void executeDaemons(final Scheduler executor) {
        // run portfolio update twice a day
        final Duration twelveHours = Duration.ofHours(12);
        LOGGER.trace("Scheduling portfolio update every {} seconds.", twelveHours.getSeconds());
        executor.submit(portfolioUpdater, twelveHours);
        // also run blocked amounts update every now and then to detect changes made outside of the robot
        final Duration oneHour = Duration.ofHours(1);
        LOGGER.trace("Scheduling blocked amounts update every {} seconds, starting in {} seconds.",
                     oneHour.getSeconds(), oneHour.getSeconds());
        executor.submit(blockedAmountsUpdater, oneHour, oneHour);
        // run investing and purchasing daemons
        IntStream.range(0, daemons.size()).boxed().forEach(daemonId -> {
            final DaemonOperation d = daemons.get(daemonId);
            final long initialDelay = daemonId * 250; // quarter second apart
            final Runnable task = new Skippable(d, portfolioUpdater::isUpdating);
            LOGGER.trace("Scheduling {} every {} ms, starting in {} ms.", task, d.getRefreshInterval().toMillis(),
                         initialDelay);
            executor.submit(task, d.getRefreshInterval(), Duration.ofMillis(initialDelay));
        });
    }

    @Override
    public ReturnCode apply(final RuntimeHandler runtimeHandler) {
        try (final Scheduler executor = Schedulers.INSTANCE.create(2, THREAD_FACTORY)) {
            // schedule the tasks
            executeDaemons(executor);
            // block until request to stop the app is received
            runtimeHandler.suspend();
            LOGGER.trace("Request to stop received.");
            // signal the end of standard operation
            return ReturnCode.OK;
        }
    }

    @Override
    public void close() throws Exception {
        LOGGER.trace("Closing marketplace.");
        this.marketplace.close();
    }
}

@@ -3887,164 +3887,8 @@
 day%0A
-        final Duration twelveHours = Duration.ofHours(12);%0A        LOGGER.trace(%22Scheduling portfolio update every %7B%7D seconds.%22, twelveHours.getSeconds());%0A
     
@@ -3929,19 +3929,28 @@
 er, 
-twelve
+Duration.of
 Hours
+(12)
 );%0A 
@@ -4112,174 +4112,8 @@
 1);%0A
-        LOGGER.trace(%22Scheduling blocked amounts update every %7B%7D seconds, starting in %7B%7D seconds.%22,%0A                     oneHour.getSeconds(), oneHour.getSeconds());%0A
     
@@ -4520,163 +4520,8 @@
 g);%0A
-            LOGGER.trace(%22Scheduling %7B%7D every %7B%7D ms, starting in %7B%7D ms.%22, task, d.getRefreshInterval().toMillis(),%0A                         initialDelay);%0A
     

Remove redundant logging