/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "wifi_legacy_hal_factory.h"

#include <android-base/logging.h>
#include <dirent.h>
#include <dlfcn.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xmlmemory.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "wifi_legacy_hal_stubs.h"

namespace {
static constexpr char kVendorHalsDescPath[] = "/vendor/etc/wifi/vendor_hals";
static constexpr char kVendorHalsDescExt[] = ".xml";
static constexpr uint32_t kVendorHalsDescVersion = 1;

bool isDirectory(struct dirent* entryPtr) {
    bool isDir = false;
    if (entryPtr->d_type != DT_UNKNOWN && entryPtr->d_type != DT_LNK) {
        isDir = (entryPtr->d_type == DT_DIR);
    } else {
        struct stat entryStat;
        stat(entryPtr->d_name, &entryStat);
        isDir = S_ISDIR(entryStat.st_mode);
    }
    return isDir;
}

bool isFileExtension(const char* name, const char* ext) {
    if (name == NULL) return false;
    if (ext == NULL) return false;

    size_t extLen = strlen(ext);
    size_t nameLen = strlen(name);

    if (extLen > nameLen) return false;

    if (strncmp(name + nameLen - extLen, ext, extLen) != 0) return false;

    return true;
}
};  // namespace

namespace aidl {
namespace android {
namespace hardware {
namespace wifi {
namespace legacy_hal {

WifiLegacyHalFactory::WifiLegacyHalFactory(
        const std::weak_ptr<::android::wifi_system::InterfaceTool> iface_tool)
    : iface_tool_(iface_tool) {}

std::vector<std::shared_ptr<WifiLegacyHal>> WifiLegacyHalFactory::getHals() {
    if (legacy_hals_.empty()) {
        if (!initVendorHalDescriptorFromLinked()) initVendorHalsDescriptorList();
        for (auto& desc : descs_) {
            std::shared_ptr<WifiLegacyHal> hal =
                    std::make_shared<WifiLegacyHal>(iface_tool_, desc.fn, desc.primary);
            legacy_hals_.push_back(hal);
        }
    }

    return legacy_hals_;
}

bool WifiLegacyHalFactory::initVendorHalDescriptorFromLinked() {
    wifi_hal_lib_desc desc;

    if (!initLinkedHalFunctionTable(&desc.fn)) return false;

    desc.primary = true;
    desc.handle = NULL;
    descs_.push_back(desc);
    return true;
}

bool WifiLegacyHalFactory::initLinkedHalFunctionTable(wifi_hal_fn* hal_fn) {
    init_wifi_vendor_hal_func_table_t initfn;

    initfn = (init_wifi_vendor_hal_func_table_t)dlsym(RTLD_DEFAULT,
                                                      "init_wifi_vendor_hal_func_table");
    if (!initfn) {
        LOG(INFO) << "no vendor HAL library linked, will try dynamic load";
        return false;
    }

    if (!initHalFuncTableWithStubs(hal_fn)) {
        LOG(ERROR) << "Can not initialize the basic function pointer table";
        return false;
    }

    if (initfn(hal_fn) != WIFI_SUCCESS) {
        LOG(ERROR) << "Can not initialize the vendor function pointer table";
        return false;
    }

    return true;
}

/*
 * Overall structure of the HAL descriptor XML schema
 *
 * <?xml version="1.0" encoding="UTF-8"?>
 * <WifiVendorHal version="1">
 * <path>/vendor/lib64/libwifi-hal-qcom.so</path>
 * <primary>1</primary>
 * </WifiVendorHal>
 */
void WifiLegacyHalFactory::initVendorHalsDescriptorList() {
    xmlDocPtr xml;
    xmlNodePtr node, cnode;
    char* version = NULL;
    std::string path;
    xmlChar* value;
    wifi_hal_lib_desc desc;

    LOG(INFO) << "processing vendor HALs descriptions in " << kVendorHalsDescPath;
    DIR* dirPtr = ::opendir(kVendorHalsDescPath);
    if (dirPtr == NULL) {
        LOG(ERROR) << "failed to open " << kVendorHalsDescPath;
        return;
    }
    for (struct dirent* entryPtr = ::readdir(dirPtr); entryPtr != NULL;
         entryPtr = ::readdir(dirPtr)) {
        if (isDirectory(entryPtr)) continue;

        if (!isFileExtension(entryPtr->d_name, kVendorHalsDescExt))
            continue;  // only process .xml files

        LOG(INFO) << "processing config file: " << entryPtr->d_name;

        std::string fullPath(kVendorHalsDescPath);
        fullPath.append("/");
        fullPath.append(entryPtr->d_name);
        xml = xmlReadFile(fullPath.c_str(), "UTF-8", XML_PARSE_RECOVER);
        if (!xml) {
            LOG(ERROR) << "failed to parse: " << entryPtr->d_name << " skipping...";
            continue;
        }
        node = xmlDocGetRootElement(xml);
        if (!node) {
            LOG(ERROR) << "empty config file: " << entryPtr->d_name << " skipping...";
            goto skip;
        }
        if (xmlStrcmp(node->name, BAD_CAST "WifiVendorHal")) {
            LOG(ERROR) << "bad config, root element not WifiVendorHal: " << entryPtr->d_name
                       << " skipping...";
            goto skip;
        }
        version = (char*)xmlGetProp(node, BAD_CAST "version");
        if (!version || strtoul(version, NULL, 0) != kVendorHalsDescVersion) {
            LOG(ERROR) << "conf file: " << entryPtr->d_name
                       << "must have version: " << kVendorHalsDescVersion << ", skipping...";
            goto skip;
        }
        cnode = node->children;
        path.clear();
        desc.primary = false;
        while (cnode) {
            if (!xmlStrcmp(cnode->name, BAD_CAST "path")) {
                value = xmlNodeListGetString(xml, cnode->children, 1);
                if (value) path = (char*)value;
                xmlFree(value);
            } else if (!xmlStrcmp(cnode->name, BAD_CAST "primary")) {
                value = xmlNodeListGetString(xml, cnode->children, 1);
                desc.primary = !xmlStrcmp(value, BAD_CAST "1");
                xmlFree(value);
            }
            cnode = cnode->next;
        }
        if (path.empty()) {
            LOG(ERROR) << "hal library path not provided in: " << entryPtr->d_name
                       << ", skipping...";
            goto skip;
        }
        if (loadVendorHalLib(path, desc)) {
            if (desc.primary)
                descs_.insert(descs_.begin(), desc);
            else
                descs_.push_back(desc);
        }
    skip:
        xmlFreeDoc(xml);
        if (version) {
            xmlFree(version);
            version = NULL;
        }
    }
    ::closedir(dirPtr);
}

bool WifiLegacyHalFactory::loadVendorHalLib(const std::string& path, wifi_hal_lib_desc& desc) {
    void* h = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
    init_wifi_vendor_hal_func_table_t initfn;
    wifi_error res;

    if (!h) {
        LOG(ERROR) << "failed to open vendor hal library: " << path;
        return false;
    }
    initfn = (init_wifi_vendor_hal_func_table_t)dlsym(h, "init_wifi_vendor_hal_func_table");
    if (!initfn) {
        LOG(ERROR) << "init_wifi_vendor_hal_func_table not found in: " << path;
        goto out_err;
    }

    if (!initHalFuncTableWithStubs(&desc.fn)) {
        LOG(ERROR) << "Can not initialize the basic function pointer table";
        goto out_err;
    }
    res = initfn(&desc.fn);
    if (res != WIFI_SUCCESS) {
        LOG(ERROR) << "failed to initialize the vendor func table in: " << path
                   << " error: " << res;
        goto out_err;
    }

    res = desc.fn.wifi_early_initialize();
    // vendor HALs which do not implement early_initialize will return
    // WIFI_ERROR_NOT_SUPPORTED, treat this as success.
    if (res != WIFI_SUCCESS && res != WIFI_ERROR_NOT_SUPPORTED) {
        LOG(ERROR) << "early initialization failed in: " << path << " error: " << res;
        goto out_err;
    }

    desc.handle = h;
    return true;
out_err:
    dlclose(h);
    return false;
}

}  // namespace legacy_hal
}  // namespace wifi
}  // namespace hardware
}  // namespace android
}  // namespace aidl
