#!/usr/bin/env python

###
# Copyright (c) 2002-2009 Systems in Motion
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#

###
# Pivy distutils setup script.
#

"""Pivy is a Coin binding for Python. Coin is a high-level 3D graphics
library with a C++ Application Programming Interface. Coin uses
scene-graph data structures to render real-time graphics suitable for
mostly all kinds of scientific and engineering visualization
applications.
"""
from __future__ import print_function

###
# Setup file for the Pivy distribution.
#
import glob, os, shutil, subprocess, sys

from distutils.command.build import build
from distutils.command.clean import clean
from distutils.command.install import install
from distutils.core import setup
from distutils.extension import Extension
from distutils import sysconfig

# if we are on a Gentoo box salute the chap and output stuff in nice colors
# Gentoo is Python friendly, so be especially friendly to them! ;)
try:
    from portage.output import green, blue, turquoise, red, yellow
    print(red("Oooh, it's a Gentoo! Nice nice! tuhtah salutes you! :)"))
except:
    try:
        from colorama import Fore, Style
        def red(text): return Fore.RED + text + Style.RESET_ALL
        def green(text): return Fore.GREEN + text + Style.RESET_ALL
        def blue(text): return Fore.BLUE + text + Style.RESET_ALL
        def turquoise(text): return Fore.CYAN + text + Style.RESET_ALL
        def yellow(text): return Fore.YELLOW + text + Style.RESET_ALL
    except:
        def red(text): return text
        def green(text): return text
        def blue(text): return text
        def turquoise(text): return text
        def yellow(text): return text

PIVY_CLASSIFIERS = """\
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Operating System :: MacOS :: MacOS X
Operating System :: Microsoft :: Windows
Operating System :: Unix
Programming Language :: Python
Topic :: Multimedia :: Graphics
Topic :: Multimedia :: Graphics :: 3D Modeling
Topic :: Multimedia :: Graphics :: 3D Rendering
Topic :: Software Development :: Libraries :: Python Modules
"""

__dir_name__ = os.path.dirname(__file__)
pivy_dir = os.path.join(__dir_name__, "pivy")
sys.path.append(pivy_dir)
import pivy_meta
PIVY_VERSION = pivy_meta.__version__
sys.path.pop(-1)

class pivy_build(build):
    PIVY_SNAKES = r"""
                            _____
                        .-'`     '.                                     
                     __/  __       \                                   
                    /  \ /  \       |    ___                          
                   | /`\| /`\|      | .-'  /^\/^\                   
                   | \(/| \(/|      |/     |) |)|                     
                  .-\__/ \__/       |      \_/\_/__..._             
          _...---'-.                /   _              '.               
         /,      ,             \   '|  `\                \           
        | ))     ))           /`|   \    `.       /)  /) |             
        | `      `          .'       |     `-._         /               
        \                 .'         |     ,_  `--....-'               
         `.           __.' ,         |     / /`'''`                     
           `'-.____.-' /  /,         |    / /                           
               `. `-.-` .'  \        /   / |                           
                 `-.__.'|    \      |   |  |-.                         
                    _.._|     |     /   |  |  `'.                       
              .-''``    |     |     |   /  |     `-.                    
           .'`         /      /     /  |   |        '.                  
         /`           /      /     |   /   |\         \               
        /            |      |      |   |   /\          |               
       ||            |      /      |   /     '.        |                
       |\            \      |      /   |       '.      /              
       \ `.           '.    /      |    \        '---'/               
        \  '.           `-./        \    '.          /                
         '.  `'.            `-._     '.__  '-._____.'--'''''--.         
           '-.  `'--._          `.__     `';----`              \       
              `-.     `-.          `.''```                     ;        
                 `'-..,_ `-.         `'-.                     /         
                        '.  '.           '.                 .'          


                            ~~~ HISSSSSSSSSS ~~~
                           Welcome to Pivy %s!
                 Building Pivy has never been so much fun!

    """ % PIVY_VERSION

    pivy_header_include = """\
#ifdef __PIVY__
%%include %s
#endif

"""
    
    SWIG = ((sys.platform == "win32" and "swig.exe") or "swig")

    SWIG_SUPPRESS_WARNINGS = "-w302,306,307,312,314,325,361,362,467,389,503,509,510"
    if sys.version_info.major < 3:
        SWIG_PARAMS = "-c++ -python -includeall -modern -D__PIVY__ " + \
                      "-I. -Ifake_headers -I\"%s\" %s -o %s_wrap.cpp " + \
                      "interfaces" + os.sep + "%s.i"
    else:
        SWIG_PARAMS = "-c++ -python -includeall -modern -py3 -D__PIVY__ " + \
                      "-I. -Ifake_headers -I\"%s\" %s -o %s_wrap.cpp " + \
                      "interfaces" + os.sep + "%s.i"


    SOGUI = ['soqt', 'soxt', 'sogtk', 'sowin']

    MODULES = {}
    MODULES['coin']      = ['_coin',      'coin-config',      'pivy.',        "coin"]
    MODULES['soqt']      = ['gui._soqt',  'soqt-config',      'pivy.gui.',    "soqt"]

    if sys.version_info.major < 3:
        MODULES['simvoleon'] = ['_simvoleon', 'simvoleon-config', 'pivy.',        "simvoleon"]
        MODULES['soxt']      = ['gui._soxt',  'soxt-config',      'pivy.gui.',    "soxt"]
        MODULES['sogtk']     = ['gui._sogtk', 'sogtk-config',     'pivy.gui.',    "sogtk"]
        MODULES['sowin']     = ['gui._sowin', 'sowin-config',     'pivy.gui.',    "sowin"]

    if sys.version_info.major < 3:
        MODULES['coin'][3] = 'coin2'
        MODULES['soqt'][3] = 'soqt2'

    SUPPORTED_SWIG_VERSIONS = ['1.3.31', '1.3.33', '1.3.35', '1.3.40']
    SWIG_VERSION = ""
    SWIG_COND_SYMBOLS = []
    CXX_INCS = "-Iinterfaces "
    CXX_LIBS = ""

    ext_modules = []
    py_modules = ['pivy.quarter.ContextMenu',
                  'pivy.quarter.ImageReader',
                  'pivy.quarter.QuarterWidget',
                  'pivy.quarter.SensorManager',
                  'pivy.quarter.SignalThread',
                  'pivy.quarter.devices.DeviceHandler',
                  'pivy.quarter.devices.DeviceManager',
                  'pivy.quarter.devices.KeyboardHandler',
                  'pivy.quarter.devices.MouseHandler',
                  'pivy.quarter.eventhandlers.DragDropHandler',
                  'pivy.quarter.eventhandlers.EventHandler',
                  'pivy.quarter.eventhandlers.EventManager',
                  'pivy.quarter.plugins.designer.python.PyQuarterWidgetPlugin',
                  'pivy.utils',
                  'pivy.graphics.']

    if sys.version_info.major < 3:
        py_modules = ['pivy.sogui'] + py_modules

    def do_os_popen(self, cmd):
        "return the output of a command in a single line"
        fd = os.popen(cmd)
        lines = fd.readlines()
        for i in range(len(lines)):
            lines[i] = lines[i].strip()
        lines = " ".join(lines)
        fd.close()
        return lines

    def check_cmd_exists(self, cmd):
        "return the path of the specified command if it exists"
        print(blue("Checking for %s..." % cmd))
        for path in os.environ['PATH'].split(os.path.pathsep):
            if os.path.exists(os.path.join(path, cmd)):
                print(blue("'%s'" % os.path.join(path, cmd)))
                return 1
        print(red("not found."))
        return 0

    def check_python_version(self):
        "check the Python version"
        print(blue("Python version...%s" % sys.version.split(" ")[0]))
        if int(sys.version[0]) < 2:
            print(red("Pivy only works with Python versions >= 2.0."))
            sys.exit(1)

    def check_coin_version(self):
        "check the Coin version"
        if sys.platform == "win32": return
        if not self.check_cmd_exists("coin-config"):
            sys.exit(1)
        print(blue("Coin version..."))
        version = self.do_os_popen("coin-config --version")
        print(blue("%s" % version))
        if not version.startswith('3'):
            print(yellow("** Warning: Pivy has only been tested with Coin "
                         "versions Coin-dev 3."))

    def check_simvoleon_version(self):
        "return if SIMVoleon is available and check the version"
        if sys.platform == "win32" or not self.check_cmd_exists("simvoleon-config"):
            self.MODULES.pop('simvoleon', None)
            return False

        print(blue("SIMVoleon version..."))
        version = self.do_os_popen("simvoleon-config --version")
        print(blue("%s" % version))
        if not version.startswith('2.0'):
            print(yellow("** Warning: Pivy has only been tested with SIMVoleon "
                         "versions 2.0.x."))
        return True

    def check_gui_bindings(self):
        "check for availability of SoGui bindings and removes the not available ones"
        if sys.platform == "win32":
            self.MODULES.pop('soxt', None)
            self.MODULES.pop('sogtk', None)
            print(blue("Checking for SoWin..."))
            if not os.path.exists(os.path.join(os.getenv("COINDIR"), "include", "Inventor", "Win", "SoWin.h")):
                self.MODULES.pop('sowin', None)
                print(red("COINDIR\\include\\Inventor\\Win\\SoWin.h not found. (SoWin bindings won't be built)"))
            print(blue("Checking for QTDIR environment variable..."))
            if os.getenv("QTDIR"):
                print(blue(os.getenv("QTDIR")))
            else:
                self.MODULES.pop('soqt', None)
                print(red("not set. (SoQt bindings won't be built)"))
        else:
            for gui in self.SOGUI:
                if gui not in self.MODULES:
                    continue
                gui_config_cmd = self.MODULES[gui][1]
                if not self.check_cmd_exists(gui_config_cmd):
                    self.MODULES.pop(gui, None)
                else:
                    print(blue("Checking for %s version..." % gui))
                    version = self.do_os_popen("%s --version" % gui_config_cmd)
                    print(blue("%s" % version))

    def get_coin_features(self):
        "set the global variable SWIG_COND_SYMBOLS needed for conditional " + \
        "wrapping"
        if sys.platform == "win32": return
        print(blue("Checking for Coin features..."))
        if not os.system("coin-config --have-feature 3ds_import"):
            self.SWIG_COND_SYMBOLS.append("-DHAVE_FEATURE_3DS_IMPORT")
            print(green("3ds import "))

        if not os.system("coin-config --have-feature vrml97"):
            self.SWIG_COND_SYMBOLS.append("-DHAVE_FEATURE_VRML97")
            print(green("vrml97 "))

        if not os.system("coin-config --have-feature sound"):
            self.SWIG_COND_SYMBOLS.append("-DHAVE_FEATURE_SOUND")
            print(green("sound "))

        if not os.system("coin-config --have-feature superglu"):
            self.SWIG_COND_SYMBOLS.append("-DHAVE_FEATURE_SUPERGLUE")
            print(green("superglu "))

        if not os.system("coin-config --have-feature threads"):
            self.SWIG_COND_SYMBOLS.append("-DHAVE_FEATURE_THREADS")
            print(green("threads "))

        if not os.system("coin-config --have-feature threadsafe"):
            self.SWIG_COND_SYMBOLS.append("-DHAVE_FEATURE_THREADSAFE")
            print(green("threadsafe "))

        print()

    def check_swig_version(self, swig):
        "check for the swig version"
        global SWIG_VERSION
        if not self.check_cmd_exists(swig):
            # on some systems there is only a swig3.0 so check for this and
            # set SWIG to "swig3.0"
            swig = "swig3.0"
            if not self.check_cmd_exists(swig):
                sys.exit(1)
            else:
                self.SWIG = swig
        print(blue("Checking for SWIG version..."))
        p = subprocess.Popen("%s -version" % swig, 
                             shell=True, stdout=subprocess.PIPE)
        version = str(p.stdout.readlines()[1].strip()).split(" ")[2]
        p.stdout.close()
        print(blue("%s" % version))
        SWIG_VERSION = version
        if not version in self.SUPPORTED_SWIG_VERSIONS:
            print(yellow("Warning: Pivy has only been tested with the following " + \
                         "SWIG versions: %s." % " ".join(self.SUPPORTED_SWIG_VERSIONS)))

    def copy_and_swigify_headers(self, includedir, dirname, files):
        """Copy the header files to the local include directories. Add an
        #include line at the beginning for the SWIG interface files..."""
        for file in files:
            if not os.path.isfile(os.path.join(dirname, file)):
                continue

            if file[-2:] == ".i":
                file_i = os.path.join(dirname, file)
                file_h = os.path.join(dirname, file)[:-2] + ".h"

                if (not os.path.exists(file_h) and
                    os.path.exists(os.path.join(includedir, file_h))):
                    shutil.copyfile(os.path.join(includedir, file_h), file_h)
                    sys.stdout.write(' ' + turquoise(file_h))
                    fd = open(file_h, 'r+')
                    contents = fd.readlines()

                    ins_line_nr = -1
                    for line in contents:
                        ins_line_nr += 1
                        if line.find("#include ") != -1:
                            break

                    if ins_line_nr != -1:
                        contents.insert(ins_line_nr, self.pivy_header_include % (file_i))
                        fd.seek(0)
                        fd.writelines(contents)
                    else:
                        print(blue("[") + red("failed") + blue("]"))
                        sys.exit(1)
                    fd.close
            # fixes for SWIG 1.3.21 and upwards
            # (mostly workarounding swig's preprocessor "function like macros"
            # preprocessor bug when no parameters are provided which then results
            # in no constructors being created in the wrapper)
            elif file[-4:] == ".fix":
                sys.stdout.write(' ' + red(os.path.join(dirname, file)[:-4]))
                shutil.copyfile(os.path.join(dirname, file),
                                os.path.join(dirname, file)[:-4])
            # had to introduce this because windows is a piece of crap
            elif sys.platform == "win32" and file[-6:] == ".win32":
                sys.stdout.write(' ' + red(os.path.join(dirname, file)[:-6]))
                shutil.copyfile(os.path.join(dirname, file),
                                os.path.join(dirname, file)[:-6])

    def pivy_configure(self):
        "configure Pivy"
        print(turquoise(self.PIVY_SNAKES))
        print(blue("Platform...%s" % sys.platform))
        self.check_python_version()
        self.check_swig_version(self.SWIG)
        self.check_coin_version()
        self.get_coin_features()
        if self.SOGUI: self.check_gui_bindings()
        
        if 'simvoleon' in self.MODULES and self.check_simvoleon_version():
            if sys.platform == "win32":
                INCLUDE_DIR = os.getenv("SIMVOLEONDIR") + "\\include"
            else:
                INCLUDE_DIR = self.do_os_popen("simvoleon-config --includedir")

            sys.stdout.write(blue("Preparing") + green(" VolumeViz ") + blue("headers:"))
            dir_gen = os.walk("VolumeViz", INCLUDE_DIR)
            for _dir, _, names in dir_gen:
                self.copy_and_swigify_headers(INCLUDE_DIR, _dir, names)
            print(green("."))

        if sys.platform == "win32":
            INCLUDE_DIR = os.path.join(os.getenv("COINDIR"), "include")
        else:
            INCLUDE_DIR = self.do_os_popen("coin-config --includedir")

        sys.stdout.write(blue("Preparing") + green(" Inventor ") + blue("headers:"))
        dir_gen = os.walk("Inventor", INCLUDE_DIR)
        for _dir, _, names in dir_gen:
            self.copy_and_swigify_headers(INCLUDE_DIR, _dir, names)
        print(green("."))

    def swig_generate(self):
        "build all available modules"

        quote = lambda s : '"' + s + '"'
        for module in self.MODULES:
            module_name = self.MODULES[module][0]
            config_cmd = self.MODULES[module][1]
            module_pkg_name = self.MODULES[module][2]
            mod_hack_name = self.MODULES[module][3]
            mod_out_prefix = module_pkg_name.replace('.', os.sep) + module
            
            if sys.platform == "win32":
                INCLUDE_DIR = os.path.join(os.getenv("COINDIR"), "include")
                CPP_FLAGS = "-I" + quote(INCLUDE_DIR) + " " + \
                            "-I" + quote(os.path.join(os.getenv("COINDIR"), "include", "Inventor", "annex")) + \
                            " /DCOIN_DLL /wd4244 /wd4049"
                # aquire highest non-debug Coin library version
                try:
                    LDFLAGS_LIBS = quote(max(glob.glob(os.path.join(os.getenv("COINDIR"), "lib", "coin?.lib")))) + " "
                # with cmake the coin library is named Coin4.lib
                except ValueError:
                    LDFLAGS_LIBS = quote(max(glob.glob(os.path.join(os.getenv("COINDIR"), "lib", "Coin?.lib")))) + " "

                if module == "sowin":
                    CPP_FLAGS += " /DSOWIN_DLL"
                    LDFLAGS_LIBS += quote(os.path.join(os.getenv("COINDIR"), "lib", "sowin1.lib"))
                elif module == "soqt":
                    CPP_FLAGS += " -I" + '"' + os.getenv("QTDIR") + "\\include\"  /DSOQT_DLL"
                    if os.path.isdir(os.getenv("QTDIR") + "\\include\Qt\""):
                        CPP_FLAGS += " -I" + '"' + os.getenv("QTDIR") + "\\include\Qt\""
                        LDFLAGS_LIBS += os.path.join(os.getenv("COINDIR"), "lib", "soqt1.lib") + " "
                    else:
                        # workaround for conda qt4:
                        CPP_FLAGS += " -I" + '"' + os.getenv("QTDIR") + "\\include\qt\Qt\""
                        CPP_FLAGS += " -I" + '"' + os.getenv("QTDIR") + "\\include\qt\""
                        LDFLAGS_LIBS += os.path.join(os.getenv("COINDIR"), "lib", "SoQt.lib") + " "
            else:
                INCLUDE_DIR = self.do_os_popen("coin-config --includedir")
                if module_name != 'coin':
                    mod_include_dir = self.do_os_popen("%s --includedir" % config_cmd)
                    if mod_include_dir != INCLUDE_DIR:
                        INCLUDE_DIR += '\" -I\"%s' % mod_include_dir
                CPP_FLAGS = self.do_os_popen("%s --cppflags" % config_cmd) + " -Wno-unused -Wno-maybe-uninitialized"
                LDFLAGS_LIBS = self.do_os_popen("%s --ldflags --libs" % config_cmd)

            if not os.path.isfile(mod_out_prefix + "_wrap.cpp"):
                print(red("\n=== Generating %s_wrap.cpp for %s ===\n" %
                          (mod_out_prefix, module)))
                print(blue(self.SWIG + " " + self.SWIG_SUPPRESS_WARNINGS + " " + self.SWIG_PARAMS %
                           (INCLUDE_DIR,
                            self.CXX_INCS,
                            mod_out_prefix, module)))
                if os.system(self.SWIG + " " + self.SWIG_SUPPRESS_WARNINGS + " " + self.SWIG_PARAMS %
                             (INCLUDE_DIR,
                              self.CXX_INCS,
                              mod_out_prefix, mod_hack_name)):
                    print(red("SWIG did not generate wrappers successfully! ** Aborting **"))
                    sys.exit(1)
            else:
                print(red("=== %s_wrap.cpp for %s already exists! ===" % (mod_out_prefix, module_pkg_name + module)))

            self.ext_modules.append(Extension(module_name, [mod_out_prefix + "_wrap.cpp"],
                                              extra_compile_args=(self.CXX_INCS + CPP_FLAGS).split(),
                                              extra_link_args=(self.CXX_LIBS + LDFLAGS_LIBS).split()))

    def run(self):
        "the entry point for the distutils build class"
        if sys.platform == "win32" and not os.getenv("COINDIR"):
            print("Please set the COINDIR environment variable to your Coin root directory! ** Aborting **")
            sys.exit(1)

        self.pivy_configure()
        self.swig_generate()

        for cmd_name in self.get_sub_commands():
            self.run_command(cmd_name)

class pivy_clean(clean):
    pivy_path = 'pivy' + os.sep
    gui_path = 'pivy' + os.sep + 'gui' + os.sep
    REMOVE_FILES = (pivy_path + '__init__.pyc', gui_path + '__init__.pyc',
                    pivy_path + 'coin_wrap.cpp', pivy_path + 'coin2_wrap.cpp',  pivy_path + 'coin.py',  pivy_path + 'coin.pyc',
                    pivy_path + 'simvoleon_wrap.cpp',  pivy_path + 'simvoleon.py',  pivy_path + 'simvoleon.pyc',
                    gui_path + 'soqt_wrap.cpp',  gui_path + 'soqt.py',  gui_path + 'soqt.pyc',
                    gui_path + 'sogtk_wrap.cpp', gui_path + 'sogtk.py', gui_path + 'sogtk.py',
                    gui_path + 'soxt_wrap.cpp',  gui_path + 'soxt.py',  gui_path + 'soxt.pyc',
                    gui_path + 'sowin_wrap.cpp', gui_path + 'sowin.py', gui_path + 'sowin.pyc',
                    pivy_path + 'sogui.pyc')

    def remove_headers(self, arg, dirname, files):
        "remove the coin headers from the pivy Inventor directory"
        for file in files:
            if not os.path.isfile(os.path.join(dirname, file)) or file[-2:] != ".h":
                continue
            sys.stdout.write(' ' + turquoise(os.path.join(dirname, file)))
            os.remove(os.path.join(dirname, file))

    def run(self):
        "the entry point for the distutils clean class"
        sys.stdout.write(blue("Cleaning headers:"))
        dir_gen = os.walk("Inventor")
        for _dir, _, names in dir_gen:
            self.remove_headers(None, _dir, names)

        dir_gen = os.walk("VolumeViz")
        for _dir, _, names in dir_gen:
            self.remove_headers(None, _dir, names)

        # remove the SWIG generated wrappers
        for wrapper_file in self.REMOVE_FILES:
            if os.path.isfile(wrapper_file):
                sys.stdout.write(' ' + turquoise(wrapper_file))
                os.remove(wrapper_file)
        print(green("."))

        clean.run(self)

for i in reversed(list(range(len(sys.argv)))):
    if sys.argv[i][:10] == "--without-":
        pivy_build.MODULES.pop(sys.argv[i][10:], None)
        del sys.argv[i]

setup(name = "Pivy",
      version = PIVY_VERSION,
      description = "A Python binding for Coin",
      long_description = __doc__,
      author = "Tamer Fahmy",
      author_email = "tamer@sim.no",
      download_url="https://github.com/coin3d/pivy/releases",
      url = "http://pivy.coin3d.org/",
      cmdclass = {'build'   : pivy_build,
                  'clean'   : pivy_clean},
      ext_package = 'pivy',
      ext_modules = pivy_build.ext_modules,
      py_modules  = pivy_build.py_modules,
      packages = ['pivy', 'pivy.gui'],
      classifiers = [_f for _f in PIVY_CLASSIFIERS.split("\n") if _f],
      license = "BSD License",
      platforms = ['Any']
      )
