/*
 * GlobArgs
 *
 * This module processes all recursive and wildcard pathname arguments.
 */

#include "precomp.h"
#pragma hdrstop
EnableAssert

char szStar[] = "*.*";

private F FRecurseFiles(P1(AD *));
private F FRecDir(P2(AD *pad, NE *pneFiles));
private F FDoNe(P2(AD *, NE *));
private F FCallCmd(P1(AD *));
private F FMatched(P0());
private F FDoFile(AD *, NE *, TD *, BOOL);
private F FDoPending(P1(AD *));
private F FWild(P3(AD *, char *, TD *));
private NE *PneMatch(P5(AD *, char *, F, F, F));
private F FGetStatus(P1(AD *));
private void FlushCachedStatus(P1(AD *));

F fLocal;    /* Match wildcards against local files? */

/* Recurse or glob all arguments. */
void
GlobArgs(
    AD * pad)
{
    NE *pne;
    F fOk = fTrue;

    fLocal = (pad->pecmd->gl&fglLocal) != 0;

    /* Initialize file accumulation. */
    pad->pneFiles = 0;
    PthCopy(pad->pthFiles, "");
    pad->pthGlobSubDir = pad->pthUSubDir + CchOfPth(pad->pthUSubDir);

    if (pad->flags&flagAll|| pad->pecmd->gl&fglAll)
        CreatePeekThread(pad);

    /* If we are matching wildcard files, directories do not apply.
     * For instance, consider delfile -r sub vs. delfile -r foo* sub.
     * In the latter case, we are not indicating to recursively remove
     * everything in sub, just matching patterns below it.  It is a
     * real pity that we didn't use a different flag for patterns
     * than for -a/-r.
     */
    if (pad->szPattern != 0)
        pad->pecmd->gl &= ~fglDirsToo;

    if (pad->flags&flagAll || pad->pecmd->gl&fglAll) {
        ChngDir(pad, "/");
        pad->pthGlobSubDir = pad->pthUSubDir + 1;
        fOk = FRecurseFiles(pad);
    }
    else if (pad->pneArgs == 0) {
        if (pad->flags&flagRecursive)
            fOk = FRecurseFiles(pad);
        else if (!(pad->pecmd->gl&fglFiles))
            fOk = FCallCmd(pad);
        else {
            Warn("no files specified\n");
            Usage(pad);
        }
    }

    /* Perform wildcard matching on every pathname argument. */
    else ForEachNeWhileF(pne, pad->pneArgs, fOk) {
        F fTLocal;
        TD *ptd;

        CheckForBreak();

        /* Set fLocal if argument begins with "./". */
        fTLocal = fLocal;
        fLocal |= strncmp(SzOfNe(pne), "./", 2) == 0;

        /* Do wildcard matching on this pathname.
         * Use tdMin if no time was specified with "@time".
         */
        ptd = pne->u.tdNe.tdt != tdtNone ? &pne->u.tdNe : &pad->tdMin;
        fOk &= FWild(pad, SzOfNe(pne), ptd);

        if (!FMatched())
            Error("%s: not found\n", SzOfNe(pne));

        fLocal = fTLocal;
    }

    if (fOk)
        FDoPending(pad);        /* Flush last files. */

    FlushCachedStatus(pad);

}


/* Recursively process all files in and under the current directory.
 * If pad->pecmd->gl&fglDirsToo, FDoFile the directory too;
 * pad->pecmd->gl&fglTopDown determines whether we do the directory
 * before or after its contents.
 *
 * Regardless of gl&fglFiles, we match against files if a pattern is specified.
 */
private F
FRecurseFiles(
    AD *pad)
{
    NE *pneDirs = 0;                /* Subdirectories */
    NE *pneFiles = 0;               /* Files (+ dirs?) in this directory */
    NE *pne;
    F fOk = fTrue;

    CheckForBreak();

    pneDirs = PneMatch(pad, szStar, fLocal, fTrue, fFalse);
    if (pad->pecmd->gl&fglFiles || pad->szPattern != 0)
        pneFiles = PneMatch(pad, pad->szPattern ? pad->szPattern:szStar,
                      fLocal, !!(pad->pecmd->gl&fglDirsToo), fTrue);

    if (pad->pecmd->gl&fglTopDown)
        fOk = FRecDir(pad, pneFiles);

    ForEachNeWhileF(pne, pneDirs, fOk) {
        ChngDir(pad, SzOfNe(pne));
        fOk = FRecurseFiles(pad);
        ChngDir(pad, "..");
    }

    if (fOk && !(pad->pecmd->gl&fglTopDown))
        fOk = FRecDir(pad, pneFiles);

    FreeNe(pneDirs);
    if (pneFiles)
        FreeNe(pneFiles);

    return fOk;
}


private F
FRecDir(
    AD *pad,
    NE *pneFiles)   /* Files (+ dirs?) in this directory */
{
    if (pad->pecmd->gl&fglFiles || pneFiles != 0) {
        F fOk = FDoNe(pad, pneFiles);
        return fOk;
    }
    else if (!pad->szPattern) {
        /* Call the command for this directory, with no file list.
         * First we call FDoPending, because there may be files
         * pending, i.e. "addfile -r file dir"; file is pending, so
         * we call it; then call dir recursively.  FDoPending does
         * nothing if nothing is pending.
         */
        return FDoPending(pad) && FCallCmd(pad);
    }

    /* else !fglFiles && pad->szPattern && pneFiles == 0 (no matches) */
}


/* Process all files in the list */
private F
FDoNe(
    AD *pad,
    NE *pneList)
{
    NE *pne;

    ForEachNe(pne, pneList) {
        if (!FDoFile(pad, pne, &pad->tdMin, FALSE))
            return fFalse;
        }
    return fTrue;
}


/* Call the command's dir function */
private F
FCallCmd(
    AD *pad)
{
    PTH pth[cchPthMax];

    // compute maximum path length allowance
    // magic number 18 = 12 + 5 + 1
    // 12 = 8.3 file name
    // 5 = "diff/"                  diff is larger than src or etc
    // 1 = "/" (after project name)

    if ((strlen(pad->pthSRoot)+
         strlen(pad->pthSSubDir)+
         strlen(pad->nmProj) + 18) >= cchPthMax-1 ||
        (strlen(pad->pthURoot)+strlen(pad->pthUSubDir)+12)>=cchPthMax-1) {

        Error("<SLM PATH-LIMIT EXCEEDED! STOP.>\n%&C\n", pad);
        return fFalse;
    }

    if (!FPthExists(PthForStatus(pad, pth), fFalse)) {
        UINT ulSleepTime = 10;

        Warn("%&/C is not a directory of SLM installation %&/S, project %&P\n", pad, pad, pad);
        SleepCsecs(ulSleepTime);
        if (!FPthExists(PthForStatus(pad, pth), fFalse)) {
            Warn("%&/C is not a directory of SLM installation %&/S, project %&P - skipping\n", pad, pad, pad);
            return fTrue;
        }
        PrErr("Proceeding\n");
    }

    FlushCachedStatus(pad);
    return (*pad->pecmd->pfncFDir)(pad);
}


/* Process a single file.  If it is in a new directory, call the command
 * with the collection of files that we have previously accumulated, then
 * begin accumulating again with the new file.
 */
private F
FDoFile(
    AD *pad,
    NE *pneFile,
    TD *ptd,
    BOOL fWild
    )
{
    F fOk = fTrue;
    NE *pne;
    static NE **ppneLast = 0;

    CheckForBreak();

    if (ppneLast == 0 || PthCmp(pad->pthFiles, pad->pthUSubDir) != 0) {
        fOk = FDoPending(pad);
        InitAppendNe(&ppneLast, &pad->pneFiles);

        if (!fOk)
            return fFalse;

        PthCopy(pad->pthFiles, pad->pthUSubDir);
    }

    /* Append copy of NE to end of pad->pneFiles. */
    pne = PneCopy(pneFile);
    pne->u.tdNe = *ptd;
    pne->fWild = fWild;
    AppendNe(&ppneLast, pne);

    return fOk;
}


/* Call the command with those files which have been accumulated by calls
 * to FDoFile.
 */
private F
FDoPending(
    AD *pad)
{
    F fOk = fTrue;

    if (pad->pneFiles) {
        PushDir(pad, pad->pthFiles);

        fOk = FCallCmd(pad);
        FreeNe(pad->pneFiles);
        pad->pneFiles = 0;

        PopDir(pad);
    }
    return fOk;
}


static F fMatch = fFalse;               /* has this wildcard matched yet? */

/* Return fTrue if any files matched since last call. */
private F
FMatched()
{
    F f = fMatch;

    fMatch = fFalse;
    return f;
}


/* Call FDoFile() on all pathnames matching the wildcard pathname,
 * or (if flagRecursive), call FRecurseFiles() on all matching directories.
 *
 * Each recursive invocation consumes some number of directories and one
 * wildcard component of the path.
 */
private F
FWild(
    AD *pad,
    char *szToDo,                   /* Wildcard path remaining to match. */
    TD *ptd)                        /* Optional time for this file. */
{
    char *pchSlash;                 /* Pointer to '/' in szToDo. */
    char *pchPrevSlash;             /* The preceding '/' in szToDo. */
    char *szWild;                   /* Wildcard component to match. */
    NE *pneDirs;                    /* Names matching wildcard directory. */
    NE *pneFiles;                   /* Names matching wildcard filename. */
    NE *pne;
    F fOk = fTrue;

    /* EXAMPLE: "a/b/wild*dir/file"
     * szToDo ---^
     */

    /* Search for first wildcard component of szToDo. */
    for (pchPrevSlash = 0, pchSlash = index(szToDo, '/'); pchSlash != 0;
         pchPrevSlash = pchSlash, pchSlash = index(pchSlash + 1, '/')) {
        *pchSlash = 0;          /* Temporarily terminate component. */
        if (FWildSz(szToDo)) {
            /* Found a wildcard directory component of szToDo.
             *
             * Search for directories matching the pattern, then
             * recursively call FWild on each, with what remains
             * of szToDo.
             */
            if (pchPrevSlash) {
                *pchPrevSlash = 0;

                /* EXAMPLE:  "a/b\0wild*dir\0file"
                 * szToDo ----^  ^         ^
                 * pchPrevSlash--+         +-- pchSlash
                 */
                szWild = pchPrevSlash + 1;

                PushDir(pad, szToDo);
            }
            else
                szWild = szToDo;

            /* Search for every matching directory, and recurse. */
            pneDirs = PneMatch(pad, szWild, fLocal, fTrue, fFalse);
            fOk = fTrue;
            ForEachNeWhileF(pne, pneDirs, fOk) {
                ChngDir(pad, SzOfNe(pne));
                fOk &= FWild(pad, pchSlash + 1, ptd);
                ChngDir(pad, "..");
            }

            /* Clean up. */
            FreeNe(pneDirs);
            if (pchPrevSlash) {
                *pchPrevSlash = '/';
                PopDir(pad);
            }
            *pchSlash = '/';
            return fOk;
        }
        *pchSlash = '/';
    }

    /* If the preceding loop didn't find anything, then we have a simple
     * pathname or a pathname whose last component is a wildcard.
     * The recursion stops here.
     */

    /* Extract directory component(s) of path (if any) and chdir there. */
    pchSlash = rindex(szToDo, '/');
    if (pchSlash) {
        *pchSlash = 0;

        PushDir(pad, szToDo);

        szWild = pchSlash + 1;
    }
    else
        szWild = szToDo;

    /* If the '/' is the last character, (i.e. "status dir/"):
     *   * if the command requires filenames (i.e. "out dir/"),
     *     match against "*.*"
     *   * if the command doesn't require filenames (i.e. "log dir/",
     *     directly call the command function with NO filenames.
     */
    if (*szWild == 0) {
        if (pad->pecmd->gl&fglFiles)
            szWild = szStar;
        else {
            fMatch = fTrue;
            return FDoPending(pad) && FCallCmd(pad);
        }
    }

    if (!strcmp(szWild, "*"))
        szWild = szStar;

    pneFiles = PneMatch(pad, szWild, fLocal, fTrue, fTrue);

    /* If the file doesn't necessarily exist (i.e. a log argument, etc.),
     * and if indeed it turns out not to exist, we add it anyway, so
     * long as it doesn't contain any wildcards.
     */
    if (pneFiles == 0 && pad->pecmd->gl&fglNoExist &&
        !FWildSz(szWild))
        pneFiles = PneNewNm((NM far *)szWild, strlen(szWild), faNormal);

    fMatch |= (pneFiles != 0);

    /* Process each filename. If it is a directory and the -r flag was
     * specified (i.e. in -r dir/sub*), then recurse from that directory.
     * Otherwise FDoFile it.
     */
    fOk = fTrue;
    ForEachNeWhileF(pne, pneFiles, fOk) {
        if (!FDirNe(pne)) {
            fOk = FDoFile(pad, pne, ptd, TRUE);
            continue;
        }

        /* Issue an error if the user directly specified an unexpected
         * directory name (not the result of a wildcard match).
         */
        if (!FWildSz(szWild) && !(pad->pecmd->gl&fglDirsToo) &&
              !(pad->flags&flagRecursive)) {
            AssertF(FDirNe(pne));
            Warn("ignoring directory %s\n", SzOfNe(pneFiles));
            continue;
        }

        if ((pad->pecmd->gl&(fglTopDown|fglDirsToo)) == (fglTopDown|fglDirsToo))
            fOk = FDoFile(pad, pne, ptd, TRUE);

        if (fOk && pad->flags&flagRecursive) {
            ChngDir(pad, SzOfNe(pne));
            fOk = FRecurseFiles(pad);
            ChngDir(pad, "..");
        }

        if (fOk && (pad->pecmd->gl&(fglTopDown|fglDirsToo)) == fglDirsToo)
            fOk = FDoFile(pad, pne, ptd, TRUE);
    }

    /* Clean up. */
    FreeNe(pneFiles);
    if (pchSlash) {
        *pchSlash = '/';
        PopDir(pad);
    }
    return fOk;
}


/* Return a list of all files in current directory which match this pattern
 * and are files or dirs (or either)
 */
private NE *
PneMatch(
    AD *pad,
    char *szPattern,
    F fLocal,
    F fDirs,
    F fFiles)
{
    NE *pneList = 0;
    NE **ppneLast;

    InitAppendNe(&ppneLast, &pneList);

    if (fLocal) {
        DE de;
        FA fa;
        PTH pth[cchPthMax];             /* current directory */
        char sz[cchFileMax+1];          /* one match */

        PthForUDir(pad, pth);
        OpenPatDir(&de, pth, szPattern,
           (FA)(fDirs ? (fFiles ? (faDir|faFiles) : faDir)
                  : faFiles));

        while (FGetDirSz(&de, sz, &fa)) {
            /* Lowercase, FGetDirSz always returns all uppercase. */
            LowerLsz((char far *)sz);
            AppendNe(&ppneLast, PneNewNm((NM far *)sz, strlen(sz), fa));
        }

        CloseDir(&de);
    }
    else {
        FI far *pfi;
        FI far *pfiMac;
        char sz[cchFileMax+1];

        //
        // If using IED Caching (log, status and ssync) and not in root
        // directory (so lckNil works) try to load status file
        // quickly using IED cache, which justed reads the SH, FI, 1 ED and
        // 1 FS array.  If that does not succeed go the slow way.
        //
        if (pad->pneArgs == 0 &&
            pad->flags&flagCacheIed &&
            !FTopUDir(pad) &&
            (FlushCachedStatus(pad), FLoadStatus(pad, lckNil, flsNone))) {
            //
            // Status file is loaded for both PneMatch and log/status/ssync routines.
            //
            pad->fStatusAlreadyLoaded = fTrue;
        }
        else
        if (!FGetStatus(pad))
            return (NE *)0;

        for (pfi = pad->rgfi, pfiMac = pfi + pad->psh->ifiMac;
               pfi < pfiMac; pfi++) {
            F fDir = pfi->fk == fkDir;

            SzCopyNm(sz, pfi->nmFile, cchFileMax);
            /* Use the file if the name matches and it's the type
             * of file we're looking for, unless its a deleted dir.
             */
            if (FMatch(sz, szPattern) &&
                (fDir && fDirs || !fDir && fFiles) &&
                ((pad->pecmd->cmd == cmdLog && pad->flags&flagLogDelDirToo) ||
                 !(pfi->fDeleted && fDir)))
                    AppendNe(&ppneLast,
                             PneNewNm(pfi->nmFile, cchFileMax,
                                      (FA)(fDir ? faDir
                                                : faNormal)));
            }
    }

    return pneList;
}

/*
 * Status file caching, keeps the current status file loaded as long as
 * possible.
 */

static PTH pthTag[cchPthMax]    = "";   /* empty string - invalid tag */

/* If the status file isn't already loaded into the cache, load it readonly.
 * Return fTrue if the status file is now loaded.
 */
private F
FGetStatus(
    AD *pad)
{
    PTH pth[cchPthMax];

    if (!FPthExists(PthForStatus(pad, pth), fFalse))
        return fFalse;

    /* Return if the status file for this directory is still loaded. */
    if (PthCmp(pthTag, pad->pthUSubDir) == 0)
        return fTrue;

    /* Flush the current status file. */
    FlushCachedStatus(pad);

    /* Open the status file for reading just the FI information.  Set the
     * cache tag if successful.
     */
    if (FLoadStatus(pad, lckNil, flsJustFi)) {
        PthCopy(pthTag, pad->pthUSubDir);
        return fTrue;
    }

    /* Status file wasn't cached and couldn't be loaded. */
    return fFalse;
}


/* Flush the cached status file (if any) */
private void
FlushCachedStatus(
    AD *pad)
{
    if (PthCmp(pthTag, "") != 0) {
        FlushStatus(pad);
        PthCopy(pthTag, "");
    }
}