root/src/apps/haikudepot/model/Model.cpp
/*
 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
 * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
 * Copyright 2016-2026, Andrew Lindesay <apl@lindesay.co.nz>.
 * All rights reserved. Distributed under the terms of the MIT License.
 */
#include "Model.h"

#include <algorithm>
#include <ctime>

#include <stdarg.h>
#include <stdint.h>
#include <time.h>

#include <Autolock.h>
#include <Catalog.h>
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <KeyStore.h>
#include <Locale.h>
#include <LocaleRoster.h>
#include <Message.h>
#include <Path.h>

#include "HaikuDepotConstants.h"
#include "LocaleUtils.h"
#include "Logger.h"
#include "PackageUtils.h"
#include "StorageUtils.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Model"


static const uint32 kChangeMaskAll = UINT32_MAX;
        // covers all of the possible values.


ModelListener::~ModelListener()
{
}


// #pragma mark - PackagesSummary


PackagesSummary::PackagesSummary()
        :
        fAnyProminentPackages(false),
        fAnyNativeDesktop(false),
        fAnyDesktop(false)
{
}


PackagesSummary::PackagesSummary(bool anyProminentPackages, bool anyNativeDesktop, bool anyDesktop)
        :
        fAnyProminentPackages(anyProminentPackages),
        fAnyNativeDesktop(anyNativeDesktop),
        fAnyDesktop(anyDesktop)
{
}


PackagesSummary::~PackagesSummary()
{
}


PackagesSummary&
PackagesSummary::operator=(const PackagesSummary& other)
{
        fAnyProminentPackages = other.fAnyProminentPackages;
        fAnyNativeDesktop = other.fAnyNativeDesktop;
        fAnyDesktop = other.fAnyDesktop;
        return *this;
}


bool
PackagesSummary::HasAnyProminentPackages() const
{
        return fAnyProminentPackages;
}


bool
PackagesSummary::HasAnyNativeDesktop() const
{
        return fAnyNativeDesktop;
}


bool
PackagesSummary::HasAnyDesktop() const
{
        return fAnyDesktop;
}


// #pragma mark - Model


Model::Model()
        :
        fPreferredLanguage(LanguageRef(new Language(LANGUAGE_DEFAULT_ID, "English", true), true)),
        fDepots(),
        fCategories(),
        fPackageListViewMode(PROMINENT),
        fCanShareAnonymousUsageData(false),
        fWebApp(WebAppInterfaceRef(new WebAppInterface(UserCredentials()), true)),
        fLanguages(LocaleUtils::WellKnownLanguages())
{
        fIconRepository = PackageIconRepositoryRef(new PackageIconDefaultRepository(), true);

        fFilterSpecification = PackageFilterSpecificationBuilder().BuildRef();
        fFilter = PackageFilterFactory::CreateFilter(fFilterSpecification);

        fPackageScreenshotRepository
                = new PackageScreenshotRepository(PackageScreenshotRepositoryListenerRef(this), this);
}


Model::~Model()
{
        delete fPackageScreenshotRepository;
}


const std::vector<LanguageRef>
Model::Languages() const
{
        BAutolock locker(&fLock);
        return fLanguages;
}


void
Model::SetLanguagesAndPreferred(const std::vector<LanguageRef>& value, const LanguageRef& preferred)
{
        if (value.empty())
                HDFATAL("unable to set the languages to an empty list");

        if (std::find(value.begin(), value.end(), preferred) == value.end())
                HDFATAL("the preferred language is not in the list of languages");

        BAutolock locker(&fLock);
        fLanguages = value;
        std::sort(fLanguages.begin(), fLanguages.end(), IsLanguageRefLess);
        SetPreferredLanguage(preferred);

        HDINFO("did configure %" B_PRIu32 " languages with default [%s]",
                static_cast<uint32>(fLanguages.size()), preferred->ID());
}


void
Model::SetFilterSpecification(const PackageFilterSpecificationRef& value)
{
        BAutolock locker(&fLock);
        if (value != fFilterSpecification) {
                fFilterSpecification = value;
                fFilter = PackageFilterFactory::CreateFilter(value);
                _NotifyPackageFilterChanged();
        }
}


const PackageFilterSpecificationRef
Model::FilterSpecification() const
{
        BAutolock locker(&fLock);
        return fFilterSpecification;
}


PackageFilterRef
Model::Filter() const
{
        BAutolock locker(&fLock);
        return fFilter;
}


PackageIconRepositoryRef
Model::IconRepository()
{
        BAutolock locker(&fLock);
        return fIconRepository;
}


void
Model::SetIconRepository(PackageIconRepositoryRef value)
{
        BAutolock locker(&fLock);
        fIconRepository = value;
        _NotifyIconsChanged();
}


void
Model::_NotifyIconsChanged()
{
        std::vector<ModelListenerRef>::const_iterator it;
        for (it = fListeners.begin(); it != fListeners.end(); it++) {
                const ModelListenerRef& listener = *it;
                if (listener.IsSet())
                        listener->IconsChanged();
        }
}


PackageScreenshotRepository*
Model::GetPackageScreenshotRepository()
{
        return fPackageScreenshotRepository;
}


void
Model::AddListener(const ModelListenerRef& listener)
{
        fListeners.push_back(listener);
}


void
Model::AddPackageListener(const PackageInfoListenerRef& packageListener)
{
        fPackageListeners.push_back(packageListener);
}


const LanguageRef
Model::PreferredLanguage() const
{
        BAutolock locker(&fLock);
        return fPreferredLanguage;
}


void
Model::SetPreferredLanguage(const LanguageRef& value)
{
        if (value.IsSet()) {
                BAutolock locker(&fLock);
                fPreferredLanguage = value;
        }
}


void
Model::SetDepots(const DepotInfoRef& depot)
{
        BAutolock locker(&fLock);
        std::vector<DepotInfoRef> depots;
        depots.push_back(depot);
        SetDepots(depots);
}


void
Model::SetDepots(const std::vector<DepotInfoRef>& depots)
{
        BAutolock locker(&fLock);
        fDepots.clear();

        std::vector<DepotInfoRef>::const_iterator it;

        for (it = depots.begin(); it != depots.end(); it++) {
                DepotInfoRef depot = *it;

                if (!depot.IsSet()) {
                        HDFATAL("attempt to add an unset depot");
                } else {
                        if (depot->Name().IsEmpty())
                                HDFATAL("depot has no name");

                        if (depot->Identifier().IsEmpty())
                                HDFATAL("depot has no identifier");

                        fDepots[depot->Name()] = depot;
                }
        }
}


const std::vector<DepotInfoRef>
Model::Depots() const
{
        BAutolock locker(&fLock);
        std::vector<DepotInfoRef> result;
        std::map<BString, DepotInfoRef>::const_iterator it;

        for (it = fDepots.begin(); it != fDepots.end(); it++)
                result.push_back(it->second);

        return result;
}


const DepotInfoRef
Model::DepotForName(const BString& name) const
{
        BAutolock locker(&fLock);
        std::map<BString, DepotInfoRef>::const_iterator it = fDepots.find(name);
        if (it != fDepots.end())
                return it->second;
        return DepotInfoRef();
}


const DepotInfoRef
Model::DepotForIdentifier(const BString& identifier) const
{
        BAutolock locker(&fLock);
        std::map<BString, DepotInfoRef>::const_iterator it;

        for (it = fDepots.begin(); it != fDepots.end(); it++) {
                if (it->second->Identifier() == identifier)
                        return it->second;
        }

        return DepotInfoRef();
}


const std::vector<PackageInfoRef>
Model::FilteredPackages() const
{
        BAutolock locker(&fLock);
        std::map<BString, PackageInfoRef>::const_iterator it;
        std::vector<PackageInfoRef> result;

        for (it = fPackages.begin(); it != fPackages.end(); it++) {
                PackageInfoRef package = it->second;
                if (fFilter->AcceptsPackage(package))
                        result.push_back(package);
        }

        return result;
}


const std::vector<PackageInfoRef>
Model::Packages() const
{
        BAutolock locker(&fLock);
        std::map<BString, PackageInfoRef>::const_iterator it;
        std::vector<PackageInfoRef> result;

        for (it = fPackages.begin(); it != fPackages.end(); it++)
                result.push_back(it->second);

        return result;
}


uint32
Model::_ChangeDiff(const PackageInfoRef& package)
{
        uint32 changeMask = kChangeMaskAll;
        const PackageInfoRef existingPackage = PackageForName(package->Name());

        if (existingPackage.IsSet())
                changeMask = package->ChangeMask(*(existingPackage.Get()));

        return changeMask;
}


void
Model::AddPackage(const PackageInfoRef& package)
{
        if (!package.IsSet())
                HDFATAL("attempt to add an unset package");
        BAutolock locker(&fLock);
        uint32 changeMask = _ChangeDiff(package);
        fPackages[package->Name()] = package;
        _NotifyPackageChange(PackageChangeEvent(package, changeMask));
}


void
Model::AddPackages(const std::vector<PackageInfoRef>& packages)
{
        if (packages.empty())
                return;

        BAutolock locker(&fLock);
        PackageChangeEvents events;
        std::vector<PackageInfoRef>::const_iterator it;

        for (it = packages.begin(); it != packages.end(); it++) {
                PackageInfoRef package = *it;
                events.AddEvent(PackageChangeEvent(package, _ChangeDiff(package)));
                fPackages[package->Name()] = package;
        }

        _NotifyPackageChanges(events);
}


void
Model::AddPackagesWithChange(const std::vector<PackageInfoRef>& packages, uint32 changeMask)
{
        if (packages.empty())
                return;

        BAutolock locker(&fLock);
        PackageChangeEvents events;

        std::vector<PackageInfoRef>::const_iterator it;
        for (it = packages.begin(); it != packages.end(); it++) {
                PackageInfoRef package = *it;
                events.AddEvent(PackageChangeEvent(package, changeMask));
                fPackages[package->Name()] = package;
        }

        _NotifyPackageChanges(events);
}


const PackageInfoRef
Model::PackageForName(const BString& name) const
{
        BAutolock locker(&fLock);
        std::map<BString, PackageInfoRef>::const_iterator it = fPackages.find(name);
        if (it != fPackages.end())
                return it->second;
        return PackageInfoRef();
}


bool
Model::HasPackage(const BString& packageName) const
{
        BAutolock locker(&fLock);
        if (fPackages.find(packageName) != fPackages.end())
                return true;
        return false;
}


const PackagesSummary
Model::GeneratePackagesSummary() const
{
        BAutolock locker(&fLock);
        bool anyProminentPackages = false;
        bool anyNativeDesktop = false;
        bool anyDesktop = false;
        std::map<BString, PackageInfoRef>::const_iterator it;

        for (it = fPackages.begin();
                it != fPackages.end() && (!anyProminentPackages || !anyNativeDesktop || !anyDesktop);
                it++) {
                const PackageInfoRef package = it->second;

                if (package.IsSet()) {
                        const PackageClassificationInfoRef classificationInfo
                                = package->PackageClassificationInfo();

                        if (classificationInfo.IsSet()) {
                                anyProminentPackages = anyProminentPackages | classificationInfo->IsProminent();
                                anyNativeDesktop = anyNativeDesktop | classificationInfo->IsNativeDesktop();
                                anyDesktop = anyDesktop | classificationInfo->IsDesktop();
                        }
                }
        }

        return PackagesSummary(anyProminentPackages, anyNativeDesktop, anyDesktop);
}


void
Model::Clear()
{
        BAutolock locker(&fLock);
        fIconRepository = PackageIconRepositoryRef(new PackageIconDefaultRepository(), true);
        fDepots.clear();
        fPackages.clear();
}


package_list_view_mode
Model::PackageListViewMode() const
{
        BAutolock locker(&fLock);
        return fPackageListViewMode;
}


void
Model::SetPackageListViewMode(package_list_view_mode mode)
{
        BAutolock locker(&fLock);
        fPackageListViewMode = mode;
}


bool
Model::CanShareAnonymousUsageData() const
{
        BAutolock locker(&fLock);
        return fCanShareAnonymousUsageData;
}


void
Model::SetCanShareAnonymousUsageData(bool value)
{
        BAutolock locker(&fLock);
        fCanShareAnonymousUsageData = value;
}


// #pragma mark - information retrieval

/*!     It may transpire that the package has no corresponding record on the
        server side because the repository is not represented in the server.
        In such a case, there is little point in communicating with the server
        only to hear back that the package does not exist.
*/

bool
Model::CanPopulatePackage(const PackageInfoRef& package)
{
        const BString depotName = PackageUtils::DepotName(package);
        const DepotInfoRef& depot = DepotForName(depotName);

        if (!depot.IsSet())
                return false;

        return !depot->WebAppRepositoryCode().IsEmpty();
}


WebAppInterfaceRef
Model::WebApp()
{
        BAutolock locker(&fLock);
        return fWebApp;
}


const BString&
Model::Nickname()
{
        BAutolock locker(&fLock);
        return fWebApp->Nickname();
}


void
Model::SetCredentials(const UserCredentials& credentials)
{
        BAutolock locker(&fLock);
        const BString existingNickname = Nickname();
        fWebApp = WebAppInterfaceRef(new WebAppInterface(credentials));
        if (credentials.Nickname() != existingNickname)
                _NotifyAuthorizationChanged();
}


// #pragma mark - listener notification methods


/*!     Assumes that the class is locked.
 */
void
Model::_NotifyPackageFilterChanged()
{
        std::vector<ModelListenerRef>::const_iterator it;
        for (it = fListeners.begin(); it != fListeners.end(); it++) {
                const ModelListenerRef& listener = *it;
                if (listener.IsSet())
                        listener->PackageFilterChanged();
        }
}


/*!     Assumes that the class is locked.
 */
void
Model::_NotifyAuthorizationChanged()
{
        std::vector<ModelListenerRef>::const_iterator it;
        for (it = fListeners.begin(); it != fListeners.end(); it++) {
                const ModelListenerRef& listener = *it;
                if (listener.IsSet())
                        listener->AuthorizationChanged();
        }
}


void
Model::_NotifyCategoryListChanged()
{
        std::vector<ModelListenerRef>::const_iterator it;
        for (it = fListeners.begin(); it != fListeners.end(); it++) {
                const ModelListenerRef& listener = *it;
                if (listener.IsSet())
                        listener->CategoryListChanged();
        }
}


void
Model::_NotifyPackageChange(const PackageChangeEvent& event)
{
        std::vector<PackageInfoListenerRef>::const_iterator it;
        for (it = fPackageListeners.begin(); it != fPackageListeners.end(); it++) {
                const PackageInfoListenerRef& listener = *it;
                if (listener.IsSet())
                        listener->PackagesChanged(PackageChangeEvents(event));
        }
}


// TODO: future work to optimize how this is conveyed to the listener in one go.
void
Model::_NotifyPackageChanges(const PackageChangeEvents& events)
{
        if (events.IsEmpty())
                return;

        std::vector<PackageInfoListenerRef>::const_iterator it;
        for (it = fPackageListeners.begin(); it != fPackageListeners.end(); it++) {
                const PackageInfoListenerRef& listener = *it;
                if (listener.IsSet())
                        listener->PackagesChanged(events);
        }
}


void
Model::_MaybeLogJsonRpcError(const BMessage& responsePayload, const char* sourceDescription) const
{
        BMessage error;
        BString errorMessage;
        double errorCode;

        if (responsePayload.FindMessage("error", &error) == B_OK
                && error.FindString("message", &errorMessage) == B_OK
                && error.FindDouble("code", &errorCode) == B_OK) {
                HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, errorMessage.String(), errorCode);
        } else {
                HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
        }
}


// #pragma mark - Rating Stabilities


const std::vector<RatingStabilityRef>
Model::RatingStabilities() const
{
        BAutolock locker(&fLock);
        return fRatingStabilities;
}


void
Model::SetRatingStabilities(const std::vector<RatingStabilityRef> value)
{
        BAutolock locker(&fLock);
        fRatingStabilities = value;
        std::sort(fRatingStabilities.begin(), fRatingStabilities.end(), IsRatingStabilityRefLess);
}


// #pragma mark - Categories


bool
Model::HasCategories()
{
        BAutolock locker(&fLock);
        return !fCategories.empty();
}


const std::vector<CategoryRef>
Model::Categories() const
{
        BAutolock locker(&fLock);
        return fCategories;
}


void
Model::SetCategories(const std::vector<CategoryRef> value)
{
        BAutolock locker(&fLock);
        fCategories = value;
        std::sort(fCategories.begin(), fCategories.end(), IsPackageCategoryRefLess);
        _NotifyCategoryListChanged();
}


void
Model::ScreenshotCached(const ScreenshotCoordinate& coord)
{
        BAutolock locker(&fLock);
        std::vector<ModelListenerRef>::const_iterator it;
        for (it = fListeners.begin(); it != fListeners.end(); it++) {
                const ModelListenerRef& listener = *it;
                if (listener.IsSet())
                        listener->ScreenshotCached(coord);
        }
}