/*** file.c - file management
*
*   The internal file structure uses a combination of local memory
*   (managed by LMAlloc and free) and virtual memory (managed by malloc/ffree
*   and (pb|VA)To(pb|VA)).
*
*   We maintain one record for each file that Z has "in memory".  If a file
*   appears in multiple windows, there is only one record for that file.
*   Each window is treated as a separate instance of the editor, with a
*   separate record for each file that is present in that window.
*
*   Graphically, this appears as follows:
*
*    WinList (set of windows on the screen) 0 ... cWin-1
*   +---------------+---------------+---------------+---------------+
*   |   Window 1    |   Window 2    |   Window 3    |   Window 4    |
*   |               |               |               |               |
*   |windowType     |               |               |               |
*   |               |               |               |               |
*   |pInstance-+    |pInstance-+    |pInstance-+    |pInstance-+    |
*   +----------|----+----------|----+----------|----+----------|----+
*              |               v               v               |
*              v              ...             ...              v
*     +-------------+          pFileHead              +-------------+
*     |instanceType |              |                  |instanceType |
*     |             |   +----------+-----------+      |             |
*   +--pNext        |   |          v           |    +--pNext        |
*   | |pFile------------+   +-------------+    |    | |pFile        |
*   | +-------------+       |fileType     |    |    | +-------------+
*   |                       |             |    |    |
*   +------+              +--pFileNext    |    |    +------+
*          |           +-----pName        |    |           |
*          v           |  | +-------------+    |           v
*     +-------------+  |  |                    |      +-------------+
*     |instanceType |  |  |                    |      |instanceType |
*     |             |  |  |                    |      |             |
*   +--pNext        |  |  +--------+           |    +--pNext        |
*   | |pFile----+   |  |           |           +-------pFile        |
*   | +---------|---+  |           v                | +-------------+
*   |           |      |    +-------------+         |
*   +------+    v      |    |fileType     |         +------+
*          |   ...     |    |             |                |
*          v           |  +--pFileNext    |                v
*         ...          |  | |pName        |               ...
*          +-----------+  | +-------------+
*          |              |
*          v              |
*       +--------+        |
*       |filename|        +--------+
*       +--------+                 |
*                                  v
*                                 ...
*
*   Modifications:
*
*       26-Nov-1991 mz  Strip off near/far
*
*************************************************************************/

#include "z.h"

#define DIRTY       0x01                /* file had been modified       */
#define FAKE        0x02                /* file is a pseudo file        */
#define REAL        0x04                /* file has been read from disk */
#define DOSFILE     0x08                /* file has CR-LF               */
#define TEMP        0x10                /* file is a temp file          */
#define NEW         0x20                /* file has been created by editor*/
#define REFRESH     0x40                /* file needs to be refreshed   */
#define READONLY    0x80                /* file may not be editted      */



#define DEBFLAG FILEIO

/*** AutoSave - take current file and write it out if necessary
*
* AutoSave is called when it makes sense to be paranoid about saving the
* file.  We save the file only when autosaving is enabled and when the
* file is real and dirty.
*
* Input:
*  none
*
* Output:
*  none
*
*************************************************************************/
void
AutoSave (
    void
    ) {
    AutoSaveFile (pFileHead);
}


/*** AutoSaveFile - AutoSave a specific file
*
* Called when it makes sense to be paranoid about saving a specific file. We
* save the file only when autosaving is enabled and when the file is real and
* dirty.
*
* Input:
*  pFile        = File to be autosaved
*
* Output:
*  Returns nothing
*
*************************************************************************/
void
AutoSaveFile (
    PFILE   pFile
    ) {
    if (fAutoSave && (FLAGS(pFile) & (DIRTY | FAKE)) == DIRTY) {
        fSyncFile (pFile, TRUE);
        FileWrite (NULL, pFile);
    }
}



/*  GetFileTypeName - return the text corresponding to the file type
 *
 *  GetFileTypeName takes the file type as set in the file structure of the
 *  current file and returns the textual string corresponding to that type.
 *
 *  returns         character pointer to the type-specific text
 */
char *
GetFileTypeName (
    void
    ) {
    if (TESTFLAG (FLAGS (pFileHead),FAKE)) {
        return "pseudo";
    }
    return mpTypepName[FTYPE (pFileHead)];
}



/*  SetFileType - set the file type of a file based upon its extension
 *
 *  pFile           pointer to file whose type will be determined
 */
void
SetFileType (
    PFILE pFile
    ) {
    pathbuf fext;
    REGISTER int i;

    extention (pFile->pName, fext);

    for (i = 0; ftypetbl[i].ext; i++) {
        if (!strcmp (ftypetbl[i].ext, (char *)&fext[1])) {
            break;
        }
    }

    FTYPE(pFile) = ftypetbl[i].ftype;
}




/*  fChangeFile  - change the current file, drive or directory.  We form the
 *  canonicalized name and attempt to find it in our internal list.  If
 *  present, then things are simple:  relink it to the head of the current
 *  window instance set.  If not present, then we need to read it in.
 *
 *  The actual algorithm is much simpler:
 *
 *      If file not in file list then
 *          create new entry in file list
 *      Find file in file list
 *      If file not in window instance list then
 *          add file to top of window instance list
 *      while files in window instance list do
 *          select top file
 *          if file is in memory then
 *              change succeeded
 *          else
 *          if read in succeeds then
 *              change succeeded
 *          pop off top file
 *      change failed
 *
 *
 *  fShort      TRUE => allow searching for short names
 *  name        name of file.
 *
 *  Returns:    TRUE if change succeeded
 *              FALSE otherwise
 */
flagType
fChangeFile (
    flagType  fShort,
    char      *name
    ) {

    PFILE    pFileTmp;
    pathbuf  bufCanon;
    flagType fRead;
    char     *p;

    //
    // If they explicitly specified .\ skip shortname checks
    //
    if (name[0] == '.' && name[1] == '\\') {
        fShort = FALSE;
        }

    //
    //  Turn file name into canonical form
    //

    if (!CanonFilename (name, bufCanon)) {

        //
        // We may have failed because a drive or directory
        // went away.  If the file named is on the file
        // list, we remove it.
        //

        printerror ("Cannot access %s - %s", name, error () );

        pFileTmp = FileNameToHandle (name, (fShort && fShortNames) ? name : NULL);
        if (pFileTmp != NULL) {
            RemoveFile (pFileTmp);
        }

        return FALSE;
    }

    //
    //  name     has the input name
    //  bufCanon has the full "real" name
    //
    //  Check to see if the file is in the current file set
    //

    pFileTmp = FileNameToHandle (bufCanon, (fShort && fShortNames) ? name : NULL);

    if (pFileTmp == NULL) {

        //
        //  File not loaded.  If it is a directory, change to it
        //

        if (strlen (bufCanon) == 2 && bufCanon[1] == ':') {
            bufCanon[2] = '\\';
        }

        if (_chdir (bufCanon) != -1) {
            domessage ("Changed directory to %s", bufCanon);
            return TRUE;
        }

        //
        //  Must be a file.  Create a new internal file for it
        //
        pFileTmp = AddFile (bufCanon);
    }

    //
    //  Bring the found file to the top of the MRU list
    //

    pFileToTop (pFileTmp);

    //
    // if the file is not currently in memory, read it in
    //
    domessage (NULL);

    if (((FLAGS (pFileHead) & (REAL|REFRESH)) == REAL)
        || (fRead = FileRead (pFileHead->pName, pFileHead, TRUE))) {

        //  If we just read in the file AND the file is new then
        //  reset cached location to TOF.
        //
        if (fRead && TESTFLAG (FLAGS (pFileHead), NEW)) {
            YCUR(pInsCur) = 0;
            XCUR(pInsCur) = 0;
        }
        fSyncFile (pFileHead, TRUE);
        cursorfl (pInsCur->flCursorCur);
        fInitFileMac (pFileHead);

        //
        //  Set the window's title
        //
        p = pFileHead->pName + strlen(pFileHead->pName);
        while ( p > pFileHead->pName && *p != '\\' ) {
            p--;
        }
        if ( *p == '\\' ) {
            p++;
        }
        sprintf( bufCanon, "%s - %s", pOrigTitle, p );
        SetConsoleTitle( bufCanon );
        return TRUE;
    }

    // The file was not successfully read in.  Remove this instance and
    // return the indicated error.
    //
    RemoveTop ();

    return FALSE;
}




/*** fInitFileMac - Initialize macros associated with a file
*
*  Sets the curfile family of macros, and attempts to read any extension-
*  specific section from tools.ini.
*
* Input:
*  pFileNew     = File to set information for
*
* Output:
*  Returns TRUE if TOOLS.INI section found, else FALSE
*
*************************************************************************/
flagType
fInitFileMac (
    PFILE   pFileNew
    ) {

    char  fbuf[ 512 ];

    strcpy (fbuf, pFileNew->pName);
    FmtAssign ("curFile:=\"%s\"", DoubleSlashes (fbuf));

    filename (pFileNew->pName, fbuf);
    FmtAssign ("curFileNam:=\"%s\"", fbuf);

    if (!extention (pFileNew->pName, fbuf)) {
        fbuf[0] = '.';
        fbuf[1] = '\0';
    }
    FmtAssign ("curFileExt:=\"%s\"", fbuf);

    return InitExt (fbuf);
}




/*  AddFile - create a named file buffer
 *
 *  Create and initialize a named buffer.  The contents are initially
 *  empty.
 *
 *  p           character pointer to name
 *
 *  returns     file handle to internal file structure
 */
PFILE
AddFile (
    char *p
    ) {

    PFILE pFileTmp;
    PFILE pFileSrch;

#ifdef DEBUG
    /*
     * assert we're not attempting to add a duplicate entry
     */
    for (pFileTmp = pFileHead;
         pFileTmp != NULL;
         pFileTmp = pFileTmp->pFileNext) {

        assert (_stricmp ((char *)(pFileTmp->pName), p));
    }
#endif

    pFileTmp = (PFILE) ZEROMALLOC (sizeof (*pFileTmp));
#ifdef DEBUG
    pFileTmp->id = ID_PFILE;
#endif
    pFileTmp->pName = ZMakeStr (p);

    /*
     * Everything that we explicitly set NULL, we can assume, as LMAlloc init's
     * the allocated PFILE to all nulls.
     *
     *  pFileTmp->pFileNext = NULL;
     *  pFileTmp->cLines = 0;
     *  pFileTmp->refCount = 0;
     *  FLAGS(pFileTmp) = FALSE;
     *  pFileTmp->cUndo = 0;
     */
    pFileTmp->plr      = NULL;
    pFileTmp->pbFile   = NULL;
    pFileTmp->vaColor  = (PVOID)(-1L);
    pFileTmp->vaHiLite = (PVOID)(-1L);
        pFileTmp->vaMarks  = NULL;
    pFileTmp->vaUndoCur = pFileTmp->vaUndoHead = pFileTmp->vaUndoTail = (PVOID)(-1L);

    CreateUndoList (pFileTmp);
    /*
     * Place the file at the end of the pFile list
     */
    if (pFileHead == NULL) {
        pFileHead = pFileTmp;
    } else {
        for (pFileSrch = pFileHead;
             pFileSrch->pFileNext;
             pFileSrch = pFileSrch->pFileNext) {
            ;
        }
        pFileSrch->pFileNext = pFileTmp;
    }

    SetFileType (pFileTmp);

    return pFileTmp;
}




/*  IncFileRef - note a new reference to a file
 */
void
IncFileRef (
    PFILE pFile
    ) {
    pFile -> refCount++;
}





/*  DecFileRef - remove a reference to a file
 *
 *  When the reference count goes to zero, we remove the file from the memory
 *  set
 */
void
DecFileRef (
    PFILE pFileTmp
    ) {
    if (--(pFileTmp->refCount) <= 0) {
        RemoveFile (pFileTmp);
    }
}



/*  FileNameToHandle - return handle corresponding to the file name
 *
 *  FileNameToHandle is used to locate the buffer pointer corresponding to
 *  a specified file.  Short names are allowed.  If the input name is 0-length
 *  we return the current file.
 *
 *  pName       character pointer to name being located.  Case is significant.
 *  pShortName  short name of file.  This may be NULL
 *
 *  Returns     handle to specified file (if found) or NULL.
 */
PFILE
FileNameToHandle (
    char const *pName,
    char const *pShortName
    ) {

    char const *pBaseShortName;
    char const *pFileName;
    PFILE pFileTmp;
    pathbuf nbuf;
    size_t n, o;

    if (pName[0] == 0) {
        return pFileHead;
    }

    for (pFileTmp = pFileHead; pFileTmp != NULL; pFileTmp = pFileTmp->pFileNext)
        if (!_stricmp (pName, pFileTmp->pName))
            return pFileTmp;

    if ( pShortName != NULL && GetFileAttributes( pName ) == -1) {
        pBaseShortName = pShortName;
        pFileName = pShortName;
        while (pFileName = strchr(pFileName, '\\')) {
            pBaseShortName = ++pFileName;
        }
        n = strlen (pBaseShortName);
        for (pFileTmp = pFileHead->pFileNext; pFileTmp != NULL; pFileTmp = pFileTmp->pFileNext) {
            if (fileext (pFileTmp->pName, nbuf) &&
                !_strnicmp (pBaseShortName, nbuf, n)) {
                n = (size_t)(pBaseShortName - pShortName);
                if (n == 0) {
                    return pFileTmp;
                }

                if (path (pFileTmp->pName, nbuf) &&
                    (o = strlen(nbuf)-n) > 0 &&
                    !_strnicmp(nbuf+o, pShortName, n)
                   ) {
                    return pFileTmp;
                }
            }
        }

        if (fileext (pFileHead->pName, nbuf) &&
            !_strnicmp (pBaseShortName, nbuf, n)) {
            return pFileHead;
        }
    }

    return NULL;
}



/*** pFileToTop - make the specified file the top of the current window
*
* Search  the instance list in the current window for the file. If it is
* found, relink it to be the top one. Otherwise, allocate a new instance for
* it  and  place it at the top of the instance list. Also bring the file to
* the top of the pFileHead file list. Ensure that it is on the list to begin
* with.
*
* Input:
*  pFileTmp     = file to bring to top
*
* OutPut:
*  Returns FALSE if the pFile is invalid or NULL
*
*************************************************************************/
flagType
pFileToTop (
    PFILE pFileTmp
    ) {

    EVTargs e;
    PINS    pInsLast        = (PINS) &pInsCur;
    PINS    pInsTmp         = pInsCur;
    PFILE   pFilePrev;

    assert (_pfilechk());
    assert (_pinschk(pInsCur));

    /*
     * if we're about to lose focus, declare it
     */
    if (pFileTmp != pFileHead) {
        e.pfile = pFileHead;
        DeclareEvent (EVT_LOSEFOCUS,(EVTargs *)&e);
    }

    /*
     * Move file to head of file list. Ensure, at the same time, that the file
     * is in fact ON the list, and declare the event if in fact it is moved.
     */
    if (pFileTmp != pFileHead) {
        for (pFilePrev = pFileHead;
                         pFilePrev && (pFilePrev->pFileNext != pFileTmp);
             pFilePrev = pFilePrev->pFileNext ) {
            ;

        }

        if (!pFilePrev) {
            return FALSE;
        }

        pFilePrev->pFileNext = pFileTmp->pFileNext;
        pFileTmp->pFileNext = pFileHead;
        pFileHead = pFileTmp;

        e.pfile = pFileHead;
        DeclareEvent (EVT_GETFOCUS,(EVTargs *)&e);
    }

    /*
     * pFileTmp now points to a file structure for the correct file. Try to find
     * an instance of the file in the current window. If not in the instance
     * list, allocate it. If it is in the instance list, remove it.
     */
    while (pInsTmp != NULL) {
        if (pInsTmp->pFile == pFileTmp) {
            break;
        }
        pInsLast = pInsTmp;
        pInsTmp = pInsTmp->pNext;
    }

    if (pInsTmp == NULL) {
        pInsTmp = (PINS) ZEROMALLOC (sizeof (*pInsTmp));
        pInsTmp->pFile = pFileTmp;
#ifdef DEBUG
        pInsTmp->id = ID_INSTANCE;
#endif
        IncFileRef (pFileTmp);
    } else {
        pInsLast->pNext = pInsTmp->pNext;
    }
    /*
     * Regardless, then, of where it came from, place the new instance back onto
     * the head of the list
     */
    pInsTmp->pNext = pInsCur;
    WININST(pWinCur) = pInsCur = pInsTmp;

    SETFLAG(fDisplay, RCURSOR | RSTATUS);
    newscreen ();

    return TRUE;

}



/*  RemoveTop - removes the top file in the current instance list
 *              If there is no next file, leave
 */
void
RemoveTop (
    void
    ) {
    PINS    pInsTmp = pInsCur;

    WININST(pWinCur) = pInsCur = pInsCur->pNext;
    FREE ((char *) pInsTmp);
    DecFileRef (pFileHead);
    if (pInsCur) {
        pFileToTop (pInsCur->pFile);
    }
}




/*** RemoveFile  - free up all resources attached to a particular file
*
* Purpose:
*
*   To free all memory used to keep track of a file.  If the file still
*   appears in some instance lists, it is removed from them.
*
* Input:
*
*   pFileRem - File in question
*
* Output:
*
*   Returns TRUE.
*
* Exceptions:
*
* Notes:
*
*************************************************************************/
void
RemoveFile (
    PFILE    pFileRem
    ) {

    PFILE pFilePrev = (PFILE) &pFileHead;
    PFILE pFileTmp = pFileHead;

    if (pFileRem->refCount > 0) {
        RemoveInstances (pFileRem);
    }

    while (pFileTmp != pFileRem) {
        pFilePrev = pFileTmp;
        pFileTmp = pFileTmp->pFileNext;
        if (pFileTmp == NULL) {
            IntError ("RemoveFile can't find file");
        }
    }


    /*
     * It's important that pFileNext be the first field in a pfile, and we assert
     * that here. This allows us to not special case pFileHead, but adjust it by
     * treating it as the pFileNext of a non-existant structure.
     */
    assert ((void *)&(pFilePrev->pFileNext) == (void *)pFilePrev);
    pFilePrev->pFileNext = pFileTmp->pFileNext;

    FreeFileVM (pFileTmp);

    FREE (pFileTmp->pName);

#if DEBUG
    pFileTmp->id = 0;
#endif

    FREE ((char *) pFileTmp);

    if (pFileTmp == pFileIni) {
        pFileIni = NULL;
    }
}



/*** RemoveInstances - Remove all instances of a file
*
* Purpose:
*
*  Used by RemoveFile to make sure that there are no file instances
*  referring to a given file
*
* Input:
*  pFile        = File in question
*
* Output:
*  Returns nothing
*
*************************************************************************/
void
RemoveInstances (
    PFILE   pFile
    ) {

    PINS    pIns;
    PINS    pInsPrev;
    PWND    pWndCur;

    for (pWndCur = &WinList[0];
         pWndCur < &WinList[cWin];
         pWndCur++) {

        pInsPrev = NULL;
        pIns = WININST(pWndCur);
        while (pIns) {

            /*
             * assert not an infinite loop
             */
            assert (!pInsPrev || (pIns != WININST (pWndCur)));

            if (pIns->pFile == pFile) {
                if (!pInsPrev) {
                    WININST (pWndCur) = pIns->pNext;
                } else {
                    pInsPrev->pNext = pIns->pNext;
                }
                {
                    PINS pInsTmp = pIns;
                    pIns = pIns->pNext;
                    FREE(pInsTmp);
                }
            } else {
                pInsPrev = pIns;
                pIns = pIns->pNext;
            }
        }
        assert (_pinschk (WININST (pWndCur)));
    }
    //
    // If the resulting instance list for the current window becomes empty,
    // bring up the <untitled> file in it.
    //
    if (!(pInsCur = WININST (pWinCur))) {
        fChangeFile (FALSE, RGCHUNTITLED);
    }
}




/*  fSyncFile - Attempt to make logical file and physical file the same
 *
 *  When editing in a network or multi-tasking environment, we need to make
 *  sure that changes made underneath us are properly reflected to the
 *  user.  We do this by snapshotting the time-of-last-write and periodically
 *  comparing it with the version on disk.  When a mismatch is found, we
 *  prompt the user and give him the opportunity to reread the file
 *
 *  pFileLoc    file structure of interest
 *  fPrompt     TRUE => prompt user for permission to refresh, else just
 *              refresh.
 *
 *  returns     TRUE iff the logical file and the physical file are the same.
 */
flagType
fSyncFile (
    PFILE pFileLoc,
    flagType fPrompt
    ) {
    if (pFileLoc == NULL) {
        pFileLoc = pFileHead;
    }

    switch (FileStatus (pFileLoc, NULL)) {

    case FILECHANGED:
        if (!confirmx("%s has been changed.  Refresh? ", pFileLoc->pName)) {
            /* No, validate this edit session */
            SetModTime (pFileLoc);
            return FALSE;
        }

        FileRead (strcpy( buf, pFileLoc->pName ), pFileLoc, TRUE);
        RSETFLAG (FLAGS (pFileLoc), DIRTY);
        SETFLAG (fDisplay, RSTATUS);
        return TRUE;

    case FILEDELETED:
        domessage ("File has been deleted");
        break;

    default:
        break;

    }
    return TRUE;
}




/*  FileStatus - compare logical info about a file with file on disk
 *
 *  Compare the last modified time with the last snapshot.  If the filename
 *  contains metachars, the file is not believed to have changed.  Further, if
 *  the file is a pseudo file, it cannot have changed.
 *
 *  pFile       file of interest (contains mod time)
 *  pName       name of file to examine (when writing to diff. name)
 *
 *  returns     FILECHANGED if timestamps differ
 *              FILEDELETED if file on disk does not exist
 *              FILESAME    if timestamps are the same
 */
int
FileStatus (
    PFILE pFile,
    char *pName
    ){

    time_t modtime;

    if (TESTFLAG(FLAGS(pFile),FAKE)) {
        return FILESAME;
    }

    if (pName == NULL) {
        pName = pFile->pName;
    }

    if (*strbscan (pName, "?*") != 0) {
        return FILESAME;
    }

    if ((modtime = ModTime (pName)) == 0L) {
        return FILEDELETED;
    }

    if (pFile->modify != modtime) {
        return FILECHANGED;
    }

    return FILESAME;
}




/*  SetModTime - Snapshot a file's last-modification time
 *
 *  pFile       file of interest
 */
void
SetModTime (
    PFILE pFile
    ) {
    pFile->modify = ModTime (pFile->pName);
}



/*  ModTime - Return the time of last modification for a file
 *
 *  If the file does not exist or contains meta chars, return 0 as the time-
 *  stamp.
 *
 *  pName       character pointer to file name
 *
 *  Returns     last modification time of file.
 */

time_t
ModTime (
    char *pName
    ) {

    struct _stat statbuf;

    if (*strbscan (pName, "?*")) {
        return 0L;
    }

    if (_stat (pName, &statbuf) == -1) {
        return 0L;
    }

    return statbuf.st_mtime;

}
