/*
    SPDX-FileCopyrightText: 2016 Chantara Tith <tith.chantara@gmail.com>
    SPDX-FileCopyrightText: 2016-2020 Andrius Štikonas <andrius@stikonas.eu>
    SPDX-FileCopyrightText: 2018 Caio Jordão Carvalho <caiojcarvalho@gmail.com>
    SPDX-FileCopyrightText: 2019 Yuri Chornoivan <yurchor@ukr.net>

    SPDX-License-Identifier: GPL-3.0-or-later
*/

#include "core/lvmdevice.h"
#include "core/partition.h"
#include "core/partitiontable.h"
#include "core/volumemanagerdevice_p.h"
#include "fs/filesystem.h"
#include "fs/lvm2_pv.h"
#include "fs/luks.h"
#include "fs/filesystemfactory.h"

#include "util/externalcommand.h"
#include "util/helpers.h"
#include "util/globallog.h"
#include "util/report.h"

#include <utility>

#include <QRegularExpression>
#include <QStorageInfo>
#include <QtMath>

#include <KLocalizedString>

#define d_ptr std::static_pointer_cast<LvmDevicePrivate>(d)

class LvmDevicePrivate : public VolumeManagerDevicePrivate
{
public:
    qint64 m_peSize;
    qint64 m_totalPE;
    qint64 m_allocPE;
    qint64 m_freePE;
    QString m_UUID;

    mutable QStringList m_LVPathList;
    QVector <const Partition*> m_PVs;
    mutable std::unique_ptr<QHash<QString, qint64>> m_LVSizeMap;
};

/** Constructs a representation of LVM device with initialized LV as Partitions
 *
 *  @param vgName Volume Group name
 *  @param iconName Icon representing LVM Volume group
 */
LvmDevice::LvmDevice(const QString& vgName, const QString& iconName)
    : VolumeManagerDevice(std::make_shared<LvmDevicePrivate>(),
                          vgName,
                          (QStringLiteral("/dev/") + vgName),
                          getPeSize(vgName),
                          getTotalPE(vgName),
                          iconName,
                          Device::Type::LVM_Device)
{
    d_ptr->m_peSize  = logicalSize();
    d_ptr->m_totalPE = totalLogical();
    d_ptr->m_freePE  = getFreePE(vgName);
    d_ptr->m_allocPE = d_ptr->m_totalPE - d_ptr->m_freePE;
    d_ptr->m_UUID    = getUUID(vgName);
    d_ptr->m_LVPathList = getLVs(vgName);
    d_ptr->m_LVSizeMap  = std::make_unique<QHash<QString, qint64>>();

    initPartitions();
}

/**
 * shared list of PV's paths that will be added to any VGs.
 * (have been added to an operation, but not yet applied)
*/
QVector<const Partition*> LvmDevice::s_DirtyPVs;


/**
 * shared list of PVs paths that are member of VGs that will be deleted soon.
 */
QVector<const Partition*> LvmDevice::s_OrphanPVs;

LvmDevice::~LvmDevice()
{
}

void LvmDevice::initPartitions()
{
    qint64 firstUsable = 0;
    qint64 lastUsable  = totalPE() - 1;
    PartitionTable* pTable = new PartitionTable(PartitionTable::vmd, firstUsable, lastUsable);

    for (const auto &p : scanPartitions(pTable)) {
        LVSizeMap()->insert(p->partitionPath(), p->length());
        pTable->append(p);
    }

    if (pTable)
        pTable->updateUnallocated(*this);
    else
        pTable = new PartitionTable(PartitionTable::vmd, firstUsable, lastUsable);

    setPartitionTable(pTable);
}

/**
 *  Scan LVM LV Partitions
 *
 *  @param pTable Virtual PartitionTable of LVM device
 *  @return an initialized Partition(LV) list
 */
const QList<Partition*> LvmDevice::scanPartitions(PartitionTable* pTable) const
{
    QList<Partition*> pList;
    for (const auto &lvPath : partitionNodes()) {
        Partition *p = scanPartition(lvPath, pTable);
        pList.append(p);
    }
    return pList;
}

/** scan and construct a partition(LV) at a given path
 *
 * NOTE:
 * LVM partition has 2 different start and end sector values
 * 1. representing the actual LV start from 0 -> size of LV - 1
 * 2. representing abstract LV's sector inside a VG partitionTable
 *    start from last sector + 1 of last Partitions -> size of LV - 1
 * Reason for this is for the LV Partition to work nicely with other parts of the codebase
 * without too many special cases.
 *
 * @param lvPath LVM Logical Volume path
 * @param pTable Abstract partition table representing partitions of LVM Volume Group
 * @return initialized Partition(LV)
 */
Partition* LvmDevice::scanPartition(const QString& lvPath, PartitionTable* pTable) const
{
    activateLV(lvPath);

    qint64 lvSize = getTotalLE(lvPath);
    qint64 startSector = mappedSector(lvPath, 0);
    qint64 endSector = startSector + lvSize - 1;

    FileSystem::Type type = FileSystem::detectFileSystem(lvPath);
    FileSystem* fs = FileSystemFactory::create(type, 0, lvSize - 1, logicalSize());
    fs->scan(lvPath);

    PartitionRole::Roles r = PartitionRole::Lvm_Lv;
    QString mountPoint;
    bool mounted;

    // Handle LUKS partition
    if (fs->type() == FileSystem::Type::Luks) {
        r |= PartitionRole::Luks;
        FS::luks* luksFs = static_cast<FS::luks*>(fs);
        luksFs->initLUKS();

        QString mapperNode = luksFs->mapperName();
        mountPoint = FileSystem::detectMountPoint(fs, mapperNode);
        mounted    = FileSystem::detectMountStatus(fs, mapperNode);
    } else {
        mountPoint = FileSystem::detectMountPoint(fs, lvPath);
        mounted = FileSystem::detectMountStatus(fs, lvPath);

        if (!mountPoint.isEmpty() && fs->type() != FileSystem::Type::LinuxSwap) {
            const QStorageInfo storage = QStorageInfo(mountPoint);
            if (logicalSize() > 0 && fs->type() != FileSystem::Type::Luks && mounted && storage.isValid())
                fs->setSectorsUsed( (storage.bytesTotal() - storage.bytesFree()) / logicalSize() );
        }
        else if (fs->supportGetUsed() == FileSystem::cmdSupportFileSystem)
            fs->setSectorsUsed(qCeil(fs->readUsedCapacity(lvPath) / static_cast<double>(logicalSize())));
   }

    if (fs->supportGetLabel() != FileSystem::cmdSupportNone) {
        fs->setLabel(fs->readLabel(lvPath));
    }
    if (fs->supportGetUUID() != FileSystem::cmdSupportNone)
        fs->setUUID(fs->readUUID(lvPath));

    Partition* part = new Partition(pTable,
                    *this,
                    PartitionRole(r),
                    fs,
                    startSector,
                    endSector,
                    lvPath,
                    PartitionTable::Flag::None,
                    mountPoint,
                    mounted);
    return part;
}

/** scan and construct list of initialized LvmDevice objects.
 *
 *  @param devices list of initialized Devices
 */
void LvmDevice::scanSystemLVM(QList<Device*>& devices)
{
    LvmDevice::s_OrphanPVs.clear();

    QList<LvmDevice*> lvmList;
    for (const auto &vgName : getVGs()) {
        lvmList.append(new LvmDevice(vgName));
    }

    // Some LVM operations require additional information about LVM physical volumes which we store in LVM::pvList::list()
    LVM::pvList::list().clear();
    LVM::pvList::list().append(FS::lvm2_pv::getPVs(devices));

    // Look for LVM physical volumes in LVM VGs
    for (const auto &d : lvmList) {
        devices.append(d);
        LVM::pvList::list().append(FS::lvm2_pv::getPVinNode(d->partitionTable()));
    }

    // Inform LvmDevice about which physical volumes form that particular LvmDevice
    for (const auto &d : lvmList)
        for (const auto &p : std::as_const(LVM::pvList::list()))
            if (p.vgName() == d->name())
                d->physicalVolumes().append(p.partition());

}

qint64 LvmDevice::mappedSector(const QString& lvPath, qint64 sector) const
{
    qint64 mSector = 0;
    QStringList lvpathList = partitionNodes();
    qint32 devIndex = lvpathList.indexOf(lvPath);

    if (devIndex) {
        for (int i = 0; i < devIndex; i++) {
            mSector += LVSizeMap()->value(lvpathList[i]);
        }
        mSector += sector;
    }
    return mSector;
}

const QStringList LvmDevice::deviceNodes() const
{
    QStringList pvList;
    for (const auto &p : physicalVolumes()) {
        if (p->roles().has(PartitionRole::Luks))
            pvList << static_cast<const FS::luks*>(&p->fileSystem())->mapperName();
        else
            pvList << p->partitionPath();
    }

    return pvList;
}

const QStringList& LvmDevice::partitionNodes() const
{
    return d_ptr->m_LVPathList;
}

qint64 LvmDevice::partitionSize(QString& partitionPath) const
{
    return LVSizeMap()->value(partitionPath);
}

const QStringList LvmDevice::getVGs()
{
    QStringList vgList;
    QString output = getField(QStringLiteral("vg_name"));
    if (!output.isEmpty()) {
        const QStringList vgNameList = output.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
        for (const auto &vgName : vgNameList) {
            vgList.append(vgName.trimmed());
        }
    }
    return vgList;
}

const QStringList LvmDevice::getLVs(const QString& vgName)
{
    QStringList lvPathList;
    QString cmdOutput = getField(QStringLiteral("lv_path"), vgName);

    if (cmdOutput.size()) {
        const QStringList tempPathList = cmdOutput.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
        for (const auto &lvPath : tempPathList) {
            lvPathList.append(lvPath.trimmed());
        }
    }
    return lvPathList;
}

qint64 LvmDevice::getPeSize(const QString& vgName)
{
    QString val = getField(QStringLiteral("vg_extent_size"), vgName);
    return val.isEmpty() ? -1 : val.toLongLong();
}

qint64 LvmDevice::getTotalPE(const QString& vgName)
{
    QString val = getField(QStringLiteral("vg_extent_count"), vgName);
    return val.isEmpty() ? -1 : val.toInt();
}

qint64 LvmDevice::getAllocatedPE(const QString& vgName)
{
    return getTotalPE(vgName) - getFreePE(vgName);
}

qint64 LvmDevice::getFreePE(const QString& vgName)
{
    QString val =  getField(QStringLiteral("vg_free_count"), vgName);
    return val.isEmpty() ? -1 : val.toInt();
}

QString LvmDevice::getUUID(const QString& vgName)
{
    QString val = getField(QStringLiteral("vg_uuid"), vgName);
    return val.isEmpty() ? QStringLiteral("---") : val;

}

/** Get LVM vgs command output with field name
 *
 * @param fieldName LVM field name
 * @param vgName the name of LVM Volume Group
 * @return raw output of command output, usually with many spaces within the returned string
 * */

QString LvmDevice::getField(const QString& fieldName, const QString& vgName)
{
    QStringList args = { QStringLiteral("vgs"),
              QStringLiteral("--foreign"),
              QStringLiteral("--readonly"),
              QStringLiteral("--noheadings"),
              QStringLiteral("--units"),
              QStringLiteral("B"),
              QStringLiteral("--nosuffix"),
              QStringLiteral("--options"),
              fieldName };
    if  (!vgName.isEmpty()) {
        args << vgName;
    }
    ExternalCommand cmd(QStringLiteral("lvm"), args, QProcess::ProcessChannelMode::SeparateChannels);
    if (cmd.run(-1) && cmd.exitCode() == 0) {
        return cmd.output().trimmed();
    }
    return QString();
}

qint64 LvmDevice::getTotalLE(const QString& lvPath)
{
    ExternalCommand cmd(QStringLiteral("lvm"),
            { QStringLiteral("lvdisplay"),
              lvPath});

    if (cmd.run(-1) && cmd.exitCode() == 0) {
        QRegularExpression re(QStringLiteral("Current LE\\h+(\\d+)"));
        QRegularExpressionMatch match = re.match(cmd.output());
        if (match.hasMatch()) {
             return  match.captured(1).toInt();
        }
    }
    Log(Log::Level::error) << xi18nc("@info:status", "An error occurred while running lvdisplay.");
    return -1;
}

bool LvmDevice::removeLV(Report& report, LvmDevice& d, Partition& p)
{
    ExternalCommand cmd(report, QStringLiteral("lvm"),
            { QStringLiteral("lvremove"),
              QStringLiteral("--yes"),
              p.partitionPath()});

    if (cmd.run(-1) && cmd.exitCode() == 0) {
        d.partitionTable()->remove(&p);
        return  true;
    }
    return false;
}

bool LvmDevice::createLV(Report& report, LvmDevice& d, Partition& p, const QString& lvName)
{
    ExternalCommand cmd(report, QStringLiteral("lvm"),
            { QStringLiteral("lvcreate"),
              QStringLiteral("--yes"),
              QStringLiteral("--extents"),
              QString::number(p.length()),
              QStringLiteral("--name"),
              lvName,
              d.name()});

    return (cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::createLVSnapshot(Report& report, Partition& p, const QString& name, const qint64 extents)
{
    QString numExtents = (extents > 0) ? QString::number(extents) :
        QString::number(p.length());
    ExternalCommand cmd(report, QStringLiteral("lvm"),
            { QStringLiteral("lvcreate"),
              QStringLiteral("--yes"),
              QStringLiteral("--extents"),
              numExtents,
              QStringLiteral("--snapshot"),
              QStringLiteral("--name"),
              name,
              p.partitionPath() });
    return (cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::resizeLV(Report& report, Partition& p)
{
    ExternalCommand cmd(report, QStringLiteral("lvm"),
            { QStringLiteral("lvresize"),
              QStringLiteral("--force"),
              QStringLiteral("--yes"),
              QStringLiteral("--extents"),
              QString::number(p.length()),
              p.partitionPath()});

    return (cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::removePV(Report& report, LvmDevice& d, const QString& pvPath)
{
    ExternalCommand cmd(report, QStringLiteral("lvm"),
            { QStringLiteral("vgreduce"),
              d.name(),
              pvPath});

    return (cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::insertPV(Report& report, LvmDevice& d, const QString& pvPath)
{
    ExternalCommand cmd(report, QStringLiteral("lvm"),
            { QStringLiteral("vgextend"),
              QStringLiteral("--yes"),
              d.name(),
              pvPath});

    return (cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::movePV(Report& report, const QString& pvPath, const QStringList& destinations)
{
    if (FS::lvm2_pv::getAllocatedPE(pvPath) <= 0)
        return true;

    QStringList args = { QStringLiteral("pvmove") };
    args << pvPath;
    if (!destinations.isEmpty())
        for (const auto &destPath : destinations)
            args << destPath.trimmed();

    ExternalCommand cmd(report, QStringLiteral("lvm"), args);
    return (cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::createVG(Report& report, const QString vgName, const QVector<const Partition*>& pvList, const qint32 peSize)
{
    QStringList args = { QStringLiteral("vgcreate"), QStringLiteral("--physicalextentsize"), QString::number(peSize) };
    args << vgName;
    for (const auto &p : pvList) {
        if (p->roles().has(PartitionRole::Luks))
            args << static_cast<const FS::luks*>(&p->fileSystem())->mapperName();
        else
            args << p->partitionPath();
    }

    ExternalCommand cmd(report, QStringLiteral("lvm"), args);

    return (cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::removeVG(Report& report, LvmDevice& d)
{
    bool deactivated = deactivateVG(report, d);
    ExternalCommand cmd(report, QStringLiteral("lvm"),
            { QStringLiteral("vgremove"),
              QStringLiteral("--force"),
              d.name() });
    return (deactivated && cmd.run(-1) && cmd.exitCode() == 0);
}

bool LvmDevice::deactivateVG(Report& report, const LvmDevice& d)
{
    ExternalCommand deactivate(report, QStringLiteral("lvm"),
            { QStringLiteral("vgchange"),
              QStringLiteral("--activate"), QStringLiteral("n"),
              d.name() });
    return deactivate.run(-1) && deactivate.exitCode() == 0;
}

bool LvmDevice::deactivateLV(Report& report, const Partition& p)
{
    ExternalCommand deactivate(report, QStringLiteral("lvm"),
            { QStringLiteral("lvchange"),
              QStringLiteral("--activate"), QStringLiteral("n"),
              p.partitionPath() });
    return deactivate.run(-1) && deactivate.exitCode() == 0;
}

bool LvmDevice::activateVG(Report& report, const LvmDevice& d)
{
    ExternalCommand deactivate(report, QStringLiteral("lvm"),
            { QStringLiteral("vgchange"),
              QStringLiteral("--activate"), QStringLiteral("y"),
              d.name() });
    return deactivate.run(-1) && deactivate.exitCode() == 0;
}

bool LvmDevice::activateLV(const QString& lvPath)
{
    ExternalCommand deactivate(QStringLiteral("lvm"),
            { QStringLiteral("lvchange"),
              QStringLiteral("--activate"), QStringLiteral("y"),
              lvPath });
    return deactivate.run(-1) && deactivate.exitCode() == 0;
}

qint64 LvmDevice::peSize() const
{
    return d_ptr->m_peSize;
}

qint64 LvmDevice::totalPE() const
{
    return d_ptr->m_totalPE;
}

qint64 LvmDevice::allocatedPE() const
{
    return d_ptr->m_allocPE;
}

qint64 LvmDevice::freePE() const
{
    return d_ptr->m_freePE;
}

void LvmDevice::setFreePE(qint64 freePE) const
{
    d_ptr->m_freePE = freePE;
    d_ptr->m_allocPE = d_ptr->m_totalPE - freePE;
}

QString LvmDevice::UUID() const
{
    return d_ptr->m_UUID;
}

QVector <const Partition*>& LvmDevice::physicalVolumes()
{
    return d_ptr->m_PVs;
}

const QVector <const Partition*>& LvmDevice::physicalVolumes() const
{
    return d_ptr->m_PVs;
}

std::unique_ptr<QHash<QString, qint64>>& LvmDevice::LVSizeMap() const
{
    return d_ptr->m_LVSizeMap;
}
