/* package whatever; // don't place package name! */

import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
    private static final int GROUP_SIZE = 3;
    private static final Runnable FINISHER = () -> { System.out.println("finisher executing"); };
    private static final GroupMonitoringService MONITORING_SERVICE = new GroupMonitoringService(FINISHER, GROUP_SIZE);

    public static void main(String[] args) {
        final ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
        System.out.println("- [" + LocalTime.now() + "] run parent-task...");

        // create 3 tasks: each task needs 7 seconds.
        var tasks = createTasks();
        tasks.forEach(t -> ses.scheduleWithFixedDelay(t, 0, 2, TimeUnit.SECONDS));
    }

    private static List<Runnable> createTasks() {
        List<Runnable> result = new ArrayList<>();
        for (int i = 0; i < GROUP_SIZE; i++) {
            RandomWaitTask originalTask = new RandomWaitTask();
            CountingRunnable groupedTask = new CountingRunnable(originalTask, "Task " + i);
            Runnable notifyingRunnable = () -> {
                groupedTask.run();
                MONITORING_SERVICE.taskFinished(groupedTask.getGroup());
            };
            result.add(notifyingRunnable);
        }
        return result;
    }
}

class GroupMonitoringService {
    // key: group, value: tasks
    Map<String, AtomicInteger> finishedTasks = new HashMap<>();
    private final Runnable finisher;
    private final int groupSize;

    GroupMonitoringService(Runnable finisher, int groupSize) {
        this.finisher = finisher;
        this.groupSize = groupSize;
    }

    public synchronized void taskFinished(String group) {
        var finishedInGroup = finishedTasks.computeIfAbsent(group, k -> new AtomicInteger());
        if (finishedInGroup.incrementAndGet() >= groupSize) {
            // scheduling group complete
            System.out.printf("Group %s finished executing%n", group);
            finisher.run();
            finishedTasks.remove(group);
        }
    }
}

interface GroupedRunnable extends Runnable {
    String getGroup();
}

class CountingRunnable implements GroupedRunnable {
    private AtomicInteger counter = new AtomicInteger();
    private final Runnable delegate;
    private final String taskName;

    CountingRunnable(Runnable delegate, String taskName) {
        this.delegate = delegate;
        this.taskName = taskName;
    }
    public void run() {
        System.out.printf("[%s] - Running task %s in group %s%n", LocalTime.now(), taskName, getGroup());
        delegate.run();
        counter.incrementAndGet();
        System.out.printf("[%s] - Running task %s in group %s finished%n", LocalTime.now(), taskName, getGroup());
    }

    @Override
    public String getGroup() {
        return counter.toString();
    }
}

class RandomWaitTask implements Runnable {
    static final Random RANDOM = new Random();
    @Override
    public void run() {
        try {
            Thread.sleep(RANDOM.nextInt(10_000));
        } catch (InterruptedException e) {
        }
    }
}