001package org.intellimate.izou.system.sound;
002
003import org.intellimate.izou.addon.AddOnModel;
004import org.intellimate.izou.events.EventLifeCycle;
005import org.intellimate.izou.events.EventListenerModel;
006import org.intellimate.izou.events.EventMinimalImpl;
007import org.intellimate.izou.events.EventModel;
008import org.intellimate.izou.identification.Identification;
009import org.intellimate.izou.identification.IdentificationManager;
010import org.intellimate.izou.main.Main;
011import org.intellimate.izou.resource.ResourceMinimalImpl;
012import org.intellimate.izou.resource.ResourceModel;
013import org.intellimate.izou.util.AddonThreadPoolUser;
014import org.intellimate.izou.util.IzouModule;
015import ro.fortsoft.pf4j.AspectOrAffected;
016
017import java.lang.ref.Reference;
018import java.lang.ref.WeakReference;
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.net.URL;
022import java.time.LocalDateTime;
023import java.time.temporal.ChronoUnit;
024import java.util.*;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.concurrent.Future;
027import java.util.concurrent.TimeUnit;
028import java.util.concurrent.atomic.AtomicBoolean;
029import java.util.concurrent.locks.Condition;
030import java.util.concurrent.locks.Lock;
031import java.util.concurrent.locks.ReentrantLock;
032import java.util.function.Function;
033import java.util.function.Predicate;
034import java.util.stream.Collectors;
035
036/**
037 * the SoundManager manages all IzouSoundLine, tracks them and is responsible for enforcing that only one permanent-
038 * AddOn can play at one time.
039 * @author LeanderK
040 * @version 1.0
041 */
042//TODO: native sound code enforcing (mute, stop(?) etc.
043//TODO: we must enforce sequential access (only one addon can defacto play sound). We can hide this using the IzouSoundLines (closing and opening the underlying lines).
044public class SoundManager extends IzouModule implements AddonThreadPoolUser, EventListenerModel {
045    //non-permanent and general fields
046    private ConcurrentHashMap<AddOnModel, List<WeakReference<IzouSoundLineBaseClass>>> nonPermanent = new ConcurrentHashMap<>();
047    //not null if this AddOn is currently muting the others Lines
048    private MutingManager mutingManager = null;
049    private final Object mutingLock = new Object();
050
051    //permanent fields, there is a Read/Write lock!
052    private List<WeakReference<IzouSoundLineBaseClass>> permanentLines = null;
053    private AddOnModel permanentAddOn = null;
054    //gets filled when the event got fired
055    private Identification knownIdentification = null;
056    //Addon has 10 sec to obtain an IzouSoundLine
057    private LocalDateTime permissionWithoutUsageLimit = null;
058    //if true we can do nothing to check whether he closed.
059    private boolean isUsingNonJava = false;
060    private Future permissionWithoutUsageCloseThread = null;
061    private final Object permanentUserReadWriteLock = new Object();
062    private AtomicBoolean isUsing = new AtomicBoolean(false);
063
064    public SoundManager(Main main) {
065        super(main);
066        main.getEventDistributor().registerEventListener(Arrays.asList(SoundIDs.StartEvent.descriptor, SoundIDs.StartRequest.descriptor,
067                SoundIDs.EndedEvent.descriptor), this);
068
069        URL mixerURL = this.getClass().getClassLoader().getResource("org/intellimate/izou/system/sound/replaced/MixerAspect.class");
070        AspectOrAffected mixer = new AspectOrAffected(mixerURL,
071                "org.intellimate.izou.system.sound.replaced.MixerAspect",
072                aClass -> {
073                    try {
074                        Method init = aClass.getMethod("init", Main.class);
075                        init.invoke(null, main);
076                        return aClass;
077                    } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
078                        error("error while trying to initialize the MixerAspect");
079                        return aClass;
080                    }
081                },
082                true);
083        URL audioSystemURL = this.getClass().getClassLoader().getResource("javax/sound/sampled/AudioSystem.class");
084        AspectOrAffected audioSystem = new AspectOrAffected(audioSystemURL,
085                "javax.sound.sampled.AudioSystem",
086                Function.identity(),
087                false);
088        getMain().getAddOnManager().addAspectOrAffected(audioSystem);
089        getMain().getAddOnManager().addAspectOrAffected(mixer);
090    }
091
092    /**
093     * removes obsolete references
094     */
095    private void tidy() {
096        nonPermanent.entrySet().stream()
097                .map(entry -> {
098                    List<WeakReference<IzouSoundLineBaseClass>> collect = entry.getValue().stream()
099                            .filter(izouSoundLineWeakReference -> izouSoundLineWeakReference.get() != null)
100                            .collect(Collectors.toList());
101                    if (!collect.isEmpty()) {
102                        nonPermanent.put(entry.getKey(), collect);
103                        return null;
104                    } else {
105                        return entry;
106                    }
107                })
108                .filter(Objects::nonNull)
109                .forEach(entry -> nonPermanent.remove(entry.getKey()));
110    }
111
112    /**
113     * adds an IzouSoundLine, will now be tracked by the SoundManager
114     * @param addOnModel the addOnModel where the IzouSoundLine belongs to
115     * @param izouSoundLine the IzouSoundLine to add
116     */
117    public void addIzouSoundLine(AddOnModel addOnModel, IzouSoundLineBaseClass izouSoundLine) {
118        debug("adding soundLine " + izouSoundLine + " from " + addOnModel);
119        if (permanentAddOn != null && permanentAddOn.equals(addOnModel)) {
120            addPermanent(izouSoundLine);
121        } else {
122            addNonPermanent(addOnModel, izouSoundLine);
123        }
124        izouSoundLine.registerCloseCallback(voit -> closeCallback(addOnModel, izouSoundLine));
125        izouSoundLine.registerMuteCallback(voit -> muteCallback(addOnModel, izouSoundLine));
126    }
127
128    protected void muteCallback(AddOnModel addOnModel, IzouSoundLineBaseClass izouSoundLine) {
129        synchronized (mutingLock) {
130            if (mutingManager != null && mutingManager.getMuting().equals(addOnModel)) {
131                mutingManager.add(izouSoundLine);
132            } else if (mutingManager == null || mutingManager.isTimeOut()) {
133                mutingManager = new MutingManager(this, addOnModel, izouSoundLine);
134            }
135        }
136    }
137
138    /**
139     * the close-callback or the AddonModel, removes now redundant references
140     * @param addOnModel the addOnModel where the IzouSoundLine belongs to
141     * @param izouSoundLine the izouSoundLine
142     */
143    private void closeCallback(AddOnModel addOnModel, IzouSoundLine izouSoundLine) {
144        debug("removing soundline " + izouSoundLine + " from " + addOnModel);
145        Predicate<WeakReference<IzouSoundLineBaseClass>> removeFromList =
146                weakReference -> weakReference.get() != null && weakReference.get().equals(izouSoundLine);
147        synchronized (permanentUserReadWriteLock) {
148            if (permanentAddOn != null && permanentAddOn.equals(addOnModel) && permanentLines != null) {
149                permanentLines.removeIf(removeFromList);
150                if (permanentLines.isEmpty()) {
151                    permanentLines = null;
152                    permissionWithoutUsage();
153                }
154            }
155        }
156        List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.get(addOnModel);
157        if (weakReferences != null) {
158            weakReferences.removeIf(removeFromList);
159            synchronized (mutingLock) {
160                if (mutingManager != null && mutingManager.getMuting().equals(addOnModel)) {
161                    mutingManager = mutingManager.remove(izouSoundLine);
162                }
163            }
164        }
165        submit(this::tidy);
166    }
167
168    /**
169     * creates a LocaleDateTime-Object 10 seconds in the Future and a Thread which will remove it, if it passes the threshold.
170     * the Thread
171     */
172    private void permissionWithoutUsage() {
173        if (isUsingNonJava)
174            return;
175        synchronized (permanentUserReadWriteLock) {
176            permissionWithoutUsageLimit = LocalDateTime.now().plus(10, ChronoUnit.SECONDS);
177            permissionWithoutUsageCloseThread =  getMain().getThreadPoolManager().getAddOnsThreadPool()
178                    .submit((Runnable)() -> {
179                try {
180                    Thread.sleep(10000);
181                    fireLineAccessEndedNotification();
182                    endPermanent(permanentAddOn);
183                } catch (InterruptedException ignored) {
184                    //ignored.printStackTrace();
185                }
186            });
187        }
188    }
189
190    /**
191     * removes the LocaleDateTime and Thread (if exisiting)
192     */
193    private void endWaitingForUsage() {
194        synchronized (permanentUserReadWriteLock) {
195            if (permissionWithoutUsageLimit != null)
196                permissionWithoutUsageLimit = null;
197            if (permissionWithoutUsageCloseThread != null) {
198                permissionWithoutUsageCloseThread.cancel(true);
199                permissionWithoutUsageLimit = null;
200            }
201        }
202    }
203
204    /**
205     * adds the IzouSoundLine as permanent
206     * @param izouSoundLine the izouSoundLine to add
207     */
208    private void addPermanent(IzouSoundLineBaseClass izouSoundLine) {
209        debug("adding " + izouSoundLine + " to permanent");
210        if (!izouSoundLine.isPermanent())
211            izouSoundLine.setToPermanent();
212        synchronized (permanentUserReadWriteLock) {
213            endWaitingForUsage();
214            if (permanentLines == null) {
215                permanentLines = Collections.synchronizedList(new ArrayList<>());
216            }
217            permanentLines.add(new WeakReference<>(izouSoundLine));
218        }
219        //TODO: STOP the addon via the stop event
220    }
221
222    /**
223     * adds the IzouSoundLine as NonPermanent
224     * @param addOnModel the AddonModel to
225     * @param izouSoundLine the IzouSoundLine to add
226     */
227    private void addNonPermanent(AddOnModel addOnModel, IzouSoundLineBaseClass izouSoundLine) {
228        debug("adding " + izouSoundLine + " from " + addOnModel + " to non-permanent");
229        if (izouSoundLine.isPermanent())
230            izouSoundLine.setToNonPermanent();
231        List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.get(addOnModel);
232        if (weakReferences == null)
233            weakReferences = Collections.synchronizedList(new ArrayList<>());
234        nonPermanent.put(addOnModel, weakReferences);
235        weakReferences.add(new WeakReference<>(izouSoundLine));
236    }
237
238    /**
239     * tries to register the AddonModel as permanent
240     * @param addOnModel the AddonModel to register
241     * @param source the Source which requested the usage
242     * @param nonJava true if it is not using java to play sounds
243     */
244    public void requestPermanent(AddOnModel addOnModel, Identification source, boolean nonJava) {
245        debug("requesting permanent for addon: " + addOnModel);
246        boolean notUsing = isUsing.compareAndSet(false, true);
247        if (!notUsing) {
248            debug("already used by " + permanentAddOn);
249            synchronized (permanentUserReadWriteLock) {
250                if (permanentAddOn != null && permanentAddOn.equals(addOnModel)) {
251                    if (knownIdentification == null)
252                        knownIdentification = source;
253                    return;
254                } else {
255                    endPermanent(permanentAddOn);
256                    addAsPermanent(addOnModel, source, nonJava);
257                }
258            }
259        } else {
260            addAsPermanent(addOnModel, source, nonJava);
261        }
262    }
263
264    protected void addAsPermanent(AddOnModel addOnModel, Identification source, boolean nonJava) {
265        synchronized (permanentUserReadWriteLock) {
266            permanentAddOn = addOnModel;
267            knownIdentification = source;
268            isUsingNonJava = nonJava;
269            permissionWithoutUsageLimit = null;
270            if (permissionWithoutUsageCloseThread != null)
271                permissionWithoutUsageCloseThread.cancel(true);
272            permissionWithoutUsageCloseThread = null;
273
274            List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.remove(addOnModel);
275            if (weakReferences == null) {
276                if (isUsingNonJava) {
277                    permanentLines = new ArrayList<>();
278                } else {
279                    permissionWithoutUsage();
280                }
281            } else {
282                nonPermanent.remove(addOnModel);
283                permanentLines = weakReferences;
284                permanentLines.forEach(weakReferenceLine -> {
285                    IzouSoundLineBaseClass izouSoundLineBaseClass = weakReferenceLine.get();
286                    if (izouSoundLineBaseClass != null) {
287                        izouSoundLineBaseClass.setToPermanent();
288                        izouSoundLineBaseClass.setResponsibleID(source);
289                    }
290                });
291            }
292            synchronized (mutingLock) {
293                if (mutingManager != null && mutingManager.getMuting().equals(addOnModel)) {
294                    mutingManager.cancel();
295                    mutingManager = null;
296                }
297            }
298        }
299    }
300
301    /**
302     * unregisters the AddonModel as permanent
303     * @param addOnModel the addonModel to check
304     */
305    public void endPermanent(AddOnModel addOnModel) {
306        if (!isUsing.get() || (permanentAddOn != null && !permanentAddOn.equals(addOnModel)))
307            return;
308        synchronized (permanentUserReadWriteLock) {
309            permanentAddOn = null;
310            Identification tempID = this.knownIdentification;
311            this.knownIdentification = null;
312            if (permanentLines != null) {
313                permanentLines.forEach(weakReferenceLine -> {
314                    if (weakReferenceLine.get() != null)
315                        weakReferenceLine.get().setToNonPermanent();
316                });
317                nonPermanent.put(addOnModel, permanentLines);
318                permanentLines = null;
319            }
320            stopAddon(tempID);
321            endWaitingForUsage();
322            isUsing.set(false);
323        }
324    }
325
326    private void stopAddon(Identification identification) {
327        Lock lock = new ReentrantLock();
328        Condition callback = lock.newCondition();
329        if (identification != null) {
330            IdentificationManager.getInstance()
331                    .getIdentification(this)
332                    .map(id -> new EventMinimalImpl(SoundIDs.StopEvent.type, id, SoundIDs.StopEvent.descriptors, eventLifeCycle -> {
333                        if (eventLifeCycle.equals(EventLifeCycle.ENDED)) {
334                            lock.lock();
335                            try {
336                                callback.signal();
337                            } finally {
338                                lock.unlock();
339                            }
340                        }
341                    }))
342                    .map(eventMinimal -> eventMinimal.addResource(
343                            new ResourceMinimalImpl<>(SoundIDs.StopEvent.resourceSelector, eventMinimal.getSource(), identification, null)))
344                    .ifPresent(event -> getMain().getEventDistributor().fireEventConcurrently(event));
345        }
346        lock.lock();
347        try {
348            callback.await(1, TimeUnit.SECONDS);
349        } catch (InterruptedException ignored) {
350
351        } finally {
352            lock.unlock();
353        }
354    }
355
356    private void fireLineAccessEndedNotification() {
357        if (knownIdentification != null) {
358            EventModel event = new EventMinimalImpl(SoundIDs.EndedEvent.type, knownIdentification, SoundIDs.EndedEvent.descriptors);
359            getMain().getEventDistributor().fireEventConcurrently(event);
360        }
361    }
362
363    /**
364     * mutes the other Addons
365     * @param addOnModel the addonModel responsible
366     */
367    void muteOthers(AddOnModel addOnModel) {
368        Set<AddOnModel> toMute = nonPermanent.entrySet().stream()
369                .filter(entry -> !entry.getKey().equals(addOnModel))
370                .flatMap(entry -> entry.getValue().stream())
371                .map(Reference::get)
372                .filter(Objects::nonNull)
373                .peek(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(true))
374                .map(IzouSoundLine::getAddOnModel)
375                .collect(Collectors.toSet());
376        if (permanentAddOn != null && !permanentAddOn.equals(addOnModel) && permanentLines != null) {
377            toMute.add(addOnModel);
378            permanentLines.stream()
379                .map(Reference::get)
380                .filter(Objects::nonNull)
381                .forEach(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(true));
382        }
383        toMute.forEach(this::mute);
384        List<WeakReference<IzouSoundLineBaseClass>> weakReferences = nonPermanent.get(addOnModel);
385        if (weakReferences != null) {
386            weakReferences.stream()
387                    .map(Reference::get)
388                    .filter(Objects::nonNull)
389                    .forEach(izouSoundLine -> izouSoundLine.setMutedFromSystem(false));
390        }
391    }
392
393    /**
394     * mutes the list of soundLines and fires the Mute-Event
395     * @param model the addonModel to mute
396     */
397    private void mute(AddOnModel model) {
398        IdentificationManager.getInstance()
399                .getIdentification(this)
400                .map(id -> new EventMinimalImpl(SoundIDs.MuteEvent.type, id, SoundIDs.MuteEvent.descriptors))
401                .map(eventMinimal -> eventMinimal.addResource(
402                        new ResourceMinimalImpl<>(SoundIDs.MuteEvent.resourceSelector, eventMinimal.getSource(), model, null)))
403                .ifPresent(event -> getMain().getEventDistributor().fireEventConcurrently(event));
404    }
405
406    /**
407     * unmutes all
408     */
409    void unmute() {
410        nonPermanent.entrySet().stream()
411                .flatMap(entry -> entry.getValue().stream())
412                .map(Reference::get)
413                .filter(Objects::nonNull)
414                .forEach(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(false));
415
416        if (permanentLines != null)
417            permanentLines.stream()
418                .map(Reference::get)
419                .filter(Objects::nonNull)
420                .forEach(izouSoundLineBaseClass -> izouSoundLineBaseClass.setMutedFromSystem(false));
421
422        IdentificationManager.getInstance()
423                .getIdentification(this)
424                .map(id -> new EventMinimalImpl(SoundIDs.UnMuteEvent.type, id, SoundIDs.UnMuteEvent.descriptors))
425                .ifPresent(event -> getMain().getEventDistributor().fireEventConcurrently(event));
426    }
427
428    private void checkAndUpdateIdentification(Identification identification) {
429        AddOnModel addonModel = getMain().getInternalIdentificationManager().getAddonModel(identification);
430        if (permanentAddOn.equals(addonModel)) {
431            synchronized (permanentUserReadWriteLock) {
432                if (permanentAddOn.equals(addonModel)) {
433                    knownIdentification = identification;
434                }
435            }
436        }
437    }
438
439    /**
440     * Invoked when an activator-event occurs.
441     *
442     * @param event an instance of Event
443     */
444    @Override
445    public void eventFired(EventModel event) {
446        if (event.containsDescriptor(SoundIDs.StartRequest.descriptor)) {
447            Identification identification = event.getListResourceContainer().provideResource("izou.common.resource.selector").stream()
448                    .map(ResourceModel::getResource)
449                    .filter(resource -> resource instanceof Identification)
450                    .map(resource -> (Identification) resource)
451                    .findFirst()
452                    .orElseGet(event::getSource);
453
454            AddOnModel addonModel = getMain().getInternalIdentificationManager().getAddonModel(identification);
455
456            if (addonModel != null) {
457                requestPermanent(addonModel,
458                        event.getSource(), event.containsDescriptor(SoundIDs.StartEvent.isUsingNonJava));
459            }
460
461        } else if (event.containsDescriptor(SoundIDs.StartEvent.descriptor)) {
462            checkAndUpdateIdentification(event.getSource());
463        } else {
464            Identification identification = event.getListResourceContainer().provideResource("izou.common.resource.selector").stream()
465                    .map(ResourceModel::getResource)
466                    .filter(resource -> resource instanceof Identification)
467                    .map(resource -> (Identification) resource)
468                    .findFirst()
469                    .orElseGet(event::getSource);
470            AddOnModel addonModel = getMain().getInternalIdentificationManager().getAddonModel(identification);
471            if (addonModel != null) {
472                endPermanent(addonModel);
473            }
474        }
475    }
476}