Subroutine Base_MFS(Code, FSList, Handle, Name, FMC, Record, Status)
/***********************************************************************************************************************

    This program is proprietary and is not to be used by or disclosed to others, nor is it to be copied without written
    permission from SRP Computer Solutions, Inc.

    Name        :   Base_MFS

    Description :   Base MFS (Modified File System) shell for general use.

    Notes       :   Used to track activity in a given database table, regardless of how the table is accessed.
                    Generally the MFS remains as generic as possible and makes a call to another table-specific stored
                    procedure to handle all of the main functionality.
                    
                    MFS procedures should normally be stored in the SYSPROG application for optimum accessibility.
                    The table-specific stored procedures should be stored in the local application.

                    Some methods might need the regular name of the database table. Since the MFS routine does not
                    normally provide this information we need to track it ourselves. The OPEN.FILE method gives us
                    an opportunity to retrieve the regular name as well as the table handle. This information is then
                    stored in the /Tables/ global common for convenient reference.
                    
                    Record based actions (e.g. READ.RECORD, WRITE.RECORD, DELETE.RECORD) will be routed to table
                    specific and promoted (i.e. generic) action handlers befoe the BFS is called (Call_Next_FS internal
                    method.) The MFS argument Status can be set accordingly to determine how the rest of the action
                    chain should be executed (see the ACTION_SETUP insert for more information.)

    Parameters  :
        Code        [in] -- An integer value indicating the operation to be performed (1 = read a record, 4 = delete a
                            record, 11 = open a file, etc.)
        FSList      [in] -- The list of MFSs and the BFS name for the current file or volume. This is an @SVM delimited
                            array, with the current MFS name as the first value in the array, and the BFS name as the
                            last value.
        Handle      [in] -- The file handle of the file or media map being accessed. Note, this does contain the entire
                            handle structure that the Basic+ Open statement would provide.
        Name        [in] -- The name (key) of the record or file being accessed.
        FMC         [in] -- Various functions.
        Record      [in] -- The entire record (for record-oriented functions) or a newly-created handle (for "get
                            handle" functions).
        Status  [out/in] -- A return code indicating the success or failure of an operation.

    History     :   (Date, Initials, Notes)
        07/27/10    dmb     Original programmer
        03/26/11    dmb     Save and restore @FILE.ERROR to prevent incorrect error messages being passed down the line.
        05/03/16    dmb     [SRPFW-124] Revise the Get_Original_Record logic to call the remaing MFS chain rather
                            than just try to call the BFS directly.
        06/09/16    dmb     [SRPFW-282] Update the CLEARFILE action to gosub to Action_Chain rather than Call_Next_FS
                            so the promoted action can be invoked.
        09/18/19    dmb     [SRPFW-282] Update OPEN.FILE to set the volume based on the path in the handle
                            (Record argument).
        06/25/20    dmb     [SRPFW-282] Update OPEN.FILE to also remove the Table*Database prefix in the Record
                            argument if it exists.
        09/10/20    dmb     [SRPFW-282] Update OPEN.FILE to default Volume to REVBOOT.

***********************************************************************************************************************/

#pragma precomp SRP_PreCompiler

$insert LOGICAL
$insert FSERRORS_HDR
$insert FILE.SYSTEM.EQUATES
$insert ACTION_SETUP

Declare subroutine  SRP_Stopwatch

Actions     = 'READ_RECORD,READONLY_RECORD,WRITE_RECORD,DELETE_RECORD,LOCK_RECORD,UNLOCK_RECORD,SELECT,READNEXT,'
Actions    := 'CLEARSELECT,CLEARFILE,OPEN_FILE,CREATE_FILE,RENAME_FILE,MOVE_FILE,DELETE_FILE,OPEN_MEDIA,CREATE_MEDIA,'
Actions    := 'READ_MEDIA,WRITE_MEDIA,UNLOCK_ALL,FLUSH,INSTALL,RESERVED,RESERVED,RESERVED,OMNI_SCRIPT,CLOSE_MEDIA,'
Actions    := 'RECORD_COUNT,REMAKE_FILE,CREATE_INDEX,DELETE_INDEX,UPDATE_INDEX,SELECT_INDEX,READNEXT_INDEX'
BaseAction  = Field(Actions, ',', Code)

// Initialize the ActionFlow variable. Assume the action will chain forward.
ActionFlow  = ACTION_CONTINUE$

// Initialize the OrigRecord variable. The WRITE.RECORD and DELETE.RECORD actions will populate this.
OrigRecord  = ''

// FILE.SYSTEM.ONGOSUB has the On Code GoSub... command
$insert FILE.SYSTEM.ONGOSUB

Return


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MFS Actions
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

READ.RECORD:
    GoSub Action_Chain
return


READO.RECORD:
    GoSub Action_Chain
return


WRITE.RECORD:
    // Get the original (static) record from the database table.
    GoSub Get_Original_Record

    GoSub Action_Chain
return


DELETE.RECORD:
    // Get the original (static) record from the database table.
    GoSub Get_Original_Record

    GoSub Action_Chain
return


LOCK.RECORD:
    GoSub Call_Next_FS
return


UNLOCK.RECORD:
    GoSub Call_Next_FS
return


SELECT:
    GoSub Call_Next_FS
return


READNEXT:
    GoSub Call_Next_FS
return


CLEARSELECT:
    GoSub Call_Next_FS
return


CLEARFILE:
    GoSub Action_Chain
return


OPEN.FILE:
    // Call BFS in order to get the table handle. The regular name of the table will be returned in the Name argument
    // and the handle will be returned in the Record argument.
    GoSub Call_Next_FS

    // Load the handle and table name into the labelled common.
    If Status then
        TableName       = Name[1, '*']
        Accountname     = Name[Col2() + 1, '999']
        Volume          = Record[-1, 'B' : @TM]
        Volume          = Volume[14, 9999]
        Volume[-12, 12] = ''
        If Volume EQ '' then Volume = 'REVBOOT'
        Locate TableName in TableNames@ using @FM Setting fPos then
            If TableHandles@<fPos> EQ Record else
                // There is a new handle for the indicated table. This could be the same table name from a different
                // volume or an updated handle for the same table. Either way, just append a new handle/table pair
                // to the lookup arrays.
                TableNames@         := TableName     : @FM
                TableAccounts@      := AccountName   : @FM
                TableHandles@       := Record        : @FM
                TableVolumes@       := Volume        : @FM
            end
        end else
            TableNames@         := TableName     : @FM
            TableAccounts@      := AccountName   : @FM
            TableHandles@       := Record        : @FM
            TableVolumes@       := Volume        : @FM
        end
    end
return


CREATE.FILE:
    GoSub Call_Next_FS
return


RENAME.FILE:
    GoSub Call_Next_FS
return


MOVE.FILE:
    GoSub Call_Next_FS
return


DELETE.FILE:
    GoSub Call_Next_FS
return


OPEN.MEDIA:
    GoSub Call_Next_FS
return

CREATE.MEDIA:
    GoSub Call_Next_FS
return


READ.MEDIA:
    GoSub Call_Next_FS
return


WRITE.MEDIA:
    GoSub Call_Next_FS
return


UNLOCK.ALL:
    Record  = ''
    Status  = ACTION_CONTINUE$
return


FLUSH:
    Record  = ''
    Status  = ACTION_CONTINUE$
return


INSTALL:
    Status  = ACTION_CONTINUE$
return


RESERVED:
    // There is a critical error if this has been reached.
    Status  = ACTION_STOP$
return


OMNI.SCRIPT:
    GoSub Call_Next_FS
return


CLOSE.MEDIA:
    GoSub Call_Next_FS
return


RECORD.COUNT:
    GoSub Call_Next_FS
return


REMAKE.FILE:
    GoSub Call_Next_FS
return


CREATE.INDEX:
    GoSub Call_Next_FS
return


DELETE.INDEX:
    GoSub Call_Next_FS
return


UPDATE.INDEX:
    GoSub Call_Next_FS
return


SELECT.INDEX:
    GoSub Call_Next_FS
return


READNEXT.INDEX:
    GoSub Call_Next_FS
return


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal GoSubs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Get_Original_Record:
    // To get the original record from the database table a direct call to this table's remaing chain must be made.
    @FILE.ERROR = ''
    NewFSList   = Delete(FSList, 1, 1, 1)
    NextFS      = NewFSList<1, 1, 1>
    Call @NextFS(READO.RECORD, NewFSList, Handle, Name, FMC, OrigRecord, ActionStatus)
    // If ActionStatus is Null then it is a new record or an error reading.
return


Call_Next_FS:
    // Since this MFS is being executed it is responsible for moving the chain forward. The next MFS/BFS item is in the
    // BFS array. Pull it from the top and pass the remaining items.
    NewFSList   = Delete(FSList, 1, 1, 1)
    NextFS      = NewFSList<1, 1, 1>
    If Len(NextFS) then
        Call @NextFS(Code, NewFSList, Handle, Name, FMC, Record, Status)
    end
return


Action_Chain:
    // This internal method provides the developer with a complete chain of actions. Prior to the BFS call the developer
    // can execute logic in a table specific action handler and then a promoted (i.e. generic) action handler. The
    // action will be suffixed with '_PRE' to identify the action logic before the BFS. After the BFS the table specific
    // action handler and promoted action handler will be called again. This is very analogous to the way event handling
    // in OpenInsight is managed (i.e. pre-system event handler, system event handler, post-system event handler.) 
    Action = BaseAction : '_PRE'

    GoSub Call_Table_Actions

    If ActionFlow EQ ACTION_CONTINUE$ OR ActionFlow EQ ACTION_CONTINUE_NO_SYSTEM$ then
        GoSub Call_Promoted_Actions
    end

    If ActionFlow EQ ACTION_CONTINUE$ OR ActionFlow EQ ACTION_CONTINUE_NO_PROMOTED$ OR ActionFlow EQ ACTION_SYSTEM_ONLY$ then
        GoSub Call_Next_FS
    end

    Action = BaseAction
    If ActionFlow EQ ACTION_CONTINUE$ OR ActionFlow EQ ACTION_CONTINUE_NO_PROMOTED$ OR ActionFlow EQ ACTION_CONTINUE_NO_SYSTEM$ then
        GoSub Call_Table_Actions
    end

    If ActionFlow EQ ACTION_CONTINUE$ OR ActionFlow EQ ACTION_CONTINUE_NO_SYSTEM$ then
        GoSub Call_Promoted_Actions
    end
return


Call_Table_Actions:
    // Pass activity to the datatable table's action handler if it exists.
    // Note: It is critical that handler routine be named in this format: TableName_ACTIONS

    // Check to see if this table has already been determine to have an action handler. Once it has already been
    // checked, whether or not a handler exists, it will not be checked again during this session. This will optimize
    // performance.
    InActionList    = False$    ; // Assume False for now.
    InNoActionList  = False$    ; // Assume False for now.

    InActionList    = SRP_List_Locate(ActionListHandle@, TableName) NE 0
    If Not(InActionList) then
        InNoActionList  = SRP_List_Locate(NoActionListHandle@, TableName)
    end

    If Not(InActionList) AND Not(InNoActionList) then
        // This table has not yet been added to either list, so a table action handler might exist.
        NumApps         = Count(@APPID, @FM) + (@APPID NE '')

        // Starting with the current application, search for an action routine and go through the list of inherited
        // applications until SYSPROG has been checked.
        For AppCnt = 1 to NumApps
            AppID   = @APPID<AppCnt>
            If AppID _EQC 'SYSPROG' then
                SysObjKey   = '$' : TableName : '_ACTIONS'
            end else
                SysObjKey   = '$' : TableName : '_ACTIONS' : '*' : @APPID<AppCnt>
            end
            If Len(SysObjHandle@) then
                OrigFileError   = @FILE.ERROR
                @FILE.ERROR     = ''
                BFS             = 'RTP57'
                Call @BFS(READO.RECORD, BFS, SysObjHandle@, SysObjKey, FMC, SysObjRecord, ActionStatus)
                @FILE.ERROR     = OrigFileError
                If ActionStatus then InActionList = True$
            end
        Until InActionList
        Next AppCnt

        If (InActionList) then
            SRP_List_Add(ActionListHandle@, TableName)
        end else
            SRP_List_Add(NoActionListHandle@, TableName)
        end
    end

    If InActionList then
        ActionRoutine   = TableName : '_ACTIONS'
        Transfer ActionFlow to OrigActionFlow ; // Save the current action flow.
        ActionFlow      = Function(@ActionRoutine(Action, '', FSList, Handle, Name, FMC, Record, Status, OrigRecord))

        // If the table action returned ACTION_CONTINUE, then this means no special action flow was returned.
        // Therefore, restore the action flow that existed before the table action call.
        If ActionFlow EQ ACTION_CONTINUE$ then Transfer OrigActionFlow to ActionFlow
    end
return


Call_Promoted_Actions:
    // Pass activity to the application's promoted action handler if it exists.
    // Note: It is critical that handler routine be named in this format: PROMOTED_BaseAction_ACTION

    // Check to see if this action has already been determine to have a promoted handler. Once it has already been
    // checked, whether or not a handler exists, it will not be checked again during this session. This will optimize
    // performance.
    InNoPromotedList    = False$    ; // Assume False for now.
    InPromotedList      = SRP_List_Locate(PromotedListHandle@, BaseAction) NE 0
    If Not(InPromotedList) then
        InNoPromotedList    = SRP_List_Locate(NoPromotedListHandle@, BaseAction)
    end

    If Not(InPromotedList) AND Not(InNoPromotedList) then
        // This action has not yet been added to either list, so a promoted action handler might exist.
        NumApps         = Count(@APPID, @FM) + (@APPID NE '')

        // Starting with the current application, search for an action routine and go through the list of inherited
        // applications until SYSPROG has been checked.
        For AppCnt = 1 to NumApps
            AppID   = @APPID<AppCnt>
            If AppID _EQC 'SYSPROG' then
                SysObjKey = '$PROMOTED_' : BaseAction : '_ACTION'
            end else
                SysObjKey = '$PROMOTED_' : BaseAction : '_ACTION' : '*' : @APPID<AppCnt>
            end
            If Len(SysObjHandle@) then
                OrigFileError   = @FILE.ERROR
                @FILE.ERROR     = ''
                BFS             = 'RTP57'
                Call @BFS(READO.RECORD, BFS, SysObjHandle@, SysObjKey, FMC, SysObjRecord, ActionStatus)
                @FILE.ERROR     = OrigFileError
                If ActionStatus then InPromotedList = True$
            end
        Until InPromotedList
        Next AppCnt

        If (InPromotedList) then
            SRP_List_Add(PromotedListHandle@, BaseAction)
        end else
            SRP_List_Add(NoPromotedListHandle@, BaseAction)
        end
    end

    If InPromotedList then
        ActionRoutine   = 'PROMOTED_' : BaseAction : '_ACTION'
        Transfer ActionFlow to OrigActionFlow ; // Save the current action flow.
        ActionFlow      = Function(@ActionRoutine(Action, '', FSList, Handle, Name, FMC, Record, Status, OrigRecord))

        // If the promoted action returned ACTION_CONTINUE, then this means no special action flow was returned.
        // Therefore, restore the action flow that existed before the promoted action call.
        If ActionFlow EQ ACTION_CONTINUE$ then Transfer OrigActionFlow to ActionFlow
    end
return
