/*--------------------------------------------------------------------------

  All you need to evaluate macros with SICS.
        
  The implmentation fo the macro stuff is complex and non intuitive.
  This is the price to pay for adding the extremly powerful and
  strong Tcl-interpreter to SICS. The problem is that Tcl does not
  know anything about connections and our error handling. We have
  to transport this around it. This is done via the unknown mechanism.
  The unknown-command  is called any time Tcl does not find a command
  in its command list. Our unknown calls Sics-objects than and 
  provides proper connection and Sics-information. As this unknown is
  going to hold the connection info, we need one interpreter per
  connection. In order to transport interrupt conditions (motor
  going bang) we need an interrupt code field in Connection. 	

  Mark Koennecke,    November 1996

  Mark Koennecke, April 1999
  where code added.

  Mark Koennecke, December 1999
  InternalFileEval added


  Copyright:

  Labor fuer Neutronenstreuung
  Paul Scherrer Institut
  CH-5423 Villigen-PSI


  The authors hereby grant permission to use, copy, modify, distribute,
  and license this software and its documentation for any purpose, provided
  that existing copyright notices are retained in all copies and that this
  notice is included verbatim in any distributions. No written agreement,
  license, or royalty fee is required for any of the authorized uses.
  Modifications to this software may be copyrighted by their authors
  and need not follow the licensing terms described here, provided that
  the new terms are clearly indicated on the first page of each file where
  they apply.

  IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
  FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
  ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
  DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.

  THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
  IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
  NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
  MODIFICATIONS.
--------------------------------------------------------------------------*/
#include "fortify.h"
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <tcl.h>
#include "SCinter.h"
#include "conman.h"
#include "macro.h"
#include "status.h"
#include "obdes.h"
#include "splitter.h"
#include "ifile.h"
#include "Dbg.h"
#include "servlog.h"

#define SICSERROR "005567SICS"

/* in SCinter.c, but only used here */

extern Tcl_Interp *InterpGetTcl(SicsInterp *pSics);

/*----------------------------------------------------------------------------
  SICS Macro uses the Tcl "unknown" mechanism  to access the Sics-objects.
  The Tcl algorithm is to call a command called unknown when it cannot
  match a command with the Tcl-built ins. Sics will now define the unknown
  to call the Sics object invocation mechanism. In order to do this
  properly the unknown needs to know about the connection invoking the
  command and the SicsInterpreter to use. A suitable datastructure will
  be defined below. And a function which be planted into Tcl to do the 
  trick.
-----------------------------------------------------------------------------*/
#define MAXSTACK 50


  struct __SicsUnknown {
                         SConnection *pCon[MAXSTACK];
                         char        *lastUnknown[MAXSTACK];
                         int iStack;
                         SicsInterp *pInter;
                        };
                        
  static struct __SicsUnknown *pUnbekannt = NULL;                      
/*---------------------------------------------------------------------------*/
  int MacroPush(SConnection *pCon)
  {
      assert(pUnbekannt);
      pUnbekannt->iStack++;
      if(pUnbekannt->iStack >= MAXSTACK)
      {
        SCWrite(pCon,"ERROR: Out of Stack in macro.c",eError);
        return 0;
      }
      pUnbekannt->pCon[pUnbekannt->iStack] = pCon;
      return 1;
  }
/*-------------------------------------------------------------------------*/  
  int MacroPop(void)
  {
    assert(pUnbekannt);
    pUnbekannt->iStack--;
    if(pUnbekannt->iStack < 1) 
    {
      pUnbekannt->iStack = 0;
    }
    return 1;
  }
/*---------------------------------------------------------------------------*/  
  static int SicsUnknownProc(ClientData pData, Tcl_Interp *pInter, 
                  int argc, char *argv[])
  {
     struct __SicsUnknown *pSics = NULL;
     char **myarg = NULL;
     int margc;
     SicsInterp *pSinter = NULL;
     SConnection *pCon = NULL;
     CommandList *pCommand = NULL;
     char *lastCommand = NULL, comBuffer[132];
     int iRet,i;
     int iMacro;
     
     /* get the datastructures */
     pSics = (struct __SicsUnknown *)pData;
     assert(pSics);
     pSinter = pSics->pInter;
     pCon =   pSics->pCon[pSics->iStack];
     lastCommand = pSics->lastUnknown[pSics->iStack];

     assert(pSinter);
     assert(pCon);

     
     /* shorten the argc, argv by one and invoke */
     margc = argc -1;
     myarg = &argv[1];

     /* find object */
     if(margc < 1)
     {
        Tcl_SetResult(pInter,"No command found",TCL_VOLATILE);
        return TCL_ERROR;
     }
     pCommand = FindCommand(pSinter,myarg[0]);
     if(!pCommand)
     {
        Tcl_AppendResult(pInter,"Object ",myarg[0]," not found",NULL);
        return TCL_ERROR;
     }

     /* check for endless loop */
     Arg2Text(margc, myarg, comBuffer,131);
     if(lastCommand != NULL)
     {
        if(strcmp(lastCommand,comBuffer) == 0)
	{
            Tcl_AppendResult(pInter,"ERROR: Never ending loop  in unknown\n",
                             "Offending command: ",comBuffer,
                             "Probably Tcl command not found",NULL);
            SCSetInterrupt(pCon,eAbortBatch);
            return TCL_ERROR;    
        }
     }
     pSics->lastUnknown[pSics->iStack] = strdup(comBuffer);     

     /* invoke */
     iMacro = SCinMacro(pCon);
     SCsetMacro(pCon,1);
     iRet = pCommand->OFunc(pCon,pSinter,pCommand->pData,margc, myarg);
     SCsetMacro(pCon,iMacro);
     
     free(pSics->lastUnknown[pSics->iStack]);      
     pSics->lastUnknown[pSics->iStack] = NULL;     
     

     /* finish */
     if(iRet)
     {
       return TCL_OK;
     }
     else
     {
       Tcl_SetVar(pInter,SICSERROR,"yes",TCL_GLOBAL_ONLY);
       return TCL_ERROR;
     }
  }
  /*-----------------------------------------------------------------------*/
    static void UnknownKill(ClientData pData)
    {
      struct __SicsUnknown *pU = NULL;
      
      pU = (struct __SicsUnknown *)pData;
      if(pU->pCon[0])
      {
        SCDeleteConnection(pU->pCon[0]);
      }
      free(pData);
      pUnbekannt = NULL;
    }
/*--------------------------------------------------------------------------
  initialises a Tcl-Interpreter, installs SICS unknown mechanism and kills
  a few dangerous commands from the normal Tcl command set 
*/

  Tcl_Interp *MacroInit(SicsInterp *pSics)
  {
      Tcl_Interp *pInter = NULL;
      struct __SicsUnknown *pUnknown = NULL;
      char *pPtr = NULL;
      char *pPtr2 = NULL;
      FILE *fp = NULL;
      char pBueffel[512];
      int iRet;
      
      assert(pSics);
      
      /* create interpreter and unknown */
      pInter = Tcl_CreateInterp();
      pUnknown = (struct __SicsUnknown *)malloc(sizeof(struct __SicsUnknown));
      if( (!pInter) || (!pUnknown) )
      {
         return NULL;
      }
      
      /* install unknown command */
      memset(pUnknown,0,sizeof(struct __SicsUnknown));
      pUnknown->pCon[0] = SCCreateDummyConnection(pSics);
      pUnknown->iStack = 0;
      pUnknown->pInter = pSics;
      pUnbekannt = pUnknown;
      Tcl_CreateCommand(pInter,"unknown",SicsUnknownProc,
                        pUnknown, UnknownKill);
      
      /* delete dangers */
      Tcl_DeleteCommand(pInter,"exit");
      Tcl_DeleteCommand(pInter,"socket");
      Tcl_DeleteCommand(pInter,"vwait");
      Tcl_DeleteCommand(pInter,"exec"); 

      return pInter;
  }
/*--------------------------------------------------------------------------*/
    void MacroDelete(Tcl_Interp *pInter)
    {
    	Tcl_DeleteInterp(pInter);
    }
/*-------------------------------------------------------------------------
   Find the first word of a command string
*/
    static void FirstWord(char *pSource, char *pTarget)
    {
        int i, iLength, iStart, iPos;
     
        iLength = strlen(pSource);

        /* find start */
        for(i = 0, iStart = 0; i < iLength; i++)
	{
           if(pSource[i] != ' ')
	   {
             break;
           }
           else
	   {
              iStart++;
           }
        }

        /* do a quick check */
        if(iStart >= iLength - 2)
	{
           pTarget[0] = '\0';
           return;
        }

        /* do the copy */
        for(i = iStart, iPos = 0; i < iLength; i++)
	{
           if(pSource[i] != ' ')
	   {
             pTarget[iPos] = pSource[i];
             iPos++;
           }
           else
	   {
             pTarget[iPos] = '\0';
             return;
           }
        }
        pTarget[iPos] = '\0';
        return;
    }
/*-------------------------------------------------------------------------*/
    static char *pWhere = NULL;    
    static char *pFile = NULL;       

    int MacroWhere(SConnection *pCon, SicsInterp *pInter, void *pData,
                      int argc, char *argv[])
    {
        assert(pCon);

        if(pWhere)
	{
           SCWrite(pCon,pWhere,eValue);
        }
        return 1;
    }
/*-------------------------------------------------------------------------*/
    void WhereKill(void *pData)
    {

        if(pWhere)
	{
          free(pWhere);
          pWhere = NULL;
        }
        if(pFile)
	{
           free(pFile);
           pFile = NULL;
        }

        if(pData)
          KillDummy(pData);

    }
/*--------------------------------------------------------------------------*/
    int MacroFileEval(SConnection *pCon, SicsInterp *pInter, void *pData,
                      int argc, char *argv[])
    {
       FILE *fp = NULL;
       char pBueffel[512];
       int iChar;
       int i, iRun;
       Tcl_DString command;
       char *pCom = NULL;
       int iRet;
       Status eOld;
       Tcl_Interp *pTcl = NULL;
       int iLine = 0;    

       assert(pCon);
       assert(pInter);
       pTcl = InterpGetTcl(pInter);

       /* check authorisation: only users permitted here */
       if(!SCMatchRights(pCon,usUser))
       {
         SCWrite(pCon,"ERROR: Insufficient Privilege to do FileEval",eError);
         return 0;
       }

       /* open filename */
       if( argc < 2)
       {
       	 SCWrite(pCon,"ERROR: No filename specified ",eError);
       	 return 0;	
       }
       fp = fopen(argv[1],"r");
       if(!fp)
       {
         sprintf(pBueffel," Failed to open file -> %s <- ",argv[1]);
         SCWrite(pCon,pBueffel,eError);
         return 0;
       }
       
       /* handle status first */
       eOld = GetStatus();
       SetStatus(eBatch);
       
       SICSLogWrite("Evaluating in MacroFileEval",eValue);
       SICSLogWrite(argv[1],eValue);
       if(pFile)
       {
          free(pFile);
       }       
       pFile = strdup(argv[1]);

       /* cycle through file and execute complete commands */
       i = 0;
       Tcl_DStringInit(&command);
       iRun = 1;
       while(iRun)
       {
         iChar = fgetc(fp);
         if(iChar == EOF)
	 {
            iChar = (int)'\n';
            iRun = 0;
         }
         if( iChar == (int)'\n' )
         {
           pBueffel[i] = (char)iChar;
           pBueffel[i+1] = '\0';
           Tcl_DStringAppend(&command,pBueffel,-1);
           pCom = Tcl_DStringValue(&command);
           if(Tcl_CommandComplete(pCom))
           {
             SetStatus(eEager);
             FirstWord(pCom,pBueffel);
             if(FindCommand(pInter,pBueffel) != NULL)
	     {
               sprintf(pBueffel,"%s:%d>> %s",pFile,iLine,pCom);
               SCWrite(pCon,pBueffel,eValue);
               if(pWhere != NULL)
	       {
                 free(pWhere);
               }
               pWhere = strdup(pBueffel);
               iLine++;
             }
             iRet = Tcl_Eval(pTcl,pCom);
             SetStatus(eBatch);
             if(iRet != TCL_OK)
             {
               /* write TCL error and check for total interrupt */
               if(Tcl_GetVar(pTcl,SICSERROR,TCL_GLOBAL_ONLY) == NULL)
               {  /* Tcl error */
                 if(strlen(pTcl->result) > 2)
                 {
                    SCWrite(pCon,pTcl->result,eError);
                 }   
                 pCom = Tcl_DStringValue(&command);
                 sprintf(pBueffel,"ERROR: in Tcl line: %s",pCom);
                 SCWrite(pCon,pBueffel,eError);
               }
               else /* SICS error */
               {
                 Tcl_UnsetVar(pTcl,SICSERROR,TCL_GLOBAL_ONLY);
               /*  SCWrite(pCon,pTcl->result,eError); */
               }
             }
             if(SCGetInterrupt(pCon) >= eAbortBatch)
             {
                fclose(fp);
                Tcl_DStringFree(&command);
                SCWrite(pCon,"ERROR: batch processing interrupted",eError);
		SetStatus(eOld);
                return 0;
             }
             else
             {
                SCSetInterrupt(pCon,eContinue);
                SCSetError(pCon,OKOK);
             }
             Tcl_DStringFree(&command);
           }
           i = 0;
         }
         else
         {
           pBueffel[i] = (char)iChar;
           i++;
         } 
       } /* end while */
       
       
       /* clean up */
       fclose(fp);
       Tcl_DStringFree(&command);
       SetStatus(eOld);
       SCSendOK(pCon);
       return 1; 
    }   
/*----------------------------------------------------------------------
  InternalFileEval evaluates a file but on a dummy connection which
  only writes log files. It also configures a log file for output 
  which will be the name of the input file but with .log appended.
  This is here in order to support the evaluation of command files
  generated in a specific directory from the WWW-interface. If the latter
  does not get through, this can safely be deleted.
*/ 

    int InternalFileEval(SConnection *pCon, SicsInterp *pInter, void *pData,
                      int argc, char *argv[])
    {
       SConnection *pIntern = NULL;
       int iRet;
       char *pFil = NULL, *pExt = NULL;


       /* we need a filename */
       if( argc < 2)
       {
       	 SCWrite(pCon,"ERROR: No filename specified ",eError);
       	 return 0;	
       }

       /* allocate a dummy connection for this */
       pIntern = SCCreateDummyConnection(pInter);
       if(!pInter)
       {
          SCWrite(pCon,"ERROR: out of memory in InternalFileEval",eError);
          return NULL;
       }
       SCnoSock(pIntern);
       
       /* configure the log file */
       pFil = strdup(argv[1]);
       pExt = strrchr(pFil,(int)'.');
       if(!pExt)
       {
          SCWrite(pCon,"ERROR: no extension found in InternalFileEval",
                  eError);
          return 0;
       }
       else
       {
          strcpy(pExt,".log");
          SCAddLogFile(pIntern,pFil);     
          free(pFil);
       }
       
       /* invoke the fileeval */
       MacroPush(pIntern);
       iRet = MacroFileEval(pIntern,pInter,pData,argc,argv);
       MacroPop(); 

       /* remove our internal connection */ 
       SCDeleteConnection(pIntern);
       return iRet;
    }
/*--------------------------------------------------------------------------
   ClientPut is installed as a command to write data to the client from
   a server script. Syntax:
         ClientPut text outputcode
   The output code is optional and and defaults to eStatus.
---------------------------------------------------------------------------*/       

#include "outcode.c"

    int ClientPut(SConnection *pCon, SicsInterp *pInter, void *pData,
                      int argc, char *argv[])
    {
       OutCode eOut = eStatus;
       int i = 0, iCode, iLen;
       int iMacro;
       char *ppCode;
       char *pMessage = NULL;
       
       assert(pCon);
       assert(pInter);
       
       if(argc < 2)
       {
          SCWrite(pCon,"Insufficient arguments to ClientPut",eError);
          return 0;
       }
       
       /* handle optional I/O codes */
       if(argc > 2)
       {
          /* the last one must be the code */
          iCode = argc - 1;
          ppCode = strdup(argv[iCode]);
          strtolower(ppCode);
          while(pCode[i] != NULL)
          {
            if(strcmp(pCode[i],ppCode)== NULL)
            {
              break;
            }
            i++;
          }
          if(ppCode)
          {
             free(ppCode);
          }   
       }
       else
       {
          i = 10;
          iCode = argc;
       }
       
       switch(i)
       {
         case 0:
               eOut = eInternal;
               break;
         case 1:
              eOut = eCommand;
              break;
         case 2:
              eOut = eHWError;
              break;
         case 3:
              eOut = eInError;
              break;
         case 4:
              eOut = eStatus;
              break;
         case 5:
              eOut = eValue;
              break;
         case 6:
              eOut = eWarning;
              break;
         case 7:
              eOut = eError;
              break;
         default:
              eOut = eStatus;
              iCode = argc;
              break;      
       }

       /* recombine the message */
       /* find length */
       iLen = 0;
       for(i = 1; i < iCode; i++)
       {
         iLen += strlen(argv[i]);
       }
       pMessage = (char *)malloc((iLen+100)*sizeof(char));
       if(!pMessage)
       {
         SCWrite(pCon,"ERROR: out of memory in clientput",eError);
         return 0;
       }
       memset(pMessage,0,(iLen+100)*sizeof(char));
       Arg2Text(iCode-1,&argv[1],pMessage,(iLen+100)*sizeof(char));

       /* now write, thereby tunneling macro flag in order to get proper
          write to client and not into interpreter
       */
       iMacro = SCinMacro(pCon);
       SCsetMacro(pCon,0);
       SCWrite(pCon,pMessage,eOut);
       SCsetMacro(pCon,iMacro);
       if(pMessage)
       {
           free(pMessage);
       }
       return 1;
    }
/*---------------------------------------------------------------------------
 This implements a scheme to provide Tcl commands to Tcl. The Tcl commands
 (either procedures or objects) must be defined in a separate file. Than 
 you call:
   Publish command UserRights
 This will make the command known to Sics, and user with user rights matching
 the rights initialized may use it.
 
 Below the datastructure to hold for each command 
----------------------------------------------------------------------------*/  
 typedef struct {
                  pObjectDescriptor pDes;
                  char *command;
                  int iUser;
                } PubTcl, *pPubTcl;
/*--------------------------------------------------------------------------*/                
 static pPubTcl CreatePublish(char *name, int iUser)
 {
   pPubTcl pRes = NULL;
   
   pRes = (pPubTcl)malloc(sizeof(PubTcl));
   if(!pRes)
   {
     return NULL;
   }
   
   pRes->pDes = CreateDescriptor("Macro");
   if(!pRes->pDes)
   {
     free(pRes);
     return NULL;
   }
   
   pRes->command = strdup(name);
   pRes->iUser = iUser;
   return pRes;
 }
/*-------------------------------------------------------------------------*/
  static void DeletePublish(void *pData)
  {
    pPubTcl self = NULL;
    
    self = (pPubTcl)pData;
    assert(self);
    
    if(self->pDes)
    {
      DeleteDescriptor(self->pDes);
    }
    if(self->command)
    {
      free(self->command);
    }
    free(self);
  }   
/*--------------------------------------------------------------------------
 TclAction checks the user rights and than invokes the arguments as a Tcl
 command
 */
                    
 static int TclAction(SConnection *pCon,SicsInterp *pSics, void *pData,
               int argc, char *argv[])
 {
   char pBueffel[1024];
   pPubTcl self = NULL;
   int iRet;
   char *pPtr;
   Tcl_Interp *pTcl = NULL;
   
   self = (pPubTcl)pData;
   assert(pCon);
   assert(pSics);
   assert(self);
   pTcl = InterpGetTcl(pSics);
   
   if(!SCMatchRights(pCon,self->iUser))
   {
     sprintf(pBueffel,"ERROR: you are not authorised to invoke %s",
             argv[0]);
     SCWrite(pCon,pBueffel,eError);
     return 1;
   }

   /* make a string */
   Arg2Text(argc,argv,pBueffel,1023);
   
   
   iRet = Tcl_Eval(pTcl,pBueffel); 
   if(iRet == TCL_OK)
   {    /* possibly a bug here */
        SCWrite(pCon,pTcl->result,eStatus);
        return 1;
   }
   else
   {
        if(Tcl_GetVar(pTcl,SICSERROR,TCL_GLOBAL_ONLY) == NULL)
        {
            pPtr = strdup(pTcl->result);
            SCWrite(pCon,pPtr,eError);
            free(pPtr);
        }
        else
        {
          Tcl_UnsetVar(pTcl,SICSERROR,TCL_GLOBAL_ONLY);
          SCWrite(pCon,pTcl->result,eError);
        }
        return 0;    
   }        
   return 1;     /* not reached */ 
 }    
 
#include "access.c" 
/*--------------------------------------------------------------------------*/
 int TclPublish(SConnection *pCon, SicsInterp *pSics, void *pData, 
                int argc, char *argv[])
 {
    pPubTcl pNew = NULL;
    char pBueffel[132];
    int iUser, i, iRet;
    
    /* check no of args */
    if(argc < 3)
    {
      sprintf(pBueffel,"ERROR: Insufficient no of arguments to %s",
              argv[0]);
      SCWrite(pCon,pBueffel,eError);
      return 0;
    }
    
    /* check user rights */
    if(!SCMatchRights(pCon,usMugger))
    {
      sprintf(pBueffel,"ERROR: you are not authorised to use %s",
              argv[0]);
      SCWrite(pCon,pBueffel,eError);
      return 0;
    }
 
    /* try convert last parameter to user code */
    iUser = 0;
    strtolower(argv[2]);
    while(aCode[iUser] != NULL)
    {
      if(strcmp(aCode[iUser],argv[2]) == 0)
      {
        break;
      }
      iUser++;
    } 
    if(iUser > iCodes)
    {
      sprintf(pBueffel,"ERROR: cannot identify %s as a valid user code",
              argv[2]);
      SCWrite(pCon,pBueffel,eError);
      return 0;
    }
 
    /* do a job !*/
    pNew = CreatePublish(argv[1],iUser);
    if(!pNew)
    {
      sprintf(pBueffel,"ERROR: memory error in %s",argv[0]);
      SCWrite(pCon,pBueffel,eError);
      return 0;
    }
    iRet = AddCommand(pSics,argv[1],TclAction,DeletePublish,(void *)pNew);
    if(!iRet)
    {
        sprintf(pBueffel,"ERROR: duplicate command %s not created",argv[1]);
        SCWrite(pCon,pBueffel,eError);
        return 0;
    }
    return 1;
    
 }               
/*-------------------------------------------------------------------------
  Transact executes a command and sends a TRANSACTIONFINISHED string
  at the end. This is to permit clients to search for this string in 
  order to find out when a command has finished.
*/



 int TransactAction(SConnection *pCon, SicsInterp *pSics, void *pData, 
                int argc, char *argv[])
 {
    char pCommand[1024];
    int iRet;

    Arg2Text(argc-1,&argv[1],pCommand,1023);
    iRet = InterpExecute(pSics,pCon,pCommand);
    SicsWait(1);      
    SCWrite(pCon,"TRANSACTIONFINISHED",eError);
    return iRet;
 }
