#!/usr/bin/env python
#
# Copyright 2016 Pixar
#
# Licensed under the Apache License, Version 2.0 (the "Apache License")
# with the following modification; you may not use this file except in
# compliance with the Apache License and the following modification to it:
# Section 6. Trademarks. is deleted and replaced with:
#
# 6. Trademarks. This License does not grant permission to use the trade
#    names, trademarks, service marks, or product names of the Licensor
#    and its affiliates, except as required to comply with Section 4(c) of
#    the License and to reproduce the content of the NOTICE file.
#
# You may obtain a copy of the Apache License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the Apache License with the above modification is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the Apache License for the specific
# language governing permissions and limitations under the Apache License.
#

########################################################################
# Code generation script for GfVec, GfRange, GfQuat (and in future, GfMatrix)
# classes.
#
# Run this script manually to update the source code that's checked in.  Run
# with --validate to compare what would be generated with the existing code.  If
# it differs, this script will print a diff and error out.
#

import os, sys, itertools
from argparse import ArgumentParser

from jinja2 import Environment, FileSystemLoader
from jinja2.exceptions import TemplateError

# Write filePath if its content differs from \p content.  If unchanged, print a
# message indicating that.  If filePath is not writable, print a diff.
def _WriteFile(filePath, content, verbose=True):
    import difflib
    # If file currently exists and content is unchanged, do nothing.
    existingContent = '\n'
    content = (content + '\n'
               if content and not content.endswith('\n') else content)
    if os.path.exists(filePath):
        existingContent = open(filePath, 'r').read()
        if existingContent == content:
            if verbose:
                print '\tunchanged %s' % filePath
            return
    # Otherwise attempt to write to file.
    try:
        with open(filePath, 'w') as curfile:
            curfile.write(content)
            if verbose:
                print '\t    wrote %s' % filePath
    except IOError as ioe:
        print '\t', ioe
        print 'Diff:'
        print '\n'.join(difflib.unified_diff(existingContent.split('\n'),
                                             content.split('\n')))

def IsFloatingPoint(t):
    return t in ['double', 'float', 'half']

def RankScalar(s):
    # Return a numeric rank for a scalar type.  We allow implicit conversions to
    # scalars of greater rank.
    return dict([(t, n) for n, t in
                 enumerate(['int', 'half', 'float', 'double'])])[s]

def AllowImplicitConversion(src, dst):
    # Somewhat questionably, we always allow ints to implicitly convert to
    # floating point types.
    return RankScalar(src) <= RankScalar(dst)

def MakeListFn(defaultN):
    def List(fmt, sep=', ', num=None):
        num = num if num is not None else defaultN
        return sep.join([fmt % {'i': i} for i in range(num)])
    return List

def MakeMatrixFn(defaultN):
    def Matrix(fmt, sep=', ', indent=0, diagFmt=None, num=None):
        num = num if num is not None else defaultN
        diagFmt = diagFmt if diagFmt is not None else fmt

        def GetFmt(row, col):
            return diagFmt if row == col else fmt

        strs = [(sep.join([GetFmt(i,j) % {'i':i,'j':j} for j in range(num)])) \
                    for i in range(num)]
        indentStr = (' ' * indent)
        lineSep = sep + ('\n' + indentStr if '\n' not in sep else '')
        result = lineSep.join(strs)
        return result
    return Matrix

def GenerateFromTemplates(env, templates, suffix, outputPath, verbose=True):
    try:
        for tmpl in templates:
            template = env.get_template(tmpl % '.template')
            _WriteFile(os.path.join(outputPath, tmpl % suffix),
                       template.render(), verbose=verbose)
    except TemplateError as err:
        print '\t', err,
        print 'Aborting ...'

def VecName(dim, scl):
    return 'GfVec%s%s' % (dim, scl[0])

def Eps(scl):
    return '0.001' if scl == 'half' else 'GF_MIN_VECTOR_LENGTH'

########################################################################
# GfVec
def GetVecSpecs():
    scalarTypes = ['double', 'float', 'half', 'int']
    dimensions = [2, 3, 4]
    vecSpecs = sorted(
        [dict(SCL=scl,
              DIM=dim,
              SUFFIX=str(dim) + scl[0],
              VEC=VecName(dim, scl),
              EPS=Eps(scl),
              LIST=MakeListFn(dim),
              VECNAME=VecName,
              SCALARS=scalarTypes)
         for scl, dim in itertools.product(scalarTypes, dimensions)],
        key=lambda d: RankScalar(d['SCL']))

    return dict(templates=['vec%s.h', 'vec%s.cpp', 'wrapVec%s.cpp'],
                specs=vecSpecs)

########################################################################
# GfRange
def GetRangeSpecs():
    def RngName(dim, scl):
        return 'GfRange%s%s' % (dim, scl[0])

    def MinMaxType(dim, scl):
        return scl if dim == 1 else VecName(dim, scl)

    def MinMaxParm(dim, scl):
        t = MinMaxType(dim, scl)
        return t + ' ' if dim == 1 else 'const %s &' % t

    scalarTypes = ['double', 'float']
    dimensions = [1, 2, 3]
    rngSpecs = sorted(
        [dict(SCL=scl,
              MINMAX=MinMaxType(dim, scl),
              MINMAXPARM=MinMaxParm(dim, scl),
              DIM=dim,
              SUFFIX=str(dim) + scl[0],
              RNG=RngName(dim, scl),
              RNGNAME=RngName,
              SCALARS=scalarTypes,
              LIST=MakeListFn(dim))
         for scl, dim in itertools.product(scalarTypes, dimensions)],
        key=lambda d: RankScalar(d['SCL']))

    return dict(templates=['range%s.h', 'range%s.cpp', 'wrapRange%s.cpp'],
                specs=rngSpecs)
    
########################################################################
# GfQuat
def GetQuatSpecs():
    def QuatName(scl):
        return 'GfQuat%s' % scl[0]

    scalarTypes = ['double', 'float', 'half']
    quatSpecs = sorted(
        [dict(SCL=scl,
              SUFFIX=scl[0],
              QUAT=QuatName(scl),
              QUATNAME=QuatName,
              SCALARS=scalarTypes,
              LIST=MakeListFn(4))
         for scl in scalarTypes],
        key=lambda d: RankScalar(d['SCL']))

    return dict(templates=['quat%s.h', 'quat%s.cpp', 'wrapQuat%s.cpp'],
                specs=quatSpecs)

########################################################################
# GfMatrix
def GetMatrixSpecs(dim):
    def MatrixName(dim, scl):
        return 'GfMatrix%s%s' % (dim, scl[0])

    scalarTypes = ['double', 'float']
    dimensions = [dim]

    matrixSpecs = sorted(
        [dict(SCL=scl,
              DIM=dim,
              FILESUFFIX=scl[0],
              SUFFIX=str(dim) + scl[0],
              MAT=MatrixName(dim, scl),
              LIST=MakeListFn(dim),
              MATRIX=MakeMatrixFn(dim),
              MATNAME=MatrixName,
              SCALARS=scalarTypes)
         for scl, dim in itertools.product(scalarTypes, dimensions)],
        key=lambda d: RankScalar(d['SCL']))

    return dict(templates=['matrix%s%%s.h' % dim,
                           'matrix%s%%s.cpp' % dim,
                           'wrapMatrix%s%%s.cpp' % dim ], specs=matrixSpecs)

def GetMatrix2Specs():
    return GetMatrixSpecs(dim = 2)

def GetMatrix3Specs():
    return GetMatrixSpecs(dim = 3)

def GetMatrix4Specs():
    return GetMatrixSpecs(dim = 4)

# Check that each file in dstDir matches the corresponding file in srcDir.
def ValidateFiles(srcDir, dstDir):
    import difflib
    missing = []
    diffs = []
    for dstFile in [os.path.join(dstDir, f) for f in os.listdir(dstDir)
                    if os.path.isfile(os.path.join(dstDir, f))]:
        srcFile = os.path.join(srcDir, os.path.basename(dstFile))
        if not os.path.isfile(srcFile):
            missing.append(srcFile)
            continue
        dstContent, srcContent = open(dstFile).read(), open(srcFile).read()
        if dstContent != srcContent:
            diff = '\n'.join(difflib.unified_diff(
                srcContent.split('\n'),
                dstContent.split('\n'),
                'Source ' + os.path.basename(srcFile),
                'Generated ' + os.path.basename(dstFile)))
            diffs.append(diff)
            continue

    if missing or diffs:
        msg = []
        if missing:
            msg.append('*** Missing Generated Files: ' + ', '.join(missing))
        if diffs:
            msg.append('*** Differing Generated Files:\n' + '\n'.join(diffs))
        raise RuntimeError('\n' + '\n'.join(msg))

if __name__ == '__main__':
    ap = ArgumentParser(
        description='Generate source code for GfVec, GfRange, GfQuat.')
    ap.add_argument('--validate', action='store_true')
    ap.add_argument('--dstDir', default=os.curdir)
    ap.add_argument('--srcDir', default=os.curdir)
    args = ap.parse_args()

    stdEnv = dict(UPPER=str.upper, LOWER=str.lower,
                  ALLOW_IMPLICIT_CONVERSION=AllowImplicitConversion,
                  IS_FLOATING_POINT=IsFloatingPoint)

    if args.validate:
        # Make a temporary directory for results.
        import tempfile
        args.dstDir = tempfile.mkdtemp()

    try:
        for s in [GetVecSpecs(), GetRangeSpecs(), GetQuatSpecs(),
                  GetMatrix2Specs(), GetMatrix3Specs(), GetMatrix4Specs()]:
            env = Environment(loader=FileSystemLoader(args.srcDir),
                              trim_blocks=True)
            env.globals.update(**stdEnv)
            templates, specs = s['templates'], s['specs']
            for spec in specs:
                env.globals.update(**spec)
                GenerateFromTemplates(
                    env, templates, spec.get('FILESUFFIX', spec['SUFFIX']),
                    args.dstDir, verbose=not args.validate)

        if args.validate:
            ValidateFiles(args.srcDir, args.dstDir)

    finally:
        # Remove the temporary directory.
        if args.validate:
            import shutil
            shutil.rmtree(args.dstDir)
