#!/usr/bin/env python3

################################################################################
##                                                                            ##
##  This file is part of NCrystal (see https://mctools.github.io/ncrystal/)   ##
##                                                                            ##
##  Copyright 2015-2020 NCrystal developers                                   ##
##                                                                            ##
##  Licensed under the Apache License, Version 2.0 (the "License");           ##
##  you may not use this file except in compliance with the License.          ##
##  You may obtain a copy of the License at                                   ##
##                                                                            ##
##      http://www.apache.org/licenses/LICENSE-2.0                            ##
##                                                                            ##
##  Unless required by applicable law or agreed to in writing, software       ##
##  distributed under the License is distributed on an "AS IS" BASIS,         ##
##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  ##
##  See the License for the specific language governing permissions and       ##
##  limitations under the License.                                            ##
##                                                                            ##
################################################################################

"""

Script which can be used to embed the content of .ncmat files directly into a
C++ library. It does so by reading the .ncmat files and creating C++ code which
keeps the contents of the files in static strings, and registers those strings
with NCrystal, using the original filename as key. Naturally, those file must be
compiled along with the rest of the C++ library, and the containing function
must be invoked.

"""

import sys
if not (sys.version_info >= (3, 0)):
    raise SystemExit('ERROR: This script requires Python3.')
if not (sys.version_info >= (3, 6)):
    print('WARNING: This script was only tested with Python3.6 and later.')
import argparse
import pathlib

def tryImportNCrystal():
    try:
        import NCrystal as _nc
    except ImportError:
        NCrystal = None
        #Perhaps PYTHONPATH was not setup correctly. If we are running out of a
        #standard NCrystal installation, we can try to amend the search path
        #manually for the current script:
        op=os.path
        test=op.abspath(op.realpath(op.join(op.dirname(__file__),'../python')))
        if op.exists(op.join(test,'NCrystal/__init__.py')):
            sys.path += [test]
        try:
            import NCrystal as _nc
        except ImportError:
            _nc = None
    return _nc

def parseArgs():
    descr="""

Script which can be used to embed the content of .ncmat files directly into a
C++ library. It does so by reading the .ncmat files and creating C++ code which
keeps the contents of the files in static strings, and registers those strings
with NCrystal, using the original filename as key. Naturally, those file must be
compiled along with the rest of the C++ library, and the containing function
must be invoked.

"""
    parser = argparse.ArgumentParser(description=descr)
    parser.add_argument('FILE', type=str, nargs='+',
                        help="""One or more NCMAT files. They will be registered with a key equal to their
                        filename (without preceding directory name), which must
                        therefore be unique in the list""")
    parser.add_argument('--full','-f', action='store_true',
                        help="""Unless this option is provided, all comments and excess whitespace will be
                        stripped from the file data.""")
    parser.add_argument('--validate','-v', action='store_true',
                        help="""If specified input files will be validated by confirming that they can be
                        loaded with NCrystal. For this to work, the NCrystal python module must be
                        available.""")
    parser.add_argument("--name",'-n',default='registerNCMATData',type=str,
                        help="""Name of C++ function to create which must be called in order to register the
                        data with NCrystal. If desired, it can contain namespace(s), e.g. a value of
                        "MyNameSpace::myFunction" will create a function "void myFunction()" in the
                        namespace "MyNameSpace.""")
    parser.add_argument("--regfctname",default='NCrystal::registerInMemoryStaticFileData(const std::string&,const char*)',type=str,
                        help="""Name of C++ function used to register string objects with NCrystal.""")
    parser.add_argument("--width",'-w',type=int,default=80,
                        help="""Wrap C++ code at this column width. Ignored when running with --full.""")
    parser.add_argument('--outfile','-o',type=argparse.FileType('w'),default=sys.stdout,
                        help="Name of output file (default: stdout)")

    args=parser.parse_args()
    if not args.name or ' ' in args.name:
        parser.error('Invalid C++ function name provided to --name')
    filepaths = set()
    bns=set()
    for f in set(args.FILE):
        p=pathlib.Path(f)
        if not p.exists():
            parser.error('File not found: %s'%f)
        p=p.resolve().absolute()
        if p in filepaths:
            parser.error('The same file is specified more than once: %s'%p)
        if p.name in bns:
            parser.error('Filenames without directory part is not unique: %s'%f)
        filepaths.add(p)
        bns.add(p.name)
    args.files = list(sorted(filepaths))
    args.FILE=None

    wmin=30
    wmax=999999
    if args.width>wmax:
        args.width=wmax
    if args.width < wmin:
        parser.error('Out of range value of --width (must be at least %i)'%wmin)

    return args

def files2cppcode(infiles,outfile,
                  cppfunctionname='registerData',
                  compact=True,
                  compactwidth=140,
                  validatefct=None,
                  regfctname='NCrystal::registerInMemoryStaticFileData(const std::string&,const char*)' ):
    out=['// Code automatically generated by ncrystal_ncmat2cpp','']

    def fwddeclare(out,fctname,args_str=''):
        if not '(' in fctname:
            fctname+='()'
        _ = fctname.split('(',1)[0].split('::')
        namespaces,justname = _[0:-1],_[-1]
        argssignature='('+fctname.split('(',1)[1]
        tmp=''
        for ns in namespaces:
            tmp += 'namespace %s { '%ns
        tmp += 'void %s%s;'%(justname,argssignature)
        tmp += ' }'*len(namespaces)
        out+=[tmp]
        out+=['']

    fwddeclare(out,regfctname)
    if '::' in cppfunctionname:
        fwddeclare(out,cppfunctionname)

    out+=['void %s()'%cppfunctionname,'{']
    prefix='  '
    seen = set()

    for p in [pathlib.Path(f) for f in infiles]:
        fn=p.name
        assert not fn in seen, "ERROR: Multiple files in input named: %s"%fn
        seen.add(fn)
        if validatefct:
            print("Trying to validate: %s"%fn)
            validatefct(p)
            print('  -> OK')
        out+= [prefix+"{"]
        out+= [prefix+"  // File %s%s"%(fn,' (compact form without comments)' if compact else '')]
        lines = list(p.read_text().splitlines())
        assert lines,"file was empty: %s"%fn
        def fmtline(line):
            if compact:
                ncmatcfg=None
                if 'NCRYSTALMATCFG[' in line:
                    #special case: preserve NCRYSTALMATCFG
                    _=line.split('NCRYSTALMATCFG[',1)[1]
                    assert not 'NCRYSTALMATCFG' in _, "multiple NCRYSTALMATCFG entries in a single line"
                    assert ']' in _, "NCRYSTALMATCFG[ entry without closing ] bracket"
                    ncmatcfg=_.split(']',1)[0].strip()
                    ncmatcfg.encode('utf8')#Just a check
                line=' '.join(line.split('#',1)[0].split())
                line.encode('ascii')#Just a check
                if ncmatcfg:
                    line += '#NCRYSTALMATCFG[%s]'%ncmatcfg
                if not line:
                    return ''
            else:
                line.encode('utf8')#Just a check
            return line.replace('"',r'\"')+r'\n'

        def as_c_str(strdata):
            try:
                strdata.encode('ascii')
                return '"%s"'%strdata
            except UnicodeEncodeError:
                pass
            try:
                strdata.encode('utf8')
                return 'u8"%s"'%strdata
            except UnicodeEncodeError:
                raise SystemExit('Invalid encoding encountered in input (must be ASCII or UTF8)')
        firstlinepattern = lambda strdata : '  const char * textdata = %s'%as_c_str(strdata)
        otherlinepattern = lambda strdata : '    %s'%as_c_str(strdata)
        if not compact:
            out+= [prefix+firstlinepattern(fmtline(lines[0]))]
            for line in lines[1:]:
                out+= [prefix+otherlinepattern(fmtline(line))]
        else:
            alldata=''
            for line in lines:
                alldata += fmtline(line)
            first=True
            while alldata:
                strpatfct=firstlinepattern if first else otherlinepattern
                first = False
                n = compactwidth-(len(strpatfct('')))
                if alldata[n-1:n+1]==r'\n':
                    n+=1#dont break up '\n' entries
                out += [ prefix+strpatfct(alldata[0:n])]
                alldata = alldata[n:]
        out[-1]+=';'
        out+= [prefix+"  ::%s(\"%s\",textdata);"%(regfctname.split('(')[0],fn)]
        out+= [prefix+"}"]
    out+= ['}','']

    out = '\n'.join(out)
    if hasattr(outfile,'write'):
        outfile.write(out)
        if hasattr(outfile,'name'):
            print('Wrote: %s'%outfile.name)
    else:
        with pathlib.Path(outfile).open('wt') as fh:
            fh.write(out)
        print('Wrote: %s'%outfile)

def main():
    args=parseArgs()

    validatefct=None
    if args.validate:
        nc=tryImportNCrystal()
        if not nc:
            raise SystemExit("ERROR: Could not import the NCrystal Python module (this is required"
                             " when running with --validate). If it is installed, make sure your"
                             " PYTHONPATH is setup correctly.")
        validatefct = lambda filename : nc.createInfo('%s;dcutoff=-1;inelas=sterile'%filename)

    files2cppcode( args.files,
                   outfile = args.outfile,
                   cppfunctionname = args.name,
                   compact = not args.full,
                   compactwidth = args.width,
                   validatefct = validatefct,
                   regfctname = args.regfctname )

if __name__=='__main__':
    main()
