#!/usr/bin/env python
import os
import sys
import argparse
import globals
import toolconfig
import subprocess
import json
import re
from globals import UserParamsException
import epicsCommandParsers.macLibSimple as macLibSimple
import epicsCommandParsers.epicsEnvSetParser as epicsEnvSetParser
import epicsCommandParsers.epicsCdParser as epicsCdParser
import epicsCommandParsers.dbLoadRecordsParser as dbLoadRecordsParser
import epicsCommandParsers.includeParser as includeParser

class LintState:
    '''Various parameters for processing a st.cmd file'''
    def __init__(self, topDir, iocDir):
        self.env = {}
        for (key,value) in os.environ.iteritems():
            self.env[key] = value
        self.env['TOP'] = topDir
        self.topDir = topDir
        self.IOCDir = iocDir
        # The startup.cmd's typically start the the boot dir for the specific IOC.
        self.currentDir = iocDir
        self.errors = []



def appendError(lintState, message, sourcefilName, sourceLineNumber):
    '''Appends errors in a standard format'''
    lintState.errors.append({'msg' : message, 'srcFile' : sourcefilName, 'srcLine' : sourceLineNumber})

def makeSureIncludedFileIsInPackage(lintState, includedFile):
    '''Makes sure that the included file is in the software package'''
    
def processFile(lintState, fileFullPath, srcFileName=None, srcLineNum=0):
    '''Process the given file and add to the errors any potential cram integration issues.
    '''
    rawfileFullPath = fileFullPath
    if '$' in fileFullPath:
        fileFullPath = macLibSimple.substMacro(fileFullPath, lintState.env)
    if not os.path.exists(fileFullPath):
        appendError(lintState, "Included file {0} does not exist.".format(rawfileFullPath), srcFileName, srcLineNum)
        return
    lineNum = 1
    with open(fileFullPath, 'r') as stcmd:
        if globals.verbose:
            print "Current folder is ", lintState.currentDir
        rawlines = stcmd.readlines()
        for rawline in rawlines:
            line = rawline.strip()
            lineNum = lineNum + 1
            if not line:
                continue
            if line.startswith('#'):
                continue
            if globals.verbose:
                print str(lineNum)+"|"+line+"|"
            if line.startswith('<') or line.startswith('.'):
                rawincludedFile = includeParser.parseIncludeCommand(line)
                includedFile = macLibSimple.substMacro(rawincludedFile, lintState.env)
                if not includedFile.startswith('/') and not lintState.currentDir == "${TOP}":
                    includedFile = os.path.join(lintState.currentDir, includedFile)
                if globals.verbose:
                    print "Processing included file", includedFile
                if not lintState.topDir in os.path.abspath(includedFile) and not (includedFile.endswith('pre_st.cmd') or includedFile.endswith('post_st.cmd')):
                    appendError(lintState, "Including external file {0} which is not in the current folder".format(includedFile), fileFullPath, lineNum-1)
                processFile(lintState, includedFile, fileFullPath, lineNum)
            elif line.startswith('cd') or line.startswith('chdir'):
                rawNewFolder = epicsCdParser.parseEpicsCDCommand(line)
                expandedNewFolder = macLibSimple.substMacro(rawNewFolder, lintState.env)
                lintState.currentDir = os.path.abspath(os.path.join(lintState.currentDir, expandedNewFolder))
                if globals.verbose:
                    print "Changing folder to ", lintState.currentDir, " from command in line {0} of file {1}".format(lineNum, fileFullPath)
                if not lintState.topDir in lintState.currentDir and not (rawNewFolder.startswith('${IOC_DATA}') or rawNewFolder.startswith('$(IOC_DATA)')):
                    appendError(lintState, "Comment out the 'cd' to an external folder {0}".format(lintState.currentDir), fileFullPath, lineNum-1)
            elif line.startswith('dbLoadRecords'):
                rawRecordsFile = dbLoadRecordsParser.parseDBLoadRecordsCommand(line)
                recordsFile = macLibSimple.substMacro(rawRecordsFile, lintState.env)
                if not recordsFile.startswith('/') and not lintState.currentDir == "${TOP}":
                    recordsFile = os.path.join(lintState.currentDir, recordsFile)
                if '$' in recordsFile:
                    recordsFile = macLibSimple.substMacro(recordsFile, lintState.env)
                if globals.verbose:
                    print "Loading records from ", rawRecordsFile, " abs path ", os.path.abspath(recordsFile), " with top dir", lintState.topDir
                    
                if not lintState.topDir in os.path.abspath(recordsFile):
                    appendError(lintState, 'Add DB_INSTALLS += {0} to your Makefile and change this to dbLoadRecords("db/{1}")'.format(rawRecordsFile, os.path.basename(recordsFile)), fileFullPath, lineNum-1)
                else:
                    expandedOnceMorePath = macLibSimple.substMacro(recordsFile, lintState.env)
                    if "db/" in expandedOnceMorePath and not os.path.exists(expandedOnceMorePath):
                        appendError(lintState, 'Missing file {0} in {1}; perhaps you missed adding a DB_INSTALLS += {0} to your Makefile'.format(expandedOnceMorePath, lintState.currentDir), fileFullPath, lineNum-1)
            elif line.startswith('epicsEnvSet'):
                try:
                    (name, value) = epicsEnvSetParser.parseSingleEnvPathsLine(line)
                    value = macLibSimple.substMacro(value, lintState.env)
                    if globals.verbose:
                        print "Adding environment var ", name, "=", value
                    lintState.env[name] = value
                except Exception:
                    print "Exception parsing epicsEnvSet", '|' + line + '|'
    

def lintRelease(args):
    """For IOC packages, this parses the st.cmd's of all the IOCs in iocBoot and tries to determine the typical cram integration issues.
    1) We look at all dbLoadRecords statements and make sure that the file exists and is contained within the software package.
    2) We look at all cd statements and make sure that the folder we are changing to exists and is contained within the software package.
    3) We process all include statements that look like so "< iocBoot/${IOC}/areaDetector.cfg" and make sure make sure that the file exists and is contained within the software package.
    """
    packageInfo = toolconfig.determinePackageInfoFromArgs(args)
    if not packageInfo['type'] in ['SIOC', 'HIOC']:
        print "cram lint is applicable for HIOC's and SIOC's only."
        return None
    if not os.path.exists('iocBoot'):
        print "cram lint is applicable for HIOC's and SIOC's that have an iocBoot folder with some IOC's which have a st.cmd."
        return None
    topDir = os.path.realpath(os.path.curdir)
    lintStates = []
    for root, dirs, files in os.walk('iocBoot'):
        if os.path.exists(os.path.join(root, 'st.cmd')):
            print "Linting IOC", root
            lintState = LintState(topDir, os.path.join(topDir, root))
            processFile(lintState, os.path.join(root, 'st.cmd'), os.path.join(root, 'st.cmd'))
            lintStates.append((root, lintState))
    return lintStates


def lintReleaseAndPrint(args):
    '''Call lintRelease and pretty print the results'''
    lintStates = lintRelease(args)
    if not lintStates:
        return
    for (iocFolder, lintState) in lintStates:
        for error in lintState.errors:
            print "{0}:{1} {2}".format(error['srcFile'], error['srcLine'], error['msg'])

def execute(args):
    parser = argparse.ArgumentParser(prog="cram  lint",
                                     formatter_class=argparse.RawDescriptionHelpFormatter, 
                                     description='''cram lint performs some basic validations for IOCs to help with cram integration. 
We parse the st.cmd's for all IOCs in iocBoot and try to estimate potential cram integration issues.
Currently,
    1) We look at all dbLoadRecords statements and make sure that the file exists and is contained within the software package.
    2) We look at all cd statements and make sure that the folder we are changing to exists and is contained within the software package.
    3) We process all include statements that look like so < iocBoot/${IOC}/areaDetector.cfg and make sure make sure that the file exists and is contained within the software package.

Please let us know if you encounter false positives or false negatives with cram lint. 
If there is potentially an automated way to identify integration issues, we'll be happy to add it to this utility.   
'''
                                     )
    parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose logging')
    args = parser.parse_args(args)
    globals.verbose = args.verbose
    try:
        lintReleaseAndPrint(args)
    except UserParamsException as upe:
        if not globals.verbose and not globals.isunittests:
            print upe
            sys.exit(-1)
        else:
            raise

if __name__ == "__main__":
    execute(sys.argv[1:])
