/**
 * PROJECT:         ExectOS
 * COPYRIGHT:       See COPYING.md in the top level directory
 * FILE:            xtldr/config.c
 * DESCRIPTION:     XT Boot Loader Configuration
 * DEVELOPERS:      Rafal Kupiec <belliash@codingworkshop.eu.org>
 */

#include <xtldr.h>


/**
 * Returns a value of the specified configuration key.
 *
 * @param ConfigName
 *        Specifies the configuration key to return its value.
 *
 * @return This routine returns a pointer to the configuration value, or NULL if key was not found.
 *
 * @since XT 1.0
 */
XTCDECL
PWCHAR
BlGetConfigValue(IN CONST PWCHAR ConfigName)
{
    PXTBL_CONFIG_ENTRY ConfigEntry;
    PLIST_ENTRY ConfigListEntry;
    SIZE_T KeyLength, ValueLength;
    EFI_STATUS Status;
    PWCHAR Value;

    /* Get config entry name length */
    KeyLength = RtlWideStringLength(ConfigName, 0);

    /* Iterate through config entries */
    ConfigListEntry = BlpConfig.Flink;
    while(ConfigListEntry != &BlpConfig)
    {
        /* Get config entry */
        ConfigEntry = CONTAIN_RECORD(ConfigListEntry, XTBL_CONFIG_ENTRY, Flink);

        /* Check if requested configuration found */
        if(RtlCompareWideStringInsensitive(ConfigEntry->Name, ConfigName, KeyLength) == 0)
        {
            /* Get value length */
            ValueLength = RtlWideStringLength(ConfigEntry->Value, 0);

            /* Allocate memory for value */
            Status = BlMemoryAllocatePool(ValueLength * sizeof(WCHAR), (PVOID *)&Value);
            if(Status != STATUS_EFI_SUCCESS)
            {
                /* Memory allocation failure, return NULL */
                BlDebugPrint(L"ERROR: Memory allocation failure (Status Code: 0x%lx)\n", Status);
                return NULL;
            }

            /* Copy value and return it */
            RtlCopyMemory(Value, ConfigEntry->Value, ValueLength * sizeof(WCHAR));
            Value[ValueLength] = 0;
            return Value;
        }

        /* Move to the next config entry */
        ConfigListEntry = ConfigListEntry->Flink;
    }

    /* Config entry not found, return NULL */
    return NULL;
}

/**
 * Updates existing configuration value.
 *
 * @param ConfigName
 *        Specifies the configuration key to update.
 *
 * @param ConfigValue
 *        Specifies the new configuration value.
 *
 * @return This routine returns status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
BlSetConfigValue(IN CONST PWCHAR ConfigName,
                 IN CONST PWCHAR ConfigValue)
{
    PXTBL_CONFIG_ENTRY ConfigEntry;
    PLIST_ENTRY ConfigListEntry;
    SIZE_T Length;

    /* Get config entry name length */
    Length = RtlWideStringLength(ConfigName, 0);

    /* Iterate through config entries */
    ConfigListEntry = BlpConfig.Flink;
    while(ConfigListEntry != &BlpConfig)
    {
        /* Get config entry */
        ConfigEntry = CONTAIN_RECORD(ConfigListEntry, XTBL_CONFIG_ENTRY, Flink);

        /* Check if requested configuration found */
        if(RtlCompareWideStringInsensitive(ConfigEntry->Name, ConfigName, Length) == 0)
        {
            /* Update config value */
            ConfigEntry->Value = ConfigValue;

            /* Return success */
            return STATUS_EFI_SUCCESS;
        }

        /* Move to the next config entry */
        ConfigListEntry = ConfigListEntry->Flink;
    }

    /* Config entry not found */
    return STATUS_EFI_NOT_FOUND;
}

/**
 * Loads and parses XTLDR configuration file.
 *
 * @return This routine returns status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
BlpLoadConfiguration()
{
    PLIST_ENTRY SectionListEntry;
    EFI_STATUS Status;
    PCHAR ConfigData;

    /* Initialize configuration pointer */
    RtlInitializeListHead(&BlpConfigSections);

    /* Read data from configuration file */
    Status = BlpReadConfigFile(XTBL_LOADER_DIRECTORY_PATH, L"XTLDR.INI", &ConfigData);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to read config file, try with architecture specific directory */
        Status = BlpReadConfigFile(XTBL_ARCH_LOADER_DIRECTORY_PATH, L"XTLDR.INI", &ConfigData);
    }

    /* Check if configuration was read successfully */
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to load configuration */
        BlDebugPrint(L"Failed to load FS0:/EFI/BOOT/XTLDR.INI configuration file (Status Code: 0x%lx)\n", Status);
        return Status;
    }

    /* Parse configuration data */
    Status = BlpParseConfigFile(ConfigData, &BlpConfigSections);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to parse configuration */
        BlDebugPrint(L"Failed to parse FS0:/EFI/BOOT/XTLDR.INI configuration file (Status Code: 0x%lx)\n", Status);
        return Status;
    }

    /* Iterate through config sections */
    SectionListEntry = BlpConfigSections.Flink;
    while(SectionListEntry != &BlpConfigSections)
    {
        /* Get config section */
        PXTBL_CONFIG_SECTION Section = CONTAIN_RECORD(SectionListEntry, XTBL_CONFIG_SECTION, Flink);

        /* Look for global XTLDR configuration section */
        if(RtlCompareWideStringInsensitive(Section->SectionName, L"XTLDR", 5) == 0)
        {
            /* Update global configuration */
            BlpUpdateConfiguration(&Section->Options);

            /* Remove XTLDR section from the list */
            RtlRemoveEntryList(SectionListEntry);
            break;
        }

        /* Move to the next section */
        SectionListEntry = SectionListEntry->Flink;
    }

    /* Update boot menu OS list */
    BlpMenuList = &BlpConfigSections;

    /* Return success */
    return STATUS_EFI_SUCCESS;
}

/**
 * Parses command line arguments and updates global configuration.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
BlpParseCommandLine(VOID)
{
    EFI_GUID LIPGuid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
    PEFI_LOADED_IMAGE_PROTOCOL LoadedImage;
    PWCHAR Argument, Key, LastArg, Value;
    PXTBL_CONFIG_ENTRY Option;
    EFI_STATUS Status;
    SIZE_T KeyLength, ValueLength;
    LIST_ENTRY Config;

    /* Initialize configuration list */
    RtlInitializeListHead(&Config);

    /* Handle loaded image protocol */
    Status = EfiSystemTable->BootServices->HandleProtocol(EfiImageHandle, &LIPGuid, (PVOID *)&LoadedImage);
    if(Status == STATUS_EFI_SUCCESS)
    {
        /* Check if launched from UEFI shell */
        if(LoadedImage && LoadedImage->LoadOptions)
        {
            /* Tokenize provided options */
            Argument = RtlTokenizeWideString(LoadedImage->LoadOptions, L" ", &LastArg);

            /* Iterate over all arguments passed to boot loader */
            while(Argument != NULL)
            {
                /* Store key name */
                Key = Argument;

                /* Find end of the key */
                while(*Argument != '=' && *Argument != 0 && *Argument != '\n')
                {
                    /* Advance to the next character */
                    Argument++;
                }

                /* Mark end of the key and advance to the next character */
                *Argument = 0;
                Argument++;

                /* Store value */
                Value = Argument;

                /* Find end of the value */
                while(*Argument != 0 && *Argument != '\n')
                {
                    /* Advance to the next character */
                    Argument++;
                }

                /* Mark end of the value and advance to the next character */
                *Argument = 0;
                Argument++;

                /* Get length of the key and its value */
                KeyLength = RtlWideStringLength(Key, 0);
                ValueLength = RtlWideStringLength(Value, 0);

                /* Check if argument is valid */
                if(KeyLength == 0 || ValueLength == 0)
                {
                    /* Invalid argument, skip to the next one */
                    continue;
                }

                /* Allocate memory for new option */
                Status = BlMemoryAllocatePool(sizeof(XTBL_CONFIG_ENTRY), (PVOID*)&Option);
                if(Status == STATUS_EFI_SUCCESS)
                {
                    /* Allocate more memory for option name */
                    Status = BlMemoryAllocatePool(sizeof(PWCHAR) * (KeyLength + 1), (PVOID*)&Option->Name);
                    if(Status == STATUS_EFI_SUCCESS)
                    {
                        /* Allocate even more memory for option value */
                        Status = BlMemoryAllocatePool(sizeof(PWCHAR) * (ValueLength + 1), (PVOID*)&Option->Value);
                    }
                }
                if(Status != STATUS_EFI_SUCCESS)
                {
                    /* Some memory allocation failed */
                    return Status;
                }

                /* Set entry name and value */
                RtlCopyMemory(Option->Name, Key, (KeyLength * sizeof(WCHAR)));
                RtlCopyMemory(Option->Value, Value, (ValueLength * sizeof(WCHAR)));
                Option->Name[KeyLength] = 0;
                Option->Value[ValueLength] = 0;

                /* Add entry to the list */
                RtlInsertTailList(&Config, &Option->Flink);

                /* Take next argument */
                Argument = RtlTokenizeWideString(NULL, L" ", &LastArg);
            }

            /* Update global configuration */
            BlpUpdateConfiguration(&Config);
        }
    }

    /* Return success */
    return STATUS_EFI_SUCCESS;
}

/**
 * Parses configuration INI file.
 *
 * @param RawConfig
 *        Suplies a pointer to configuration INI file to be parsed.
 *
 * @param Configuration
 *        Supplies a pointer to memory region where parsed configuration will be stored.
 *
 * @return This routine returns a status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
BlpParseConfigFile(IN CONST PCHAR RawConfig,
                   OUT PLIST_ENTRY Configuration)
{
    SIZE_T SectionLength, KeyLength, ValueLength;
    PCHAR InputData, Key, SectionName, Value;
    PXTBL_CONFIG_SECTION Section;
    PXTBL_CONFIG_ENTRY Option;
    EFI_STATUS Status;

    /* Initialize pointers */
    InputData = RawConfig;
    Section = NULL;
    Option = NULL;
    SectionName = NULL;
    Key = NULL;
    Value = NULL;

    /* Analyze configuration data until end of file is reached */
    while(*InputData != 0)
    {
        if(*InputData == ';' || *InputData == '#')
        {
            /* Skip comment until end of the line */
            while(*InputData != 0 && *InputData != '\n')
            {
                /* Advance to the next character */
                InputData++;
            }
        }
        else if(*InputData == ' ' || *InputData == '\t' || *InputData == '\r' || *InputData == '\n')
        {
            /* Skip whitespaces */
            InputData++;
        }
        else if(*InputData == '[')
        {
            /* Skip leading bracket */
            InputData++;

            /* Store section name */
            SectionName = InputData;

            /* Find end of the section name */
            while(*InputData != ']' && *InputData != 0 && *InputData != '\n')
            {
                /* Advance to the next character */
                InputData++;
            }

            /* Check if end of the section name is reached */
            if(*InputData != ']')
            {
                /* Section name does not end */
                return STATUS_EFI_INVALID_PARAMETER;
            }

            /* Mark end of the section name and advance to the next character */
            *InputData = 0;
            InputData++;

            /* Remove leading and trailing spaces from section name */
            SectionName = RtlTrimString(SectionName);

            /* Find length of the section name */
            SectionLength = RtlStringLength(SectionName, 0);

            /* Allocate memory for new section */
            Status = BlMemoryAllocatePool(sizeof(XTBL_CONFIG_SECTION), (PVOID*)&Section);
            if(Status == STATUS_EFI_SUCCESS)
            {
                /* Allocate more memory for section name */
                Status = BlMemoryAllocatePool(sizeof(PWCHAR) * (SectionLength + 1), (PVOID*)&Section->SectionName);
            }
            if(Status != STATUS_EFI_SUCCESS)
            {
                /* Some memory allocation failed */
                return Status;
            }

            /* Initialize new section and add it to the list */
            RtlInitializeListHead(&Section->Options);
            RtlStringToWideString(Section->SectionName, &SectionName, SectionLength + 1);
            RtlInsertTailList(Configuration, &Section->Flink);
        }
        else
        {
            /* Store key */
            Key = InputData;

            /* Find end of the key */
            while(*InputData != '=' && *InputData != 0 && *InputData != '\n')
            {
                /* Advance to the next character */
                InputData++;
            }

            /* Check if end of the key is reached */
            if(*InputData != '=')
            {
                /* Key name does not end */
                return STATUS_EFI_INVALID_PARAMETER;
            }

            /* Mark end of the key and advance to the next character */
            *InputData = 0;
            InputData++;

            /* Skip all leading spaces in the value */
            while(*InputData == ' ')
            {
                /* Advance to the next character */
                InputData++;
            }

            /* Store value */
            Value = InputData;

            /* Find end of the value */
            while(*InputData != 0 && *InputData != '\n')
            {
                /* Advance to the next character */
                InputData++;
            }

            /* Mark end of the value and advance to the next character */
            *InputData = 0;
            InputData++;

            /* Remove leading and trailing spaces from key and value */
            Key = RtlTrimString(Key);
            Value = RtlTrimString(Value);

            /* Find length of the key and its value */
            KeyLength = RtlStringLength(Key, 0);
            ValueLength = RtlStringLength(Value, 0);

            /* Allocate memory for new option */
            Status = BlMemoryAllocatePool(sizeof(XTBL_CONFIG_ENTRY), (PVOID*)&Option);
            if(Status == STATUS_EFI_SUCCESS)
            {
                /* Allocate more memory for option name */
                Status = BlMemoryAllocatePool(sizeof(PWCHAR) * (KeyLength + 1), (PVOID*)&Option->Name);
                if(Status == STATUS_EFI_SUCCESS)
                {
                    /* Allocate even more memory for option value */
                    Status = BlMemoryAllocatePool(sizeof(PWCHAR) * (ValueLength + 1), (PVOID*)&Option->Value);
                }
            }
            if(Status != STATUS_EFI_SUCCESS)
            {
                /* Some memory allocation failed */
                return Status;
            }

            /* Remove leading quotes from the value */
            if(*Value == '"' || *Value == '\'')
            {
                Value++;
            }

            /* Remove trailing quotes from the value */
            if(Value[ValueLength - 2] == '"' || Value[ValueLength - 2] == '\'')
            {
                Value[ValueLength - 2] = 0;
            }

            /* Initialize new option and add it to the list */
            RtlStringToWideString(Option->Name, &Key, RtlStringLength(Key, 0) + 1);
            RtlStringToWideString(Option->Value, &Value, RtlStringLength(Value, 0) + 1);
            RtlInsertTailList(&Section->Options, &Option->Flink);
        }
    }

    /* Return success */
    return STATUS_EFI_SUCCESS;
}

/**
 * Loads configuration file from the specified directory on the FS0:/ drive.
 *
 * @param ConfigDirectory
 *        Specifies a path to the directory containing the configuration file.
 *
 * @param ConfigFile
 *        Specifies the name of the configuration file.
 *
 * @param ConfigData
 *        Provides a buffer to store the data read from the configuration file.
 *
 * @return This routine returns status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
BlpReadConfigFile(IN CONST PWCHAR ConfigDirectory,
                  IN CONST PWCHAR ConfigFile,
                  OUT PCHAR *ConfigData)
{
    PEFI_FILE_HANDLE DirHandle, FsHandle;
    EFI_HANDLE DiskHandle;
    EFI_STATUS Status;
    SIZE_T FileSize;

    /* Open EFI volume */
    Status = BlOpenVolume(NULL, &DiskHandle, &FsHandle);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to open a volume */
        return Status;
    }

    /* Open specified directory, containing the configuration file and close the FS immediately */
    Status = FsHandle->Open(FsHandle, &DirHandle, ConfigDirectory, EFI_FILE_MODE_READ, 0);
    FsHandle->Close(FsHandle);

    /* Check if directory opened successfully */
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to open directory */
        BlCloseVolume(DiskHandle);
        return Status;
    }

    /* Read configuration file and close directory */
    Status = BlReadFile(DirHandle, ConfigFile, (PVOID *)ConfigData, &FileSize);
    DirHandle->Close(DirHandle);

    /* Close EFI volume */
    BlCloseVolume(DiskHandle);

    /* Return read status */
    return Status;
}

/**
 * Adds new XTLDR configuration entries to the global configuration list. Existing entries are not overwritten.
 *
 * @param NewConfig
 *        Supplies a pointer to a linked list containing new configuration entries.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpUpdateConfiguration(IN PLIST_ENTRY NewConfig)
{
    PXTBL_CONFIG_ENTRY ConfigEntry;
    PLIST_ENTRY ConfigListEntry, NextListEntry;

    /* Iterate through new config entries */
    ConfigListEntry = NewConfig->Flink;
    while(ConfigListEntry != NewConfig)
    {
        /* Get new config entry */
        ConfigEntry = CONTAIN_RECORD(ConfigListEntry, XTBL_CONFIG_ENTRY, Flink);

        /* Get next config entry */
        NextListEntry = ConfigListEntry->Flink;

        /* Make sure config entry does not exist yet */
        if(BlGetConfigValue(ConfigEntry->Name) == NULL)
        {
            /* Remove new config entry from input list and put it into global config list */
            RtlRemoveEntryList(&ConfigEntry->Flink);
            RtlInsertTailList(&BlpConfig, &ConfigEntry->Flink);
        }

        /* Move to the next new config entry */
        ConfigListEntry = NextListEntry;
    }
}