#include "engine/sync/enginesync.h"

#include <QStringList>

#include "engine/channels/enginechannel.h"
#include "engine/enginebuffer.h"
#include "engine/sync/internalclock.h"
#include "util/assert.h"
#include "util/logger.h"

namespace {
const mixxx::Logger kLogger("EngineSync");
} // namespace

EngineSync::EngineSync(UserSettingsPointer pConfig)
        : BaseSyncableListener(pConfig) {
}

EngineSync::~EngineSync() {
}

Syncable* EngineSync::pickMaster(Syncable* enabling_syncable) {
    // If there is an explicit master, and it is playing, keep it.
    if (m_pMasterSyncable && m_pMasterSyncable->getSyncMode() == SYNC_MASTER_EXPLICIT) {
        return m_pMasterSyncable;
    }

    Syncable* first_stopped_deck = nullptr;
    Syncable* first_playing_deck = nullptr;
    int stopped_deck_count = 0;
    int playing_deck_count = 0;

    for (const auto& pSyncable : qAsConst(m_syncables)) {
        if (pSyncable->getBaseBpm() <= 0.0) {
            continue;
        }

        if (pSyncable != enabling_syncable) {
            if (!pSyncable->getChannel()->isPrimaryDeck()) {
                continue;
            }
            if (!pSyncable->isSynchronized()) {
                continue;
            }
        }

        if (pSyncable->isPlaying() && pSyncable->isAudible()) {
            if (playing_deck_count == 0) {
                first_playing_deck = pSyncable;
            }
            playing_deck_count++;
        } else {
            if (stopped_deck_count == 0) {
                first_stopped_deck = pSyncable;
            }
            stopped_deck_count++;
        }
    }

    if (playing_deck_count == 1) {
        return first_playing_deck;
    } else if (playing_deck_count > 1) {
        return m_pInternalClock;
    }

    // No valid playing sync decks
    if (stopped_deck_count == 1) {
        return first_stopped_deck;
    } else if (stopped_deck_count > 1) {
        return m_pInternalClock;
    }

    // No valid stopped sync decks
    return nullptr;
}

void EngineSync::requestSyncMode(Syncable* pSyncable, SyncMode mode) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::requestSyncMode" << pSyncable->getGroup() << mode;
    }
    // Based on the call hierarchy I don't think this is possible. (Famous last words.)
    VERIFY_OR_DEBUG_ASSERT(pSyncable) {
        return;
    }

    if (mode == SYNC_MASTER_EXPLICIT) {
        // Note: we enable master unconditionally. If it has no valid
        // tempo, the tempo of the old master remains until we know better
        activateMaster(pSyncable, true);
        if (pSyncable->getBaseBpm() > 0) {
            setMasterParams(pSyncable, pSyncable->getBeatDistance(),
                            pSyncable->getBaseBpm(), pSyncable->getBpm());
        }
    } else if (mode == SYNC_FOLLOWER ||
            mode == SYNC_MASTER_SOFT ||
            pSyncable == m_pInternalClock) {
        // Note: SYNC_MASTER_SOFT and SYNC_FOLLOWER cannot be set explicitly,
        // they are calculated by pickMaster.
        // Internal clock cannot be disabled, it is always listening
        if (m_pMasterSyncable == pSyncable) {
            // This Syncable was master before. Hand off.
            m_pMasterSyncable = nullptr;
            pSyncable->setSyncMode(SYNC_FOLLOWER);
        }
        Syncable* newMaster = pickMaster(pSyncable);
        if (newMaster) {
            activateMaster(newMaster, false);
        }
        if (pSyncable != newMaster) {
            activateFollower(pSyncable);
        }
    } else {
        deactivateSync(pSyncable);
    }
    checkUniquePlayingSyncable();
}

Syncable* EngineSync::findBpmMatchTarget(Syncable* requester) {
    Syncable* pStoppedTarget = nullptr;

    for (const auto& pOtherSyncable : qAsConst(m_syncables)) {
        if (pOtherSyncable == requester) {
            continue;
        }
        // Skip non-master decks, like preview decks.
        if (!pOtherSyncable->getChannel()->isMasterEnabled()) {
            continue;
        }
        if (!pOtherSyncable->getChannel()->isPrimaryDeck()) {
            continue;
        }
        if (pOtherSyncable->getBaseBpm() == 0.0) {
            continue;
        }

        // If the other deck is playing we stop looking immediately. Otherwise continue looking
        // for a playing deck with bpm > 0.0.
        if (pOtherSyncable->isPlaying() && pOtherSyncable->isAudible()) {
            return pOtherSyncable;
        }

        // The target is not playing. If this is the first one we have seen,
        // record it. If we never find a playing target, we'll return
        // this one as a fallback.
        if (!pStoppedTarget && !requester->isPlaying()) {
            pStoppedTarget = pOtherSyncable;
        }
    }

    return pStoppedTarget;
}

void EngineSync::requestEnableSync(Syncable* pSyncable, bool bEnabled) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::requestEnableSync " << pSyncable->getGroup() << bEnabled;
    }
    // Sync disable request, hand off to a different function
    if (!bEnabled) {
        // Already disabled?  Do nothing.
        if (!pSyncable->isSynchronized()) {
            return;
        }
        deactivateSync(pSyncable);
        return;
    }

    // Already enabled?  Do nothing.
    if (pSyncable->isSynchronized()) {
        return;
    }

    // Now go through and possible pick a new master deck if we can find one.
    Syncable* newMaster = pickMaster(pSyncable);

    // The syncable that will be used to initialize the master params, if needed
    Syncable* pParamsSyncable = nullptr;

    if (newMaster == m_pInternalClock) {
        // This happens if we had a single master before
        if (m_pMasterSyncable != m_pInternalClock) {
            if (pSyncable->isPlaying()) {
                // We are already playing, only change speed if
                // the old master is playing as well
                if (m_pMasterSyncable && m_pMasterSyncable->isPlaying()) {
                    // Adopt value from the old master if it is still playing
                    pParamsSyncable = m_pMasterSyncable;
                } else {
                    DEBUG_ASSERT(pSyncable->getBaseBpm() > 0);
                    pParamsSyncable = pSyncable;
                }
            } else {
                if (m_pMasterSyncable) {
                    pParamsSyncable = m_pMasterSyncable;
                } else {
                    // Can this ever happen?
                    DEBUG_ASSERT(false);
                    pParamsSyncable = pSyncable;
                }
            }
        }
    } else if (newMaster == pSyncable) {
        // There is no other synced deck. If any other deck is playing we will
        // match the first available bpm -- sync won't be enabled on these decks,
        // otherwise there would have been a master.
        pParamsSyncable = findBpmMatchTarget(pSyncable);
        if (pParamsSyncable == nullptr) {
            // We weren't able to find anything to match to, so set ourselves as the
            // target.  That way we'll use our own params when we setMasterParams below.
            pParamsSyncable = pSyncable;
        }
    } else if (newMaster) {
        // This happens if this Deck has no valid BPM
        // avoid that other decks are adjusted
        pParamsSyncable = newMaster;
    } else {
        // This happens if there is no other SyncDeck around and this has no valid BPM
        // nothing to do.
    }

    if (newMaster != nullptr && newMaster != m_pMasterSyncable) {
        activateMaster(newMaster, false);
    }

    if (newMaster != pSyncable) {
        pSyncable->setSyncMode(SYNC_FOLLOWER);
    }

    if (pParamsSyncable != nullptr) {
        setMasterParams(pParamsSyncable,
                pParamsSyncable->getBeatDistance(),
                pParamsSyncable->getBaseBpm(),
                pParamsSyncable->getBpm());
        pSyncable->setInstantaneousBpm(pParamsSyncable->getBpm());
        if (pParamsSyncable != pSyncable) {
            pSyncable->requestSync();
        }
    }
}

void EngineSync::notifyPlayingAudible(Syncable* pSyncable, bool playingAudible) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::notifyPlayingAudible"
                        << pSyncable->getGroup() << playingAudible;
    }
    // For now we don't care if the deck is now playing or stopping.
    if (!pSyncable->isSynchronized()) {
        return;
    }

    // similar to enablesync -- we pick a new master and maybe reinit.
    Syncable* newMaster = pickMaster(playingAudible ? pSyncable : nullptr);

    if (newMaster != nullptr && newMaster != m_pMasterSyncable) {
        activateMaster(newMaster, false);
        setMasterParams(newMaster, newMaster->getBeatDistance(), newMaster->getBaseBpm(), newMaster->getBpm());
    }
}

void EngineSync::notifyScratching(Syncable* pSyncable, bool scratching) {
    // No special behavior for now.
    Q_UNUSED(pSyncable);
    Q_UNUSED(scratching);
}

void EngineSync::notifyBpmChanged(Syncable* pSyncable, double bpm) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::notifyBpmChanged" << pSyncable->getGroup() << bpm;
    }

    // Master Base BPM shouldn't be updated for every random deck that twiddles
    // the rate.
    setMasterBpm(pSyncable, bpm);
}

void EngineSync::requestBpmUpdate(Syncable* pSyncable, double bpm) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::requestBpmUpdate" << pSyncable->getGroup() << bpm;
    }

    double mbaseBpm = 0.0;
    double mbpm = 0.0;
    double beatDistance = 0.0;
    if (m_pMasterSyncable) {
        mbaseBpm = m_pMasterSyncable->getBaseBpm();
        mbpm = m_pMasterSyncable->getBpm();
        beatDistance = m_pMasterSyncable->getBeatDistance();
    }

    if (mbaseBpm != 0.0 && mbpm != 0.0) {
        // resync to current master
        pSyncable->setMasterParams(beatDistance, mbaseBpm, mbpm);
    } else {
        // There is no other master, adopt this bpm as master
        pSyncable->setMasterParams(0.0, 0.0, bpm);
    }
}

void EngineSync::notifyInstantaneousBpmChanged(Syncable* pSyncable, double bpm) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::notifyInstantaneousBpmChanged" << pSyncable->getGroup() << bpm;
    }
    if (!isMaster(pSyncable->getSyncMode())) {
        return;
    }

    // Do not update the master rate slider because instantaneous changes are
    // not user visible.
    setMasterInstantaneousBpm(pSyncable, bpm);
}

void EngineSync::notifyBeatDistanceChanged(Syncable* pSyncable, double beat_distance) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::notifyBeatDistanceChanged" << pSyncable->getGroup() << beat_distance;
    }
    if (!isMaster(pSyncable->getSyncMode())) {
        return;
    }

    setMasterBeatDistance(pSyncable, beat_distance);
}

void EngineSync::activateFollower(Syncable* pSyncable) {
    if (pSyncable == nullptr) {
        qWarning() << "WARNING: Logic Error: Called activateFollower on a nullptr Syncable.";
        return;
    }

    pSyncable->setSyncMode(SYNC_FOLLOWER);
    pSyncable->setMasterParams(masterBeatDistance(), masterBaseBpm(), masterBpm());
    pSyncable->setInstantaneousBpm(masterBpm());
}

void EngineSync::activateMaster(Syncable* pSyncable, bool explicitMaster) {
    VERIFY_OR_DEBUG_ASSERT(pSyncable) {
        qWarning() << "WARNING: Logic Error: Called activateMaster on a nullptr Syncable.";
        return;
    }
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::activateMaster: "
                        << pSyncable->getGroup() << "explicit? "
                        << explicitMaster;
    }

    if (m_pMasterSyncable == pSyncable) {
        // Already master, update the explicit State.
        if (explicitMaster) {
            if (m_pMasterSyncable->getSyncMode() != SYNC_MASTER_EXPLICIT) {
                m_pMasterSyncable->setSyncMode(SYNC_MASTER_EXPLICIT);
            } else if (m_pMasterSyncable->getSyncMode() != SYNC_MASTER_SOFT) {
                m_pMasterSyncable->setSyncMode(SYNC_MASTER_SOFT);
            } else {
                DEBUG_ASSERT(!"Logic Error: m_pMasterSyncable is a syncable that does not think it is master.");
            }
        }
        // nothing else to do
        return;
    }

    // If a channel is master, disable it.
    Syncable* pOldChannelMaster = m_pMasterSyncable;

    m_pMasterSyncable = nullptr;
    if (pOldChannelMaster) {
        pOldChannelMaster->setSyncMode(SYNC_FOLLOWER);
    }

    //qDebug() << "Setting up master " << pSyncable->getGroup();
    m_pMasterSyncable = pSyncable;
    if (explicitMaster) {
        pSyncable->setSyncMode(SYNC_MASTER_EXPLICIT);
    } else {
        pSyncable->setSyncMode(SYNC_MASTER_SOFT);
    }
    pSyncable->setMasterParams(masterBeatDistance(), masterBaseBpm(), masterBpm());
    pSyncable->setInstantaneousBpm(masterBpm());

    if (m_pMasterSyncable != m_pInternalClock) {
        activateFollower(m_pInternalClock);
    }

    // It is up to callers of this function to initialize bpm and beat_distance
    // if necessary.
}

void EngineSync::deactivateSync(Syncable* pSyncable) {
    if (kLogger.traceEnabled()) {
        kLogger.trace() << "EngineSync::deactivateSync" << pSyncable->getGroup();
    }
    bool wasMaster = isMaster(pSyncable->getSyncMode());
    if (wasMaster) {
        m_pMasterSyncable = nullptr;
    }

    // Notifications happen after-the-fact.
    pSyncable->setSyncMode(SYNC_NONE);

    bool bSyncDeckExists = syncDeckExists();

    if (pSyncable != m_pInternalClock && !bSyncDeckExists) {
        // Deactivate the internal clock if there are no more sync decks left.
        m_pMasterSyncable = nullptr;
        m_pInternalClock->setSyncMode(SYNC_NONE);
    }

    Syncable* newMaster = pickMaster(nullptr);
    if (newMaster != nullptr && m_pMasterSyncable != newMaster) {
        activateMaster(newMaster, false);
    }
}

Syncable* EngineSync::pickNonSyncSyncTarget(EngineChannel* pDontPick) const {
    // First choice: the sync master, if it's a deck
    if (m_pMasterSyncable &&
            m_pMasterSyncable->getChannel() &&
            m_pMasterSyncable->getChannel() != pDontPick) {
        return m_pMasterSyncable;
    }

    Syncable* pFirstPlayingDeck = nullptr;
    Syncable* pFirstNonplayingDeck = nullptr;
    foreach (Syncable* pSyncable, m_syncables) {
        EngineChannel* pChannel = pSyncable->getChannel();
        // Exclude non-decks
        if (pChannel == nullptr || pChannel == pDontPick) {
            continue;
        }

        // Only consider channels that have a track loaded, are in the master
        // mix, and are primary decks.
        if (pChannel->isActive() && pChannel->isMasterEnabled() && pChannel->isPrimaryDeck()) {
            EngineBuffer* pBuffer = pChannel->getEngineBuffer();
            if (pBuffer && pBuffer->getBpm() > 0) {
                if (pBuffer->getSpeed() != 0.0) {
                    if (pSyncable->getSyncMode() != SYNC_NONE) {
                        // Second choice: first playing sync deck
                        return pSyncable;
                    }
                    if (pFirstPlayingDeck == nullptr) {
                        pFirstPlayingDeck = pSyncable;
                    }
                } else if (pFirstNonplayingDeck == nullptr) {
                    pFirstNonplayingDeck = pSyncable;
                }
            }
        }
    }
    if (pFirstPlayingDeck) {
        // Third choice: first playing non-sync deck
        return pFirstPlayingDeck;
    }

    // No playing decks have a BPM. Go with the first deck that was stopped but
    // had a BPM.
    return pFirstNonplayingDeck;
}

bool EngineSync::otherSyncedPlaying(const QString& group) {
    bool othersInSync = false;
    for (Syncable* theSyncable : qAsConst(m_syncables)) {
        bool isSynchonized = theSyncable->isSynchronized();
        if (theSyncable->getGroup() == group) {
            if (!isSynchonized) {
                return false;
            }
            continue;
        }
        if (theSyncable->isPlaying() && isSynchonized) {
            othersInSync = true;
        }
    }
    return othersInSync;
}
