#Copyright ReportLab Europe Ltd. 2000-2021
#see license.txt for license details
import os, sys, glob, shutil, re, sysconfig, traceback, io, subprocess
from urllib.parse import quote as urlquote
from wheel.bdist_wheel import bdist_wheel
from setuptools import setup, Extension

platform = sys.platform
pjoin = os.path.join
abspath = os.path.abspath
normpath = os.path.normpath
isfile = os.path.isfile
isdir = os.path.isdir
dirname = os.path.dirname
basename = os.path.basename
splitext = os.path.splitext
addrSize = 64 if sys.maxsize > 2**32 else 32
sysconfig_platform = sysconfig.get_platform()
pkgDir = os.path.realpath(dirname(__file__))

def spCall(cmd,*args,**kwds):
    r = subprocess.call(
            cmd,
            stderr =subprocess.STDOUT,
            stdout = subprocess.DEVNULL if kwds.pop('dropOutput',False) else None,
            timeout = kwds.pop('timeout',3600),
            )
    if verbose>=3:
        print('%r --> %s' % (' '.join(cmd),r), pfx='!!!!!' if r else '#####', add=False)
    return r

def specialOption(n,ceq=False):
    v = 0
    while n in sys.argv:
        v += 1
        sys.argv.remove(n)
    if ceq:
        n += '='
        V = [_ for _ in sys.argv if _.startswith(n)]
        for _ in V: sys.argv.remove(_)
        if V:
            n = len(n)
            v = V[-1][n:]
    return v

usla = specialOption('--use-system-libart')
mdbg = specialOption('--memory-debug')
verbose = specialOption('--verbose',ceq=True)

def _packages_path(d):
    P = [_ for _ in sys.path if basename(_)==d]
    if P: return P[0]

package_path = _packages_path('dist-packages') or _packages_path('site-packages')
package_path = pjoin(package_path, 'reportlab')

def die(msg):
    raise ValueError(msg)

def make_libart_config(src):
    from struct import calcsize as sizeof
    L=["""/* Automatically generated by setup.py */
#ifndef _ART_CONFIG_H
#\tdefine _ART_CONFIG_H
#\tdefine ART_SIZEOF_CHAR %d
#\tdefine ART_SIZEOF_SHORT %d
#\tdefine ART_SIZEOF_INT %d
#\tdefine ART_SIZEOF_LONG %d""" % (sizeof('c'), sizeof('h'), sizeof('i'), sizeof('l'))
        ]
    aL = L.append

    if sizeof('c')==1:
        aL("typedef unsigned char art_u8;")
    else:
        die("sizeof(char) != 1")
    if sizeof('h')==2:
        aL("typedef unsigned short art_u16;")
    else:
        die("sizeof(short) != 2")

    if sizeof('i')==4:
        aL("typedef unsigned int art_u32;")
    elif sizeof('l')==4:
        aL("typedef unsigned long art_u32;")
    else:
        die("sizeof(int)!=4 and sizeof(long)!=4")
    aL('#endif\n')
    with open(pjoin(src,'art_config.h'),'w') as f:
        f.write('\n'.join(L))

#this code from /FBot's PIL setup.py
def aDir(P, d, x=None):
    if d and isdir(d) and d not in P:
        if x is None:
            P.append(d)
        else:
            P.insert(x, d)

def findFile(root, wanted, followlinks=True):
    visited = set()
    for p, D, F in os.walk(root,followlinks=followlinks):
        #scan directories to check for prior visits
        #use dev/inode to make unique key
        SD = [].append
        for d in D:
            dk = os.stat(pjoin(p,d))
            dk = dk.st_dev, dk.st_ino
            if dk not in visited:
                visited.add(dk)
                SD(d)
        D[:] = SD.__self__  #set the dirs to be scanned
        for fn in F:
            if fn==wanted:  
                return abspath(pjoin(p,fn))

def freetypeVersion(fn,default='20'):
    with open(fn,'r') as _:
        text = _.read()
    pat = re.compile(r'^#define\s+FREETYPE_(?P<level>MAJOR|MINOR|PATCH)\s*(?P<value>\d*)\s*$',re.M)
    locmap=dict(MAJOR=0,MINOR=1,PATCH=2)
    loc = ['','','']
    for m in pat.finditer(text):
        loc[locmap[m.group('level')]] = m.group('value')
    loc = list(filter(None,loc))
    return '.'.join(loc) if loc else default

class inc_lib_dirs:
    def __call__(self,libname=None):
        L = []
        I = []
        if platform == "cygwin":
            aDir(L, os.path.join("/usr/lib", "python%s" % sys.version[:3], "config"))
        elif platform == "darwin":
            machine = sysconfig_platform.split('-')[-1]
            if machine=='arm64' or os.environ.get('ARCHFLAGS','')=='-arch arm64':
                #print('!!!!! detected darwin arm64 build')
                #probably an M1
                target = pjoin(
                            ensureResourceStuff('m1stuff.tar.gz','m1stuff','tar',
                                baseDir=os.environ.get('RL_CACHE_DIR','/tmp/reportlab')),
                            'm1stuff','opt','homebrew'
                            )
                _lib = pjoin(target,'lib')
                _inc = pjoin(target,'include','freetype2')
                aDir(L, _lib)
                aDir(I, _inc)
                #print('!!!!! L=%s I=%s' % (L,I))
            elif machine=='x86_64':
                aDir(L,'/usr/local/lib')
                aDir(I, "/usr/local/include/freetype2")
            # attempt to make sure we pick freetype2 over other versions
            aDir(I, "/sw/include/freetype2")
            aDir(I, "/sw/lib/freetype2/include")
            # fink installation directories
            aDir(L, "/sw/lib")
            aDir(I, "/sw/include")
            # darwin ports installation directories
            aDir(L, "/opt/local/lib")
            aDir(I, "/opt/local/include")
        aDir(I, "/usr/local/include")
        aDir(L, "/usr/local/lib")
        aDir(I, "/usr/include")
        aDir(L, "/usr/lib")
        aDir(I, "/usr/include/freetype2")
        if addrSize==64:
            aDir(L, "/usr/lib/lib64")
            aDir(L, "/usr/lib/x86_64-linux-gnu")
        else:
            aDir(L, "/usr/lib/lib32")
        prefix = sysconfig.get_config_var("prefix")
        if prefix:
            aDir(L, pjoin(prefix, "lib"))
            aDir(I, pjoin(prefix, "include"))
        if libname:
            gsn = ''.join((('lib' if not libname.startswith('lib') else ''),libname,'*'))
            L = list(filter(lambda _: glob.glob(pjoin(_,gsn)),L))
        for d in I:
            mif = findFile(d,'ft2build.h')
            if mif:
                #print('!!!!! d=%s --> mif=%r' % (d,mif))
                break
        else:
            mif = None
        if mif:
            d = dirname(mif)
            I = [dirname(d), d]

            #fix for some RHEL systems from James Brown jbrown at easypost dot com
            subdir = pjoin(d,'freetype2')
            if isdir(subdir):
                I.append(subdir)

            ftv = freetypeVersion(findFile(d,'freetype.h'),'22')
        else:
            print('!!!!! cannot find ft2build.h')
            sys.exit(1)
        return ftv,I,L
inc_lib_dirs=inc_lib_dirs()

def _find_ccode(cn):
    fn = normpath(abspath(pjoin('src',cn)))
    return dirname(fn) if isfile(fn) else None

def url2data(url,returnRaw=False):
    import urllib.request as ureq
    remotehandle = ureq.urlopen(url)
    try:
        raw = remotehandle.read()
        return raw if returnRaw else io.BytesIO(raw)
    finally:
        remotehandle.close()

def ensureResourceStuff(
                ftpName='winstuff.zip',
                buildName='winstuff',
                extract='zip',
                baseDir=pjoin(pkgDir,'build'),
                ):
    url='https://www.reportlab.com/ftp/%s' % ftpName
    target=pjoin(baseDir,buildName)
    done = pjoin(target,'.done')
    if not isfile(done):
        if not isdir(target):
            os.makedirs(target)
            if extract=='zip':
                import zipfile
                zipfile.ZipFile(url2data(url), 'r').extractall(path=target)
            elif extract=='tar':
                import tarfile
                tarfile.open(fileobj=url2data(url), mode='r:gz').extractall(path=target)
            import time
            with open(done,'w') as _:
                _.write(time.strftime('%Y%m%dU%H%M%S\n',time.gmtime()))
    return target

def canImport(pkg):
    ns = [pkg.find(_) for _ in '<>=' if _ in pkg]
    if ns: pkg =pkg[:min(ns)]
    ns = {}
    try:
        exec('import %s as M' % pkg,{},ns)
    except:
        if verbose>=2:
            showTraceback("can't import %s" % pkg)
    return 'M' in ns

def vopt(opt):
    opt = '--%s=' % opt
    v = [_ for _ in sys.argv if _.startswith(opt)]
    for _ in v: sys.argv.remove(_)
    n = len(opt)
    return list(filter(None,[_[n:] for _ in v]))

class QUPStr(str):
    def __new__(cls,s,u,p):
        self = str.__new__(cls,s)
        self.u = u
        self.p = p
        return self

def qup(url, pat=re.compile(r'(?P<scheme>https?://)(?P<up>[^@]*)(?P<rest>@.*)$')):
    '''urlquote the user name and password'''
    m = pat.match(url)
    if m:
        u, p = m.group('up').split(':',1)
        url = "%s%s:%s%s" % (m.group('scheme'),urlquote(u),urlquote(p),m.group('rest'))
    else:
        u = p = ''
    return QUPStr(url,u,p)

def showEnv():
    action = -1 if specialOption('--show-env-only') else 1 if specialOption('--show-env') else 0
    if not action: return
    print('+++++ setup.py environment')
    print('+++++ sys.version = %s' % sys.version.replace('\n',''))
    import platform
    for k in sorted((_ for _ in dir(platform) if not _.startswith('_'))):
        try:
            v = getattr(platform,k)
            if isinstance(v,(str,list,tuple,bool)):
                v = repr(v)
            if callable(v) and v.__module__=='platform':
                v = repr(v())
            else:
                continue
        except:
            v = '?????'
        print('+++++ platform.%s = %s' % (k,v))
    print('--------------------------')
    for k, v in sorted(os.environ.items()):
        print('+++++ environ[%s] = %r' % (k,v))
    print('--------------------------')
    if action<0:
        sys.exit(0)

def main():
    showEnv()
    debug_compile_args = []
    debug_link_args = []
    debug_macros = []
    debug = int(os.environ.get('RL_DEBUG','0'))
    if debug:
        if sys.platform == 'win32':
            debug_compile_args=['/Zi']
            debug_link_args=['/DEBUG']
        if debug>1:
            debug_macros.extend([('RL_DEBUG',debug), ('ROBIN_DEBUG',None)])
    if mdbg:
        debug_macros.extend([('MEMORY_DEBUG',None)])

    LIBRARIES=[]
    EXT_MODULES = []

    RENDERPM = _find_ccode('_renderPM.c')
    print( '===================================================')
    print( 'Attempting build of _renderPM')
    print( 'extension from %r'%RENDERPM)
    print( '===================================================')
    GT1_DIR=pjoin(RENDERPM,'gt1')

    if not usla:
        LIBART_INC=None #don't use system libart
    else:
        #check for an installed libart
        LIBART_INC = list(sorted(glob.glob('/usr/include/libart-*/libart_lgpl/libart-features.h')))
    if LIBART_INC:
        def installed_libart_version(fn):
            for l in open(fn, 'r').readlines():
                if l.startswith('#define LIBART_VERSION'):
                    v = l[:-1].split(' ')[-1]
                    return v
            return '"0.0.0"'
        LIBART_INC = LIBART_INC[-1]
        LIBART_VERSION = installed_libart_version(LIBART_INC)
        LIBART_INC = os.path.dirname(LIBART_INC)
        LIBART_SOURCES=[]
        LIBART_LIB = ['art_lgpl_2']
        print('will use installed libart %s' % LIBART_VERSION.replace('"',''))
    else:
        LIBART_DIR = LIBART_INC = pjoin(RENDERPM,'libart_lgpl')
        LIBART_LIB = []
        make_libart_config(LIBART_DIR)
        LIBART_SOURCES=[
                pjoin(LIBART_DIR,'art_vpath_bpath.c'),
                pjoin(LIBART_DIR,'art_rgb_pixbuf_affine.c'),
                pjoin(LIBART_DIR,'art_rgb_svp.c'),
                pjoin(LIBART_DIR,'art_svp.c'),
                pjoin(LIBART_DIR,'art_svp_vpath.c'),
                pjoin(LIBART_DIR,'art_svp_vpath_stroke.c'),
                pjoin(LIBART_DIR,'art_svp_ops.c'),
                pjoin(LIBART_DIR,'art_svp_wind.c'),
                pjoin(LIBART_DIR,'art_vpath.c'),
                pjoin(LIBART_DIR,'art_vpath_dash.c'),
                pjoin(LIBART_DIR,'art_affine.c'),
                pjoin(LIBART_DIR,'art_rect.c'),
                pjoin(LIBART_DIR,'art_rgb_affine.c'),
                pjoin(LIBART_DIR,'art_rgb_affine_private.c'),
                pjoin(LIBART_DIR,'art_rgb.c'),
                pjoin(LIBART_DIR,'art_rgb_rgba_affine.c'),
                pjoin(LIBART_DIR,'art_svp_intersect.c'),
                pjoin(LIBART_DIR,'art_svp_render_aa.c'),
                pjoin(LIBART_DIR,'art_misc.c'),
                ]
        def libart_version():
            pat0 = re.compile(r'^\s*LIBART_(MAJOR|MINOR|MICRO)_VERSION\s*=\s*(\d+)')
            pat1 = re.compile(r'^\s*m4_define\s*\(\s*\[\s*libart_(major|minor|micro)_version\s*\]\s*,\s*\[(\d+)\]\s*\)')
            def check_match(l):
                for p in (pat0, pat1):
                    m = p.match(l)
                    if m: return m
            K = ('major','minor','micro')
            D = {}
            for l in open(pjoin(LIBART_DIR,'configure.in'),'r').readlines():
                m = check_match(l)
                if m:
                    D[m.group(1).lower()] = m.group(2)
                    if len(D)==3: break
            return '.'.join(map(lambda k,D=D: D.get(k,'?'),K))
        LIBART_VERSION = libart_version()
        print('will use package libart %s' % LIBART_VERSION.replace('"',''))

    SOURCES=[pjoin(RENDERPM,'_renderPM.c'),
                pjoin(GT1_DIR,'gt1-parset1.c'),
                pjoin(GT1_DIR,'gt1-dict.c'),
                pjoin(GT1_DIR,'gt1-namecontext.c'),
                pjoin(GT1_DIR,'gt1-region.c'),
                ]+LIBART_SOURCES

    if platform=='win32':
        target = ensureResourceStuff()
        FT_LIB = pjoin(target,'libs','amd64' if addrSize==64 else 'x86','freetype.lib')
        if not isfile(FT_LIB):
            print('freetype lib %r not found' % FT_LIB, pfx='!!!!')
            FT_LIB=[]
        if FT_LIB:
            FT_INC_DIR = pjoin(target,'include')
            if not isfile(pjoin(FT_INC_DIR,'ft2build.h')):
                FT_INC_DIR = pjoin(FT_INC_DIR,'freetype2')
                if not isfile(pjoin(FT_INC_DIR,'ft2build.h')):
                    print('freetype2 include folder %r not found' % FT_INC_DIR, pfx='!!!!!')
                    FT_LIB=FT_LIB_DIR=FT_INC_DIR=FT_MACROS=[]
            FT_MACROS = [('RENDERPM_FT',None)]
            FT_LIB_DIR = [dirname(FT_LIB)]
            FT_INC_DIR = [FT_INC_DIR]
            FT_LIB_PATH = FT_LIB
            FT_LIB = [splitext(basename(FT_LIB))[0]]
            if isdir(FT_INC_DIR[0]):
                print('installing with freetype %r' % FT_LIB_PATH)
        else:
            FT_LIB=FT_LIB_DIR=FT_INC_DIR=FT_MACROS=[]
    else:
        ftv, I, L = inc_lib_dirs('freetype')
        FT_LIB=['freetype']
        FT_LIB_DIR=L
        FT_INC_DIR=I
        FT_MACROS = [('RENDERPM_FT',None)]
        print('installing with freetype version %s' % ftv)
        print('FT_LIB_DIR=%r FT_INC_DIR=%r' % (FT_LIB_DIR,FT_INC_DIR))
    if not FT_LIB:
        raise ValueError('Cannot install without freetype which has not been found')

    def get_la_info():
        limited_api_kwds = {}
        if specialOption('--abi3') or os.environ.get('ABI3_WHEEL','0')=='1':
            #cpstr = get_abi_tag()
            #if cpstr.startswith("cp"):
            if True:
                lav = '0x03070000'
                cpstr = 'cp37'
                if sys.platform == "darwin":
                    machine = sysconfig.get_platform().split('-')[-1]
                    if machine=='arm64' or os.environ.get('ARCHFLAGS','')=='-arch arm64':
                        #according to cibuildwheel/github M1 supports pythons >= 3.8
                        cpstr = 'cp38'
                        lav = '0x03080000'

                class bdist_wheel_abi3(bdist_wheel):
                    __cpstr = cpstr
                    def get_tag(self):
                        python, abi, plat = super().get_tag()
                        if python.startswith("cp"):
                            abi = 'abi3'
                            python = self.__cpstr
                        return python,abi,plat

                limited_api_kwds = dict(
                            cmdclass={"bdist_wheel": bdist_wheel_abi3},
                            macros=[("Py_LIMITED_API", lav)],
                            )

        return limited_api_kwds

    limited_api_kwds = get_la_info()
    limited_api_macros = limited_api_kwds.pop('macros',[])

    def getVersionFromCCode(fn):
        with open(fn,'r') as _:
            tag = re.search(r'^#define\s+VERSION\s+"([^"]*)"',_.read(),re.M)
        return tag and tag.group(1) or ''

    def getREADME_md(fn='README.md'):
        try:
            with open(fn,'r') as _:
                return _.read()
        except:
            return """This is an bitmap render accelerator module for the ReportLab Toolkit Open Source Python library for generating PDFs and graphics."""

    setup(
        ext_modules=[
                    Extension( '_rl_renderPM',
                                SOURCES,
                                include_dirs=[RENDERPM,LIBART_INC,GT1_DIR]+FT_INC_DIR,
                                define_macros=(
                                        FT_MACROS
                                        +[('LIBART_COMPILATION',None)]
                                        +debug_macros+[('LIBART_VERSION',LIBART_VERSION)]
                                        +limited_api_macros
                                        ),
                                py_limited_api = limited_api_macros!=[],
                                library_dirs=[]+FT_LIB_DIR,

                                # libraries to link against
                                libraries=FT_LIB+LIBART_LIB,
                                extra_compile_args=debug_compile_args,
                                extra_link_args=debug_link_args,
                                ),
                            
                    ],
        name="rl_renderPM",
        version=getVersionFromCCode(pjoin('src','_renderPM.c')),
        license="BSD license (see LICENSE.txt for details), Copyright (c) 2000-2022, ReportLab Inc.",
        description="Bitmap Render Acclerator for ReportLab",
        long_description=getREADME_md(),
        long_description_content_type="text/x-rst",
        author="Andy Robinson, Robin Becker, the ReportLab team and the community",
        author_email="reportlab-users@lists2.reportlab.com",
        url="http://www.reportlab.com/",
        packages=[],
        package_data = {},
        classifiers = [
            'Development Status :: 5 - Production/Stable',
            'Intended Audience :: Developers',
            'License :: OSI Approved :: BSD License',
            'Topic :: Printing',
            'Topic :: Text Processing :: Markup',
            'Programming Language :: Python :: 3',
            'Programming Language :: Python :: 3.7',
            'Programming Language :: Python :: 3.8',
            'Programming Language :: Python :: 3.9',
            'Programming Language :: Python :: 3.10',
            'Programming Language :: Python :: 3.11',
            ],
        license_files = ['LICENSE.txt'],
                
        #this probably only works for setuptools, but distutils seems to ignore it
        install_requires=[],
        python_requires='>=3.7,<4',
        extras_require={
            },
        **limited_api_kwds
        )

if __name__=='__main__':
    main()
