/*++

Copyright (c) 2016 Minoca Corp. All Rights Reserved

Module Name:

    ninja.c

Abstract:

    This module implements output support for Ninja in the Minoca Build
    Generator.

Author:

    Evan Green 11-Mar-2015

Environment:

    Build

--*/

//
// ------------------------------------------------------------------- Includes
//

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "mingen.h"

//
// ---------------------------------------------------------------- Definitions
//

#define MINGEN_NINJA_FILE "build.ninja"
#define MINGEN_NINJA_VARIABLE "${%s}"
#define MINGEN_NINJA_LINE_CONTINUATION " $\n"
#define MINGEN_NINJA_INPUTS "$in"
#define MINGEN_NINJA_OUTPUT "$out"

//
// ------------------------------------------------------ Data Type Definitions
//

//
// ----------------------------------------------- Internal Function Prototypes
//

VOID
MingenNinjaPrintDefaultTargets (
    PMINGEN_CONTEXT Context,
    FILE *File
    );

VOID
MingenNinjaPrintRebuildRule (
    PMINGEN_CONTEXT Context,
    FILE *File
    );

VOID
MingenNinjaPrintWithVariableConversion (
    FILE *File,
    PSTR String
    );

VOID
MingenNinjaPrintTargetFile (
    FILE *File,
    PMINGEN_CONTEXT Context,
    PMINGEN_TARGET Target
    );

VOID
MingenNinjaPrintSource (
    FILE *File,
    PMINGEN_CONTEXT Context,
    PMINGEN_SOURCE Source
    );

VOID
MingenNinjaPrintTreeRoot (
    FILE *File,
    MINGEN_DIRECTORY_TREE Tree
    );

VOID
MingenNinjaPrintConfig (
    FILE *File,
    PMINGEN_CONTEXT Context,
    PMINGEN_TARGET Target
    );

INT
MingenNinjaPrintConfigValue (
    FILE *File,
    PCHALK_OBJECT Value
    );

//
// -------------------------------------------------------------------- Globals
//

//
// ------------------------------------------------------------------ Functions
//

INT
MingenCreateNinja (
    PMINGEN_CONTEXT Context
    )

/*++

Routine Description:

    This routine creates a Ninja build file out of the build graph.

Arguments:

    Context - Supplies a pointer to the application context.

Return Value:

    0 on success.

    Returns an error number on failure.

--*/

{

    PLIST_ENTRY CurrentEntry;
    FILE *File;
    UINTN Index;
    PMINGEN_TARGET Input;
    PSTR NinjaPath;
    PMINGEN_POOL Pool;
    PLIST_ENTRY PoolEntry;
    PMINGEN_SCRIPT Script;
    PLIST_ENTRY ScriptEntry;
    PMINGEN_SOURCE Source;
    INT Status;
    PMINGEN_TARGET Target;
    time_t Time;
    PMINGEN_TOOL Tool;

    File = NULL;
    NinjaPath = MingenAppendPaths(Context->BuildRoot, MINGEN_NINJA_FILE);
    if (NinjaPath == NULL) {
        Status = ENOMEM;
        goto CreateNinjaEnd;
    }

    if ((Context->Options & MINGEN_OPTION_VERBOSE) != 0) {
        printf("Creating %s\n", NinjaPath);
    }

    File = fopen(NinjaPath, "w");
    if (File == NULL) {
        Status = errno;
        fprintf(stderr,
                "Error: Failed to open %s: %s\n",
                NinjaPath,
                strerror(Status));

        goto CreateNinjaEnd;
    }

    Time = time(NULL);
    fprintf(File,
            "# Ninja build automatically generated by Minoca mingen at %s\n",
            ctime(&Time));

    fprintf(File, "# Define high level variables\n");
    fprintf(File,
            "%s = %s\n",
            MINGEN_VARIABLE_SOURCE_ROOT,
            Context->SourceRoot);

    fprintf(File,
            "%s = %s\n",
            MINGEN_VARIABLE_BUILD_ROOT,
            Context->BuildRoot);

    fprintf(File,
            "%s = %s\n",
            MINGEN_VARIABLE_PROJECT_PATH,
            Context->ProjectFilePath);

    MingenNinjaPrintConfig(File, Context, NULL);
    fprintf(File, "\n# Define tools\n");
    CurrentEntry = Context->ToolList.Next;
    while (CurrentEntry != &(Context->ToolList)) {
        Tool = LIST_VALUE(CurrentEntry, MINGEN_TOOL, ListEntry);
        CurrentEntry = CurrentEntry->Next;
        if ((Tool->Flags & MINGEN_TOOL_ACTIVE) == 0) {
            continue;
        }

        fprintf(File, "rule %s\n", Tool->Name);
        if (Tool->Description != NULL) {
            fprintf(File, "    description = ");
            MingenNinjaPrintWithVariableConversion(File, Tool->Description);
            fprintf(File, "\n");
        }

        fprintf(File, "    command = ");
        MingenNinjaPrintWithVariableConversion(File, Tool->Command);
        fprintf(File, "\n");
        if (Tool->Depfile != NULL) {
            fprintf(File, "    depfile = ");
            MingenNinjaPrintWithVariableConversion(File, Tool->Depfile);
            fprintf(File, "\n");
        }

        if (Tool->DepsFormat != NULL) {
            fprintf(File, "    deps = %s\n", Tool->DepsFormat);
        }

        if (Tool->Pool != NULL) {
            fprintf(File, "    pool = %s\n", Tool->Pool);
        }

        fprintf(File, "\n");
    }

    if (!LIST_EMPTY(&(Context->PoolList))) {
        fprintf(File, "# Define pools");
    }

    PoolEntry = Context->PoolList.Next;
    while (PoolEntry != &(Context->PoolList)) {
        Pool = LIST_VALUE(PoolEntry, MINGEN_POOL, ListEntry);
        PoolEntry = PoolEntry->Next;
        if ((Pool->Flags & MINGEN_POOL_ACTIVE) == 0) {
            continue;
        }

        fprintf(File, "\npool %s\n    depth = %d\n", Pool->Name, Pool->Depth);
    }

    fprintf(File, "\n");

    //
    // Loop over every script (file) in the build.
    //

    ScriptEntry = Context->ScriptList.Next;
    while (ScriptEntry != &(Context->ScriptList)) {
        Script = LIST_VALUE(ScriptEntry, MINGEN_SCRIPT, ListEntry);
        ScriptEntry = ScriptEntry->Next;
        if ((LIST_EMPTY(&(Script->TargetList))) ||
            ((Script->Flags & MINGEN_SCRIPT_ACTIVE) == 0)) {

            continue;
        }

        //
        // Loop over every target defined in the script.
        //

        if (Script->Path[0] == '\0') {
            fprintf(File, "# Define root targets\n");

        } else {
            fprintf(File, "# Define targets for %s\n", Script->Path);
        }

        CurrentEntry = Script->TargetList.Next;
        while (CurrentEntry != &(Script->TargetList)) {
            Target = LIST_VALUE(CurrentEntry, MINGEN_TARGET, ListEntry);
            CurrentEntry = CurrentEntry->Next;
            if ((Target->Flags & MINGEN_TARGET_ACTIVE) == 0) {
                continue;
            }

            //
            // Add the configs for this target.
            //

            fprintf(File, "build ");
            MingenNinjaPrintTargetFile(File, Context, Target);
            fprintf(File, ": %s ", Target->Tool);

            //
            // Add the inputs.
            //

            for (Index = 0; Index < Target->Inputs.Count; Index += 1) {
                Input = Target->Inputs.Array[Index];
                switch (Input->Type) {
                case MingenInputTarget:
                    MingenNinjaPrintTargetFile(File, Context, Input);
                    break;

                case MingenInputSource:
                    Source = (PMINGEN_SOURCE)Input;
                    MingenNinjaPrintSource(File, Context, Source);
                    break;

                default:

                    assert(FALSE);

                    break;
                }

                if (Index + 1 != Target->Inputs.Count) {
                    fprintf(File, MINGEN_NINJA_LINE_CONTINUATION "        ");
                }
            }

            //
            // Add the implicit inputs if there are any.
            //

            if (Target->Implicit.Count != 0) {
                fprintf(File, " | " MINGEN_NINJA_LINE_CONTINUATION "        ");
                for (Index = 0;
                     Index < Target->Implicit.Count;
                     Index += 1) {

                    Input = Target->Implicit.Array[Index];
                    switch (Input->Type) {
                    case MingenInputTarget:
                        MingenNinjaPrintTargetFile(File, Context, Input);
                        break;

                    case MingenInputSource:
                        Source = (PMINGEN_SOURCE)Input;
                        MingenNinjaPrintSource(File, Context, Source);
                        break;

                    default:

                        assert(FALSE);

                        break;
                    }

                    if (Index + 1 != Target->Implicit.Count) {
                        fprintf(File,
                                MINGEN_NINJA_LINE_CONTINUATION "        ");
                    }
                }
            }

            //
            // Add the order-only inputs if there are any.
            //

            if (Target->OrderOnly.Count != 0) {
                fprintf(File, " || " MINGEN_NINJA_LINE_CONTINUATION "        ");
                for (Index = 0;
                     Index < Target->OrderOnly.Count;
                     Index += 1) {

                    Input = Target->OrderOnly.Array[Index];
                    switch (Input->Type) {
                    case MingenInputTarget:
                        MingenNinjaPrintTargetFile(File, Context, Input);
                        break;

                    case MingenInputSource:
                        Source = (PMINGEN_SOURCE)Input;
                        MingenNinjaPrintSource(File, Context, Source);
                        break;

                    default:

                        assert(FALSE);

                        break;
                    }

                    if (Index + 1 != Target->OrderOnly.Count) {
                        fprintf(File,
                                MINGEN_NINJA_LINE_CONTINUATION "        ");
                    }
                }
            }

            fprintf(File, "\n");
            MingenNinjaPrintConfig(File, Context, Target);
            if (Target->Pool != NULL) {
                fprintf(File, "    pool = %s\n", Target->Pool);
            }

            //
            // Separate targets with newlines, except squeeze together a bunch
            // of one-liners.
            //

            if ((Target->Inputs.Count > 1) ||
                (Target->Implicit.Count != 0) ||
                (Target->OrderOnly.Count != 0) ||
                ((Target->Config != NULL) &&
                 (!LIST_EMPTY(&(Target->Config->Dict.EntryList)))) ||
                (Target->Pool != NULL) ||
                (CurrentEntry == &(Script->TargetList))) {

                fprintf(File, "\n");
            }
        }
    }

    if ((Context->Options & MINGEN_OPTION_NO_REBUILD_RULE) == 0) {
        MingenNinjaPrintRebuildRule(Context, File);
    }

    MingenNinjaPrintDefaultTargets(Context, File);
    Status = 0;

CreateNinjaEnd:
    if (File != NULL) {
        fclose(File);
    }

    if (NinjaPath != NULL) {
        free(NinjaPath);
    }

    return Status;
}

//
// --------------------------------------------------------- Internal Functions
//

VOID
MingenNinjaPrintDefaultTargets (
    PMINGEN_CONTEXT Context,
    FILE *File
    )

/*++

Routine Description:

    This routine emits default statements for the default targets.

Arguments:

    Context - Supplies a pointer to the application context.

    File - Supplies a pointer to the file to print the build directories to.

Return Value:

    None.

--*/

{

    PLIST_ENTRY CurrentEntry;
    BOOL PrintedBanner;
    PMINGEN_SCRIPT Script;
    PLIST_ENTRY ScriptEntry;
    PMINGEN_TARGET Target;

    PrintedBanner = FALSE;
    ScriptEntry = Context->ScriptList.Next;
    while (ScriptEntry != &(Context->ScriptList)) {
        Script = LIST_VALUE(ScriptEntry, MINGEN_SCRIPT, ListEntry);
        ScriptEntry = ScriptEntry->Next;
        if ((Script->Flags & MINGEN_SCRIPT_ACTIVE) == 0) {
            continue;
        }

        CurrentEntry = Script->TargetList.Next;
        while (CurrentEntry != &(Script->TargetList)) {
            Target = LIST_VALUE(CurrentEntry, MINGEN_TARGET, ListEntry);
            if (((Target->Flags & MINGEN_TARGET_DEFAULT) != 0) &&
                ((Target->Flags & MINGEN_TARGET_ACTIVE) != 0)) {

                if (PrintedBanner == FALSE) {
                    fprintf(File, "\n# Default target\n");
                    PrintedBanner = TRUE;
                }

                fprintf(File, "default ");
                MingenNinjaPrintTargetFile(File, Context, Target);
                fprintf(File, "\n");
            }

            CurrentEntry = CurrentEntry->Next;
        }
    }

    return;
}

VOID
MingenNinjaPrintRebuildRule (
    PMINGEN_CONTEXT Context,
    FILE *File
    )

/*++

Routine Description:

    This routine emits the built in target that rebuilds the Makefile itself
    based on the source scripts.

Arguments:

    Context - Supplies a pointer to the application context.

    File - Supplies a pointer to the file to print the build directories to.

Return Value:

    None.

--*/

{

    PSTR BuildFileName;
    PLIST_ENTRY CurrentEntry;
    PMINGEN_SCRIPT Script;

    BuildFileName = Context->BuildFileName;
    if (BuildFileName == NULL) {
        BuildFileName = MINGEN_BUILD_FILE;
    }

    fprintf(File,
            "# Built-in tool and rule for rebuilding the ninja file itself.\n");

    fprintf(File,
            "rule rebuild_ninja\n"
            "    description = Rebuilding Ninja file\n"
            "    generator = 1\n"
            "    command = ");

    MingenPrintRebuildCommand(Context, File);
    fprintf(File, "\n\n");
    fprintf(File, "build %s: rebuild_ninja ", MINGEN_NINJA_FILE);
    CurrentEntry = Context->ScriptList.Next;
    while (CurrentEntry != &(Context->ScriptList)) {
        Script = LIST_VALUE(CurrentEntry, MINGEN_SCRIPT, ListEntry);
        CurrentEntry = CurrentEntry->Next;
        if ((Script->Flags & MINGEN_SCRIPT_ACTIVE) == 0) {
            continue;
        }

        if (strcmp(Script->CompletePath, Context->ProjectFilePath) == 0) {
            fprintf(File, MINGEN_NINJA_VARIABLE, MINGEN_VARIABLE_PROJECT_PATH);

        } else if (Script->Order == MingenScriptOrderTarget) {
            MingenNinjaPrintTreeRoot(File, Script->Root);
            fprintf(File, "%s/%s", Script->Path, BuildFileName);

        } else {
            MingenNinjaPrintTreeRoot(File, Script->Root);
            fprintf(File, "%s", Script->Path);
        }

        if (CurrentEntry != &(Context->ScriptList)) {
            fprintf(File, MINGEN_NINJA_LINE_CONTINUATION "        ");
        }
    }

    fprintf(File, "\n");
    return;
}

VOID
MingenNinjaPrintWithVariableConversion (
    FILE *File,
    PSTR String
    )

/*++

Routine Description:

    This routine prints a string to the output file, converting variable
    expressions into proper Ninja format, and escaping any other characters
    that would cause issues.

Arguments:

    File - Supplies a pointer to the file to print to.

    String - Supplies a pointer to the string to print.

Return Value:

    None.

--*/

{

    PSTR Copy;
    CHAR Original;
    PSTR Variable;

    Copy = strdup(String);
    if (Copy == NULL) {
        return;
    }

    String = Copy;
    while (*String != '\0') {
        if (*String != '$') {

            //
            // Some special characters need to be escaped.
            //

            if ((*String == '\n') || (*String == ' ') || (*String == ':')) {
                fputc('$', File);
            }

            fputc(*String, File);
            String += 1;
            continue;
        }

        String += 1;

        //
        // A double dollar is just a literal dollar sign.
        //

        if (*String == '$') {
            fputc(*String, File);
            fputc(*String, File);
            String += 1;
            continue;
        }

        //
        // A dollar sign followed by // prints the source root, and ^/ prints
        // the build root.
        //

        if ((*String == '/') && (*(String + 1) == '/')) {
            MingenNinjaPrintTreeRoot(File, MingenSourceTree);
            String += 2;
            continue;
        }

        if ((*String == '^') && (*(String + 1) == '/')) {
            MingenNinjaPrintTreeRoot(File, MingenBuildTree);
            String += 2;
            continue;
        }

        //
        // A dollar sign plus some non-variable-name character is also just
        // passed over literally.
        //

        if (!MINGEN_IS_NAME0(*String)) {
            fputc('$', File);
            fputc(*String, File);
            String += 1;
            continue;
        }

        //
        // Get to the end of the variable name.
        //

        Variable = String;
        while (MINGEN_IS_NAME(*String)) {
            String += 1;
        }

        //
        // Temporarily terminate the name, and compare it against the special
        // IN and OUT variables, which substitute differently.
        //

        Original = *String;
        *String = '\0';
        if (strcasecmp(Variable, "in") == 0) {
            fprintf(File, "%s", MINGEN_NINJA_INPUTS);

        } else if (strcasecmp(Variable, "out") == 0) {
            fprintf(File, "%s", MINGEN_NINJA_OUTPUT);

        //
        // Print the variable reference in the normal Ninja way.
        //

        } else {
            fprintf(File, MINGEN_NINJA_VARIABLE, Variable);
        }

        *String = Original;
    }

    free(Copy);
    return;
}

VOID
MingenNinjaPrintTargetFile (
    FILE *File,
    PMINGEN_CONTEXT Context,
    PMINGEN_TARGET Target
    )

/*++

Routine Description:

    This routine prints a target's output file name.

Arguments:

    File - Supplies a pointer to the file to print to.

    Context - Supplies a pointer to the application context.

    Target - Supplies a pointer to the target to print.

Return Value:

    None.

--*/

{

    if ((Target->Tool != NULL) &&
        (strcmp(Target->Tool, "phony") == 0)) {

        MingenNinjaPrintWithVariableConversion(File, Target->Output);
        return;
    }

    MingenNinjaPrintTreeRoot(File, Target->Tree);
    MingenNinjaPrintWithVariableConversion(File, Target->Output);
    return;
}

VOID
MingenNinjaPrintSource (
    FILE *File,
    PMINGEN_CONTEXT Context,
    PMINGEN_SOURCE Source
    )

/*++

Routine Description:

    This routine prints a source's file name.

Arguments:

    File - Supplies a pointer to the file to print to.

    Context - Supplies a pointer to the application context.

    Source - Supplies a pointer to the source file information.

Return Value:

    None.

--*/

{

    MingenNinjaPrintTreeRoot(File, Source->Tree);
    MingenNinjaPrintWithVariableConversion(File, Source->Path);
    return;
}

VOID
MingenNinjaPrintTreeRoot (
    FILE *File,
    MINGEN_DIRECTORY_TREE Tree
    )

/*++

Routine Description:

    This routine prints the tree root shorthand for the given tree.

Arguments:

    File - Supplies a pointer to the file to print to.

    Tree - Supplies the directory tree root to print.

Return Value:

    None.

--*/

{

    switch (Tree) {
    case MingenSourceTree:
        fprintf(File, MINGEN_NINJA_VARIABLE "/", MINGEN_VARIABLE_SOURCE_ROOT);
        break;

    case MingenBuildTree:
        fprintf(File, MINGEN_NINJA_VARIABLE "/", MINGEN_VARIABLE_BUILD_ROOT);
        break;

    case MingenAbsolutePath:
        break;

    default:

        assert(FALSE);

        break;
    }

    return;
}

VOID
MingenNinjaPrintConfig (
    FILE *File,
    PMINGEN_CONTEXT Context,
    PMINGEN_TARGET Target
    )

/*++

Routine Description:

    This routine prints a target's configuration dictionary.

Arguments:

    File - Supplies a pointer to the file to print to.

    Context - Supplies a pointer to the application context.

    Target - Supplies an optional pointer to the target whose configuration
        should be printed. If NULL is supplied, then the global configuration
        is printed.

Return Value:

    None.

--*/

{

    PCHALK_OBJECT Config;
    PLIST_ENTRY CurrentEntry;
    PCHALK_DICT_ENTRY Entry;
    INT Status;
    PCHALK_OBJECT Value;

    if (Target != NULL) {
        Config = Target->Config;

    } else {
        Config = Context->GlobalConfig;
    }

    if (Config == NULL) {
        return;
    }

    assert(Config->Header.Type == ChalkObjectDict);

    CurrentEntry = Config->Dict.EntryList.Next;
    while (CurrentEntry != &(Config->Dict.EntryList)) {
        Entry = LIST_VALUE(CurrentEntry, CHALK_DICT_ENTRY, ListEntry);
        CurrentEntry = CurrentEntry->Next;
        Value = Entry->Value;
        if (Entry->Key->Header.Type != ChalkObjectString) {
            fprintf(stderr,
                    "Error: Skipping config object with non-string key.\n");

            continue;
        }

        //
        // Quietly ignore nulls.
        //

        if (Value->Header.Type == ChalkObjectNull) {
            continue;
        }

        //
        // Noisily ignore other types.
        //

        if ((Value->Header.Type != ChalkObjectString) &&
            (Value->Header.Type != ChalkObjectInteger) &&
            (Value->Header.Type != ChalkObjectList)) {

            fprintf(stderr,
                    "Error: Skipping config key %s: unsupported type.\n",
                    Entry->Key->String.String);

            continue;
        }

        if (Target != NULL) {
            fprintf(File, "    ");
        }

        fprintf(File, "%s = ", Entry->Key->String.String);
        Status = MingenNinjaPrintConfigValue(File, Value);
        if (Status != 0) {
            fprintf(stderr,
                    "Error: Skipping some values for key %s.\n",
                    Entry->Key->String.String);
        }

        fprintf(File, "\n");
    }

    return;
}

INT
MingenNinjaPrintConfigValue (
    FILE *File,
    PCHALK_OBJECT Value
    )

/*++

Routine Description:

    This routine prints a configuration value.

Arguments:

    File - Supplies a pointer to the file to print to.

    Value - Supplies a pointer to the object to print.

Return Value:

    0 on success.

    -1 if some entries were skipped.

--*/

{

    UINTN Index;
    INT Status;
    INT TotalStatus;

    TotalStatus = 0;
    if (Value->Header.Type == ChalkObjectList) {

        //
        // Recurse to print every object on the list, separated by a space.
        //

        for (Index = 0; Index < Value->List.Count; Index += 1) {
            Status = MingenNinjaPrintConfigValue(File,
                                                 Value->List.Array[Index]);

            if (Status != 0) {
                TotalStatus = Status;
            }

            if (Index != Value->List.Count - 1) {
                fprintf(File, " ");
            }
        }

    } else if (Value->Header.Type == ChalkObjectInteger) {
        fprintf(File, "%lld", Value->Integer.Value);

    } else if (Value->Header.Type == ChalkObjectString) {
        MingenNinjaPrintWithVariableConversion(File, Value->String.String);

    } else {
        TotalStatus = -1;
    }

    return TotalStatus;
}

