#!/usr/bin/env python
#
# Copyright 2012-2017, Intel Corporation, All Rights Reserved.
#
# This software is supplied under the terms of a license
# agreement or nondisclosure agreement with Intel Corp.
# and may not be copied or disclosed except in accordance
# with the terms of that agreement.
#
#  Author:  Christopher M. Cantalupo
#

"""
KNX_MICPERF_HELP
"""

import sys
import os
import cPickle
import getopt
import subprocess
import re
import datetime

import micp.common as micp_common
import micp.info as micp_info
import micp.run as micp_run
import micp.stats as micp_stats
import micp.kernel as micp_kernel
import micp.connect as micp_connect
import micp.params as micp_params
import micp.version as micp_version

from micp.common import mp_print, CAT_ERROR, CAT_INFO

HANDLED_EXCEPTIONS = (micp_kernel.NoExecutableError,
                micp_params.UnknownParamError,
                micp_params.InvalidParamTypeError,
                micp_common.WindowsMicInfoError,
                micp_common.NoExecutionPermission,
                micp_kernel.SelfCheckError,
                micp_stats.PerfRegressionError,
                micp_common.FactoryLookupError,
                micp_common.PermissionDeniedError,
                micp_common.MissingDependenciesError)

MAX_VERBOSITY = 3
VALID_CATEGORIES = ("optimal",
                    "scaling",
                    "test",
                    "scaling_quick",
                    "optimal_quick",
                    "scaling_core")

FOR_HELP_MESSAGE = 'For help run: {0} --help\n'.format(sys.argv[0])
BAD_ARCH_MESSAGE = 'Micperf can only be executed on Intel(R) Xeon Phi(TM) \
product family devices.'
LOGFILE_CREATED_MESSAGE = 'Kernels output has been saved to file {}'

if __name__ == '__main__':

    if os.path.exists("/sys/class/mic/"):
        # system is supported by coprocessor
        pass
    elif not micp_common.is_selfboot_platform():
        mp_print(BAD_ARCH_MESSAGE, CAT_ERROR)
        sys.exit(micp_common.E_EXEC)

    if len(sys.argv) > 1 and (sys.argv[1] == '-h' or sys.argv[1] == '--help'):
        print __doc__
        sys.exit(micp_common.E_NO_ERROR)

    if len(sys.argv) > 1 and (sys.argv[1] == '--version'):
        import micp.version as micp_version
        print micp_version.__version__
        sys.exit(micp_common.E_NO_ERROR)
    try:
        opts, args = getopt.gnu_getopt(sys.argv[1:], 'k:x:v:o:p:c:t:r:R:m:d:e:D', ['sudo'])
    except getopt.GetoptError as err:
        mp_print(str(err), CAT_ERROR)
        mp_print(FOR_HELP_MESSAGE, CAT_INFO)
        sys.exit(micp_common.E_PARSE)

    offMethod = ''
    kernelNames = ''
    paramCat = ''
    verbLevel = ''
    outDir = ''
    kernelArgs = ''
    tag = ''
    compareResult = ''
    compareTag = ''
    margin = ''
    device = ''
    kernelPlugin = ''
    use_ddr_on_knlsb = False   # by default use MCDRAM memory
    sudo = False

    argCounter = 1
    for flag, val in opts:
        if flag == '-k':
            kernelNames = val
        elif flag == '-x':
            offMethod = val
        elif flag == '-v':
            verbLevel = val
        elif flag == '-o':
            outDir = val
        elif flag == '-p':
            kernelArgs = val
        elif flag == '-c':
            paramCat = val
        elif flag == '-t':
            tag = val
        elif flag == '-r':
            compareResult = val
        elif flag == '-R':
            compareTag = val
        elif flag == '-m':
            margin = val
        elif flag == '-d':
            device = val
        elif flag == '-e':
            kernelPlugin = val
        elif flag == '-D':
            use_ddr_on_knlsb = True
        elif flag == '--sudo':
            sudo = True
        else :
            mp_print('Parsing command line, unknown flag {0}\n'.format(flag),
                CAT_ERROR)
            mp_print(FOR_HELP_MESSAGE, CAT_INFO)
            sys.exit(micp_common.E_PARSE)

        if not val or flag + val in sys.argv:
            argCounter += 1
        else:
            argCounter += 2

    # -D option is only valid on KNL Processors
    if use_ddr_on_knlsb and micp_version.MIC_PERF_HOST_ARCH != 'x86_64_AVX512':
        mp_print('Parsing command line, unknown flag -D.', CAT_ERROR)
        mp_print(FOR_HELP_MESSAGE, CAT_INFO)
        sys.exit(micp_common.E_PARSE)

    if argCounter != len(sys.argv):
        mp_print('Parsing command line, unused arguments.', CAT_ERROR)
        mp_print(FOR_HELP_MESSAGE, CAT_INFO)
        sys.exit(micp_common.E_PARSE)

    if margin and not (compareTag or compareResult):
        mp_print('-m option requires a pickle file (-r) or a tag (-R).',
            CAT_ERROR)
        mp_print(FOR_HELP_MESSAGE, CAT_INFO)
        sys.exit(micp_common.E_PARSE)

    if tag and not outDir:
        mp_print('-t option requires to specify an output directory -o.',
            CAT_ERROR)
        mp_print(FOR_HELP_MESSAGE, CAT_INFO)
        sys.exit(micp_common.E_PARSE)

    number_of_kernels = len(kernelNames.split(':'))
    if kernelArgs and number_of_kernels > 1:
        error = ('-p option can only modify the parameters for a'
                 ' single kernel, micprun received {0} kernels "{1}".')
        mp_print(error.format(number_of_kernels, kernelNames), CAT_ERROR)
        sys.exit(micp_common.E_PARSE)

    if kernelArgs == 'help':
        kernelArgs = '--help'

    try:
        is_selfboot = micp_common.is_selfboot_platform()
    except OSError, err:
        mp_print(str(err), CAT_ERROR)
        sys.exit(micp_common.E_EXCEPT)

    default_device = 'mic0'
    if is_selfboot:
        # For KNL Processors 'local' is used internally as offload method
        # however users are not allowed to change it using option -x
        if offMethod:
            error_msg = 'Parsing command line -x is not a valid offload method.'
            mp_print(error_msg, CAT_ERROR)
            sys.exit(micp_common.E_PARSE)

        offMethod = 'local'
        # default_device for KNL Processors
        default_device = '-1'
    else:
        # for KNX coprocessors the device should be an integer N
        # such that N identifies a card in the system e.g. N=0 for mic0
        if device in micp_common.LOCAL_HOST_ID:
            error_msg = '{0} is not a valid device.'.format(device)
            mp_print(error_msg, CAT_ERROR)
            sys.exit(micp_common.E_PARSE)

    if not device:
        device = default_device

    if tag:
        badChar = re.compile(r'[^\w.-]')
        if badChar.findall(tag):
            error_msg = 'Replaced non-alphanumeric characters in tag.'
            mp_print(error_msg, CAT_INFO)
            mp_print('IN:\t{0}\n'.format(tag))
            tag = badChar.sub('-', tag)
            mp_print('OUT:\t{0}'.format(tag))

    if outDir:
        try:
            testFile = os.path.join(outDir, 'deleteme')
            fid = open(testFile,'w')
            fid.close()
            os.remove(testFile)
        except IOError as err:
            error_msg = ('Unable to create a test file in output directory'
                        '-o option must be a writable directory.')
            mp_print(error_msg, CAT_ERROR)
            sys.exit(micp_common.E_IO)

    try:
        devIdx = micp_connect.MPSSConnect(device).get_offload_index()
        micp_info.Info(devIdx)
    except (RuntimeError, micp_common.MissingDependenciesError) as err:
        if 'No mic cores found' in err.__str__() or 'Could not find IP address' in err.__str__():
            mp_print(str(err), CAT_ERROR)
            mp_print('Make sure that the MPSS service has been started.')
            sys.exit(micp_common.E_MPSS_NA)
        elif type(err) is micp_common.MissingDependenciesError:
            mp_print(str(err), CAT_ERROR)
            sys.exit(err.micp_exit_code())
        raise
    except micp_connect.GetOffloadIndexError:
        error_msg = ('Unable to establish an SSH connection with the coprocessor. '
                    'Please make sure SSH access for the coprocessor has been '
                    'configured as described in the documentation.')
        mp_print(error_msg, CAT_ERROR)
        sys.exit(micp_common.E_EXCEPT)

    # set MCDRAM/DDR memory use policy according to the given command line arguments
    if micp_common.is_selfboot_platform():
        micp_info.Info().set_use_only_ddr_memory(use_ddr_on_knlsb)

    if compareTag:
        scs = micp_stats.StatsCollectionStore()
        if compareTag == 'help':
            allTags = scs.stored_tags()
            if allTags:
                micp_common.exit_application('\n'.join(allTags), 0)
            else:
                micp_common.exit_application(micp_common.NO_REFERENCE_TAGS_ERROR, 3)

        compareResult = scs.get_by_tag(compareTag)
        if compareResult is None:
            mp_print('Could not find reference tag {} in store.'.format(compareTag),
                CAT_ERROR)
            sys.exit(micp_common.E_IO)
        if compareTag != compareResult.tag:
            mp_print('Matching tag: {}'.format(compareResult.tag), CAT_INFO)

    elif compareResult:
        try:
            compareResult = cPickle.load(open(compareResult, 'rb'))
        except IOError as err:
            err_msg = 'Could not open input reference file for reading.'
            mp_print(str(err), CAT_ERROR)
            mp_print(err_msg)
            sys.exit(micp_common.E_IO)

    # validate if category is valid
    if paramCat and paramCat not in VALID_CATEGORIES:
        mp_print("Invalid category '{}'.".format(paramCat), CAT_ERROR)
        mp_print("Valid categories are:")
        mp_print('\t' + "\n\t".join(VALID_CATEGORIES), wrap=False)
        mp_print(FOR_HELP_MESSAGE, CAT_INFO)
        sys.exit(micp_common.E_PARSE)

    if not kernelArgs and not paramCat:
        if not compareResult:
            paramCat = 'optimal'
        else:
            paramCat = compareResult.runArgs['paramCat']
            kernelArgs = compareResult.runArgs['kernelArgs']

    if not kernelNames:
        if not compareResult:
            kernelNames = 'all'
        else:
            kernelNames = compareResult.runArgs['kernelNames']

    if not offMethod:
        if not compareResult:
            offMethod = 'native:scif'
        else:
            offMethod = compareResult.runArgs['offMethod']

    # if given validate verbosity level is correct, otherwise set it to default value
    if not verbLevel:
        if not compareResult:
            verbLevel = '0'
        else:
            verbLevel = '1'
    else:
        if not verbLevel.isdigit() or int(verbLevel) > MAX_VERBOSITY:
            err = 'Verbosity level (-v) should be an integer between 0 and {0}.'
            mp_print(err.format(MAX_VERBOSITY), CAT_ERROR)
            mp_print(FOR_HELP_MESSAGE, CAT_INFO)
            sys.exit(micp_common.E_PARSE)

    logFileName = None
    if all('help' not in arg for arg in kernelArgs):
        suffix = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        logFileName = "micp_run_log_" + suffix + ".log"

    try:
        exit_code = micp_run.run(kernelNames, offMethod, paramCat, kernelArgs,
                        device, verbLevel, outDir, tag, compareResult, margin,
                        kernelPlugin, {}, sudo, logFileName)

    except HANDLED_EXCEPTIONS as err:
        mp_print(str(err), CAT_ERROR)
        exit_code = err.micp_exit_code()
    except subprocess.CalledProcessError as err:
        mp_print(str(err), CAT_ERROR)
        exit_code = err.returncode

    # remove the logfile if no data was written
    if os.path.exists(logFileName):
        if os.stat(logFileName).st_size == 0:
            os.remove(logFileName)
        else:
            mp_print('\n')
            mp_print(LOGFILE_CREATED_MESSAGE.format(logFileName), CAT_INFO)

    sys.exit(exit_code)
