/*
 * SPDX-License-Identifier: GPL-3.0-only
 * MuseScore-CLA-applies
 *
 * MuseScore
 * Music Composition & Notation
 *
 * Copyright (C) 2021 MuseScore Limited and others
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "shortcutsregister.h"

#include <QKeySequence>

#include "actions/actiontypes.h"
#include "global/io/buffer.h"
#include "global/io/file.h"
#include "global/serialization/xmlstreamreader.h"
#include "global/serialization/xmlstreamwriter.h"

#include "multiwindows/resourcelockguard.h"

#include "log.h"

using namespace muse;
using namespace muse::shortcuts;
using namespace muse::async;

static constexpr std::string_view SHORTCUTS_TAG("Shortcuts");
static constexpr std::string_view SHORTCUT_TAG("SC");
static constexpr std::string_view ACTION_CODE_TAG("key");
static constexpr std::string_view STANDARD_KEY_TAG("std");
static constexpr std::string_view SEQUENCE_TAG("seq");
static constexpr std::string_view AUTOREPEAT_TAG("autorepeat");

static const std::string SHORTCUTS_RESOURCE_NAME("SHORTCUTS");

static const std::map<QKeySequence::StandardKey, QKeyCombination> SHORTCUTS_EXPAND_IGNORE_MAP = {
    { QKeySequence::StandardKey::HelpContents, Qt::Key_Help },
    { QKeySequence::StandardKey::Open, Qt::Key_Open },
    { QKeySequence::StandardKey::Close, Qt::Key_Close },
    { QKeySequence::StandardKey::Save, Qt::Key_Save },
    { QKeySequence::StandardKey::New, Qt::Key_New },
    { QKeySequence::StandardKey::Cut, Qt::Key_Cut },
    { QKeySequence::StandardKey::Copy, Qt::Key_Copy },
    { QKeySequence::StandardKey::Paste, Qt::Key_Paste },
    { QKeySequence::StandardKey::Undo, Qt::Key_Undo },
    { QKeySequence::StandardKey::Redo, Qt::Key_Redo },
    { QKeySequence::StandardKey::Forward, Qt::Key_Forward },
    { QKeySequence::StandardKey::Refresh, Qt::Key_Refresh },
    { QKeySequence::StandardKey::ZoomIn, Qt::Key_ZoomIn },
    { QKeySequence::StandardKey::ZoomOut, Qt::Key_ZoomOut },
    { QKeySequence::StandardKey::Find, Qt::Key_Find },
    { QKeySequence::StandardKey::SaveAs, (Qt::SHIFT | Qt::Key_Save) },
    { QKeySequence::StandardKey::Preferences, Qt::Key_Settings },
    { QKeySequence::StandardKey::Quit, Qt::Key_Exit },
    { QKeySequence::StandardKey::Cancel, Qt::Key_Cancel }
};

static const Shortcut& findShortcut(const ShortcutList& shortcuts, const std::string& actionCode)
{
    for (const Shortcut& shortcut : shortcuts) {
        if (shortcut.action == actionCode) {
            return shortcut;
        }
    }

    static Shortcut null;
    return null;
}

void ShortcutsRegister::init()
{
    multiwindowsProvider()->resourceChanged().onReceive(this, [this](const std::string& resourceName) {
        if (resourceName == SHORTCUTS_RESOURCE_NAME) {
            reload();
        }
    });

    reload();
}

void ShortcutsRegister::reload(bool onlyDef)
{
    TRACEFUNC;

    m_shortcuts.clear();
    m_defaultShortcuts.clear();

    io::path_t defPath = configuration()->shortcutsAppDataPath();
    io::path_t userPath = configuration()->shortcutsUserAppDataPath();

    bool ok = readFromFile(m_defaultShortcuts, defPath);

    if (ok) {
        expandStandardKeys(m_defaultShortcuts);

        if (!onlyDef) {
            //! NOTE The user shortcut file may change, so we need to lock it
            mi::ReadResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME);
            ok = readFromFile(m_shortcuts, userPath);
        } else {
            ok = false;
        }

        if (!ok) {
            m_shortcuts = m_defaultShortcuts;
        } else {
            mergeShortcuts(m_shortcuts, m_defaultShortcuts);
            mergeAdditionalShortcuts(m_shortcuts);
        }

        ok = true;
    }

    if (ok) {
        expandStandardKeys(m_shortcuts);
        makeUnique(m_shortcuts);
        m_shortcutsChanged.notify();
    }
}

void ShortcutsRegister::mergeShortcuts(ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const
{
    TRACEFUNC;

    ShortcutList needadd;
    for (const Shortcut& defSc : defaultShortcuts) {
        Shortcut scForAdd = defSc;
        bool found = false;

        for (Shortcut& sc : shortcuts) {
            //! NOTE If a user shortcut is found, set context & auto repeat (always use default values)
            if (sc.action == defSc.action) {
                sc.context = defSc.context;
                sc.autoRepeat = defSc.autoRepeat;
                found = true;
            } else if (sc.context == defSc.context) {
                for (const std::string& seq : sc.sequences) {
                    //! NOTE If user shortcut has sequence from default shortcut, remove the sequence from default shortcut
                    muse::remove_if(scForAdd.sequences, [&seq](const std::string& cmp){
                        return cmp == seq;
                    });
                }
            }
        }

        //! NOTE If no default shortcut is found in user shortcuts add def
        if (!found) {
            needadd.push_back(scForAdd);
        }
    }

    if (!needadd.empty()) {
        shortcuts.splice(shortcuts.end(), needadd);
    }
}

void ShortcutsRegister::mergeAdditionalShortcuts(ShortcutList& shortcuts)
{
    for (const auto& [context, additionalShortcuts] : m_additionalShortcutsMap) {
        mergeShortcuts(shortcuts, additionalShortcuts);
    }
}

void ShortcutsRegister::makeUnique(ShortcutList& shortcuts)
{
    TRACEFUNC;

    const ShortcutList all = shortcuts;

    shortcuts.clear();

    for (const Shortcut& sc : all) {
        const std::string& action = sc.action;

        auto it = std::find_if(shortcuts.begin(), shortcuts.end(), [action](const Shortcut& s) {
            return s.action == action;
        });

        if (it == shortcuts.end()) {
            shortcuts.push_back(sc);
            continue;
        }

        Shortcut& foundSc = *it;

        IF_ASSERT_FAILED(foundSc.context == sc.context) {
        }

        foundSc.sequences.insert(foundSc.sequences.end(), sc.sequences.begin(), sc.sequences.end());
    }
}

void ShortcutsRegister::expandStandardKeys(ShortcutList& shortcuts) const
{
    TRACEFUNC;

    ShortcutList expanded;
    ShortcutList notbonded;

    for (Shortcut& shortcut : shortcuts) {
        QKeySequence ignoredSeq = QKeySequence(muse::value(SHORTCUTS_EXPAND_IGNORE_MAP, shortcut.standardKey, Qt::Key_unknown));

        if (!shortcut.sequences.empty()) {
            std::string ignoredSeqStr = ignoredSeq.toString().toStdString();
            muse::remove_if(shortcut.sequences, [&ignoredSeqStr](const std::string& seq) {
                return seq == ignoredSeqStr;
            });

            continue;
        }

        QList<QKeySequence> kslist = QKeySequence::keyBindings(shortcut.standardKey);
        if (kslist.isEmpty()) {
            notbonded.push_back(shortcut);
            continue;
        }

        const QKeySequence& first = kslist.first();
        shortcut.sequences.push_back(first.toString().toStdString());
        //LOGD() << "for standard key: " << sc.standardKey << ", sequence: " << sc.sequence;

        //! NOTE If the keyBindings contains more than one result,
        //! these can be considered alternative shortcuts on the same platform for the given key.
        for (int i = 1; i < kslist.count(); ++i) {
            const QKeySequence& seq = kslist.at(i);
            if (seq == ignoredSeq) {
                continue;
            }

            Shortcut esc = shortcut;
            esc.sequences = { seq.toString().toStdString() };
            //LOGD() << "for standard key: " << esc.standardKey << ", alternative sequence: " << esc.sequence;
            expanded.push_back(esc);
        }
    }

    if (!notbonded.empty()) {
        LOGD() << "removed " << notbonded.size() << " shortcut, because they are not bound to standard key";
        for (const Shortcut& sc : notbonded) {
            shortcuts.remove(sc);
        }
    }

    if (!expanded.empty()) {
        LOGD() << "added " << expanded.size() << " shortcut, because they are alternative shortcuts for the given standard keys";

        shortcuts.splice(shortcuts.end(), expanded);
    }
}

ShortcutList ShortcutsRegister::filterAndUpdateAdditionalShortcuts(const ShortcutList& shortcuts)
{
    ShortcutList noAdditionalShortcuts = shortcuts;

    for (auto& [context, additionalShortcuts] : m_additionalShortcutsMap) {
        for (Shortcut& shortcut : additionalShortcuts) {
            auto it = std::find(shortcuts.begin(), shortcuts.end(), shortcut.action);
            if (it != shortcuts.end()) {
                shortcut = *it;
                noAdditionalShortcuts.remove(shortcut);
            }
        }
    }

    return noAdditionalShortcuts;
}

bool ShortcutsRegister::readFromFile(ShortcutList& shortcuts, const io::path_t& path) const
{
    TRACEFUNC;

    io::File file(path);
    if (!file.open(io::IODevice::ReadOnly)) {
        LOGD() << "failed to open shortcuts file: " << file.error();
        return false;
    }

    XmlStreamReader reader(&file);

    reader.readNextStartElement();
    if (reader.name() != SHORTCUTS_TAG) {
        return false;
    }

    while (reader.readNextStartElement()) {
        if (reader.name() != SHORTCUT_TAG) {
            reader.skipCurrentElement();
            continue;
        }

        Shortcut shortcut = readShortcut(reader);
        if (shortcut.isValid()) {
            shortcuts.push_back(shortcut);
        }
    }

    if (reader.isError()) {
        LOGE() << "failed parse xml, error: " << reader.error() << ", path: " << path;
        return false;
    }

    return true;
}

Shortcut ShortcutsRegister::readShortcut(XmlStreamReader& reader) const
{
    Shortcut shortcut;

    while (reader.readNextStartElement()) {
        const std::string tag(reader.name());

        if (tag == ACTION_CODE_TAG) {
            shortcut.action = reader.readAsciiText();
        } else if (tag == STANDARD_KEY_TAG) {
            shortcut.standardKey = QKeySequence::StandardKey(reader.readInt());
        } else if (tag == SEQUENCE_TAG) {
            shortcut.sequences.emplace_back(reader.readAsciiText());
        } else if (tag == AUTOREPEAT_TAG) {
            shortcut.autoRepeat = reader.readInt();
        } else {
            reader.skipCurrentElement();
        }
    }

    auto action = uiactionsRegister()->action(shortcut.action);
    if (!action.isValid()) {
        LOGW() << "ignoring shortcut for invalid action: " << shortcut.action;
        return Shortcut();
    }

    shortcut.context = action.scCtx;

    return shortcut;
}

const ShortcutList& ShortcutsRegister::shortcuts() const
{
    return m_shortcuts;
}

Ret ShortcutsRegister::setShortcuts(const ShortcutList& shortcuts)
{
    TRACEFUNC;

    if (shortcuts == m_shortcuts) {
        return true;
    }

    ShortcutList needToWrite = filterAndUpdateAdditionalShortcuts(shortcuts);

    bool ok = writeToFile(needToWrite, configuration()->shortcutsUserAppDataPath());

    if (ok) {
        m_shortcuts = needToWrite;
        mergeShortcuts(m_shortcuts, m_defaultShortcuts);
        mergeAdditionalShortcuts(m_shortcuts);
        m_shortcutsChanged.notify();
    }

    return ok;
}

void ShortcutsRegister::resetShortcuts()
{
    {
        mi::WriteResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME);
        fileSystem()->remove(configuration()->shortcutsUserAppDataPath());
    }

    reload();
}

bool ShortcutsRegister::writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const
{
    TRACEFUNC;

    ByteArray data;
    io::Buffer buf(&data);
    if (!buf.open(io::IODevice::WriteOnly)) {
        LOGE() << buf.errorString();
        return false;
    }

    XmlStreamWriter writer(&buf);
    writer.startDocument();
    writer.startElement(SHORTCUTS_TAG);

    for (const Shortcut& shortcut : shortcuts) {
        writeShortcut(writer, shortcut);
    }

    writer.endElement();
    writer.flush();

    Ret ret;
    {
        mi::WriteResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME);
        ret = fileSystem()->writeFile(path, data);
    }

    if (!ret) {
        LOGE() << ret.toString();
    }

    return ret;
}

void ShortcutsRegister::writeShortcut(XmlStreamWriter& writer, const Shortcut& shortcut) const
{
    writer.startElement(SHORTCUT_TAG);
    writer.element(ACTION_CODE_TAG, shortcut.action);

    if (shortcut.standardKey != QKeySequence::UnknownKey) {
        writer.element(STANDARD_KEY_TAG, QString("%1").arg(shortcut.standardKey).toStdString());
    }

    for (const std::string& seq : shortcut.sequences) {
        writer.element(SEQUENCE_TAG, seq);
    }

    writer.endElement();
}

Notification ShortcutsRegister::shortcutsChanged() const
{
    return m_shortcutsChanged;
}

Ret ShortcutsRegister::setAdditionalShortcuts(const std::string& context, const ShortcutList& shortcuts)
{
    m_additionalShortcutsMap[context] = shortcuts;

    mergeShortcuts(m_shortcuts, m_additionalShortcutsMap[context]);
    m_shortcutsChanged.notify();

    return muse::make_ok();
}

const Shortcut& ShortcutsRegister::shortcut(const std::string& actionCode) const
{
    const Shortcut& sh = findShortcut(m_shortcuts, actionCode);
    if (sh.isValid()) {
        return sh;
    }

    const actions::ActionCode& parentCode = uiactionsRegister()->parentActionCode(actionCode);
    return findShortcut(m_shortcuts, parentCode);
}

const Shortcut& ShortcutsRegister::defaultShortcut(const std::string& actionCode) const
{
    const Shortcut& sh = findShortcut(m_defaultShortcuts, actionCode);
    if (sh.isValid()) {
        return sh;
    }

    const actions::ActionCode& parentCode = uiactionsRegister()->parentActionCode(actionCode);
    return findShortcut(m_defaultShortcuts, parentCode);
}

bool ShortcutsRegister::isRegistered(const std::string& sequence) const
{
    for (const Shortcut& sh : m_shortcuts) {
        auto it = std::find(sh.sequences.cbegin(), sh.sequences.cend(), sequence);
        if (it != sh.sequences.cend()) {
            return true;
        }
    }
    return false;
}

ShortcutList ShortcutsRegister::shortcutsForSequence(const std::string& sequence) const
{
    ShortcutList list;
    for (const Shortcut& sh : m_shortcuts) {
        auto it = std::find(sh.sequences.cbegin(), sh.sequences.cend(), sequence);
        if (it != sh.sequences.cend()) {
            list.push_back(sh);
        }
    }
    return list;
}

Ret ShortcutsRegister::importFromFile(const io::path_t& filePath)
{
    {
        mi::ReadResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME);
        Ret ret = fileSystem()->copy(filePath, configuration()->shortcutsUserAppDataPath(), true);
        if (!ret) {
            LOGE() << "failed import file: " << ret.toString();
            return ret;
        }
    }

    reload();

    return make_ret(Ret::Code::Ok);
}

Ret ShortcutsRegister::exportToFile(const io::path_t& filePath) const
{
    return writeToFile(m_shortcuts, filePath);
}

bool ShortcutsRegister::active()
{
    return m_isActive;
}

void ShortcutsRegister::setActive(bool active)
{
    if (m_isActive == active) {
        return;
    }

    m_isActive = active;
    m_activeChanged.notify();
}

Notification ShortcutsRegister::activeChanged() const
{
    return m_activeChanged;
}
