import os.path
import os.path
import sys

from glad.lang.common.generator import Generator
from glad.lang.common.util import makefiledir

if sys.version_info >= (3, 0):
    basestring = str


def _gl_types(gen, f):
    f.write('''
  GLdebugProc* = proc (
    source: GLenum,
    typ: GLenum,
    id: GLuint,
    severity: GLenum,
    length: GLsizei,
    message: ptr GLchar,
    userParam: pointer) {.stdcall.}

  GLdebugProcArb* = proc (
    source: GLenum,
    typ: GLenum,
    id: GLuint,
    severity: GLenum,
    len: GLsizei,
    message: ptr GLchar,
    userParam: pointer) {.stdcall.}

  GLdebugProcAmd* = proc (
    id: GLuint,
    category: GLenum,
    severity: GLenum,
    len: GLsizei,
    message: ptr GLchar,
    userParam: pointer) {.stdcall.}

  GLdebugProcKhr* = proc (
    source, typ: GLenum,
    id: GLuint,
    severity: GLenum,
    length: GLsizei,
    message: ptr GLchar,
    userParam: pointer) {.stdcall.}
''')


# TODO finish converting the egl, glx & wgl loaders to Nim

# def _egl_types(gen, f):
# def _glx_types(gen, f):
# def _wgl_types(gen, f):

NIMTYPES = {
    '__other': {
        'gl': _gl_types
#        'egl': _egl_types,
#        'glx': _glx_types,
#        'wgl': _wgl_types
    },

    'gl': {
        'GLboolean': 'bool',
        'GLbyte': 'int8',
        'GLchar': 'char',
        'GLcharARB': 'char',
        'GLubyte': 'uint8',
        'GLshort': 'int16',
        'GLushort': 'uint16',
        'GLint': 'int32',
        'GLuint': 'uint32',
        'GLfixed': 'int32',
        'GLint64': 'int64',
        'GLint64EXT': 'int64',
        'GLuint64': 'uint64',
        'GLuint64EXT': 'uint64',
        'GLsizei': 'int32',
        'GLenum': 'distinct uint32',
        'GLbitfield': 'distinct uint32',
        'GLhalf': 'uint16',
        'GLhalfARB': 'uint16',
        'GLhalfNV': 'uint16',
        'GLfloat': 'float32',
        'GLclampf': 'float32',
        'GLdouble': 'float64',
        'GLclampd': 'float64',
        'GLclampx': 'int32',
        'GLintptr': 'cint',
        'GLintptrARB': 'cint',
        'GLsizeiptr': 'cint',
        'GLsizeiptrARB': 'cint',
        'GLsync': 'distinct pointer',
        'GLeglClientBufferEXT': 'pointer', # GL_EXT_external_buffer
        'GLVULKANPROCNV': 'pointer', # GL_NV_draw_vulkan_image
        'GLeglImageOES': 'distinct pointer',
        'GLhandleARB': 'uint32',
        'GLvdpauSurfaceNV': 'int32',
        'GLvoid': 'pointer',
        'ClContext': 'distinct pointer',
        'ClEvent': 'distinct pointer',
    },
    'egl': {
        'EGLAttrib': 'int32',
        'EGLAttribKHR': 'int32',
        'EGLBoolean': 'bool',
        'EGLClientBuffer': 'distinct pointer',
        'EGLConfig': 'distinct pointer',
        'EGLContext': 'distinct pointer',
        'EGLDeviceEXT': 'distinct pointer',
        'EGLDisplay': 'distinct pointer',
        'EGLImage': 'distinct pointer',
        'EGLImageKHR': 'distinct pointer',
        'EGLNativeFileDescriptorKHR': 'int32',
        'EGLOutputLayerEXT': 'distinct pointer',
        'EGLOutputPortEXT': 'distinct pointer',
        'EGLStreamKHR': 'distinct pointer',
        'EGLSurface': 'distinct pointer',
        'EGLSync': 'distinct pointer',
        'EGLSyncKHR': 'distinct pointer',
        'EGLSyncNV': 'distinct pointer',
        'EGLTimeKHR': 'uint64',
        'EGLTime': 'uint64',
        'EGLTimeNV': 'uint64',
        'EGLenum': 'distinct uint32',
        'EGLint': 'int32',
        'EGLsizeiANDROID': 'distinct pointer',
        'EGLuint64KHR': 'uint64',
        'EGLuint64MESA': 'uint64',
        'EGLuint64NV': 'uint64',
#        '__eglMustCastToProperFunctionPointerType': 'void function()'
    },
    'glx': {
        'GLbitfield': 'distinct uint32',
        'GLboolean': 'uint8',
        'GLenum': 'distinct uint32',
        'GLfloat': 'float32',
        'GLint': 'int32',
        'GLintptr': 'int32',
        'GLsizei': 'int32',
        'GLsizeiptr': 'int32',
        'GLubyte': 'uint8',
        'GLuint': 'uint32'
    },
    'wgl': {
        'GLbitfield': 'distinct uint32',
        'GLboolean': 'uint8',
        'GLenum': 'distinct uint32',
        'GLfloat': 'float32',
        'GLint': 'int32',
        'GLsizei': 'int32',
        'GLuint': 'uint32',
        'GLushort': 'uint16',
        'INT32': 'int32',
        'INT64': 'int64'
    },

    'SpecialNumbers': {
        'gl': [
            ('GL_FALSE', '0', None),
            ('GL_INVALID_INDEX', "0xFFFFFFFF'u32", None),
            ('GL_NONE', '0', None),
            ('GL_NONE_OES', '0', None),
            ('GL_NO_ERROR', '0', None),
            ('GL_ONE', '1', None),
            ('GL_TIMEOUT_IGNORED', "0xFFFFFFFFFFFFFFFF'u64", None),
            ('GL_TIMEOUT_IGNORED_APPLE', "0xFFFFFFFFFFFFFFFF'u64", None),
            ('GL_TRUE', '1', None),
            ('GL_VERSION_ES_CL_1_0', '1', None),
            ('GL_VERSION_ES_CL_1_1', '1', None),
            ('GL_VERSION_ES_CM_1_1', '1', None),
            ('GL_ZERO', '0', None),
        ],
        'egl': [
#            ('EGL_DONT_CARE', '-1', 'int'), ('EGL_UNKNOWN', '-1', 'int'),
#            ('EGL_NO_NATIVE_FENCE_FD_ANDROID', '-1', 'uint'),
#            ('EGL_DEPTH_ENCODING_NONE_NV', '0', 'uint'),
#            ('EGL_NO_CONTEXT', 'cast(EGLContext)0', 'EGLContext'),
#            ('EGL_NO_DEVICE_EXT', 'cast(EGLDeviceEXT)0', 'EGLDeviceEXT'),
#            ('EGL_NO_DISPLAY', 'cast(EGLDisplay)0', 'EGLDisplay'),
#            ('EGL_NO_IMAGE', 'cast(EGLImage)0', 'EGLImage'),
#            ('EGL_NO_IMAGE_KHR', 'cast(EGLImageKHR)0', 'EGLImageKHR'),
#            ('EGL_DEFAULT_DISPLAY', 'cast(EGLNativeDisplayType)0', 'EGLNativeDisplayType'),
#            ('EGL_NO_FILE_DESCRIPTOR_KHR', 'cast(EGLNativeFileDescriptorKHR)-1', 'EGLNativeFileDescriptorKHR'),
#            ('EGL_NO_OUTPUT_LAYER_EXT', 'cast(EGLOutputLayerEXT)0', 'EGLOutputLayerEXT'),
#            ('EGL_NO_OUTPUT_PORT_EXT', 'cast(EGLOutputPortEXT)0', 'EGLOutputPortEXT'),
#            ('EGL_NO_STREAM_KHR', 'cast(EGLStreamKHR)0', 'EGLStreamKHR'),
#            ('EGL_NO_SURFACE', 'cast(EGLSurface)0', 'EGLSurface'),
#            ('EGL_NO_SYNC', 'cast(EGLSync)0', 'EGLSync'),
#            ('EGL_NO_SYNC_KHR', 'cast(EGLSyncKHR)0', 'EGLSyncKHR'),
#            ('EGL_NO_SYNC_NV', 'cast(EGLSyncNV)0', 'EGLSyncNV'),
#            ('EGL_DISPLAY_SCALING', '10000', 'uint'),
#            ('EGL_FOREVER', '0xFFFFFFFFFFFFFFFF', 'ulong'),
#            ('EGL_FOREVER_KHR', '0xFFFFFFFFFFFFFFFF', 'ulong'),
#            ('EGL_FOREVER_NV', '0xFFFFFFFFFFFFFFFF', 'ulong')
        ],
        'glx': [
#            ('GLX_DONT_CARE', '0xFFFFFFFF', 'uint'),
#            ('GLX_CONTEXT_RELEASE_BEHAVIOR_NONE_ARB', '0', 'uint')
        ],
        'wgl': [
#            ('WGL_CONTEXT_RELEASE_BEHAVIOR_NONE_ARB', '0', 'uint'),
#            ('WGL_FONT_LINES', '0', 'uint'),
#            ('WGL_FONT_POLYGONS', 1, 'uint')
        ]
    },

    'SpecialEnumNames': {
        'gl': {
             'GL_BYTE': 'cGL_BYTE',
             'GL_SHORT': 'cGL_SHORT',
             'GL_INT': 'cGL_INT',
             'GL_FLOAT': 'cGL_FLOAT',
             'GL_DOUBLE': 'cGL_DOUBLE',
             'GL_FIXED': 'cGL_FIXED'
        },
        'egl': {},
        'glx': {},
        'wgl': {}
    },

    'SpecialFuncNames': {
        'gl': {
             'glGetTransformFeedbacki_v': 'glGetTransformFeedbacki_v2'
        },
        'egl': {},
        'glx': {},
        'wgl': {}
    }
}


class NimGenerator(Generator):
    NAME = 'nim'
    NAME_LONG = 'Nim'

    MODULE = 'glad'
    FILE_EXTENSION = '.nim'
    TYPE_DICT = NIMTYPES

    LOAD_GL_PREFIX = 'glad'
    EXT_PREFIX = 'GLAD_'

    def open(self):
        self._f_gl = open(self.make_path(self.spec.NAME), 'w')

    def close(self):
        self._f_gl.close()

    def generate_header(self):
        self._f_gl.write('#[')
        self._f_gl.write(self.header)
        self._f_gl.write(']#\n\n')
        self._f_gl.write('import strutils\n\n')
        self._f_gl.write('var glVersionMajor, glVersionMinor: int\n\n')

    def generate_loader(self, features, extensions):
        f = self._f_gl

        rfeatures = features
        if self.spec.NAME in ('egl', 'wgl'):
            features = {'egl': [], 'wgl': []}

        self.loader.write(f)
        self.loader.write_has_ext(f)

        written = set()
        for api, version in self.api.items():
            # core load procs
            for feature in features[api]:
                f.write('proc load_{}(load: proc) =\n'
                            .format(feature.name))
                if self.spec.NAME == 'gl':
                    f.write('  if not {}{}: return\n\n'.format(self.EXT_PREFIX,
                                                               feature.name))
                for func in feature.functions:
                    self.write_func_definition(f, func)
                f.write('\n\n')

            # extension load procs
            for ext in extensions[api]:
                if len(list(ext.functions)) == 0 or ext.name in written:
                    continue

                f.write('proc load_{}(load: proc) =\n'
                    .format(ext.name))
                if self.spec.NAME == 'gl':
                    f.write('  if not {}{}: return\n'.format(self.EXT_PREFIX,
                                                             ext.name))
                for func in ext.functions:
                    # even if they were in written we need to load it
                    self.write_func_definition(f, func)

                f.write('\n\n')

                written.add(ext.name)

            # findExtensions proc
            f.write('proc findExtensions{}() =\n'.format(api.upper()))
            if self.spec.NAME == 'gl':
                for ext in extensions[api]:
                    f.write('  {0}{1} = hasExt("{1}")\n'.format(self.EXT_PREFIX,
                                                                ext.name))
                else:
                    f.write('  discard\n')
            f.write('\n\n')

            # findCore proc
            f.write('proc findCore{}(glVersion: string) =\n'.format(api.upper()))
            self.loader.write_find_core(f)
            if self.spec.NAME == 'gl':
                for feature in features[api]:
                    f.write('  {}{} = (major == {num[0]} and minor >= {num[1]}) or'
                        ' major > {num[0]}\n'.format(self.EXT_PREFIX, feature.name,
                                                     num=feature.number))
            f.write('\n\n')

            # main loader proc
            loadername = 'Load' if self.LOAD_GL_PREFIX else 'load'
            f.write('proc {}{}{}*(load: proc): bool =\n'
                    .format(self.LOAD_GL_PREFIX, loadername, api.upper()))
            self.loader.write_begin_load(f)

            f.write('  findCore{}($glVersion)\n\n'.format(api.upper()))
            for feature in features[api]:
                f.write('  load_{}(load)\n'.format(feature.name))
            f.write('\n  findExtensions{}()\n\n'.format(api.upper()))
            for ext in extensions[api]:
                if len(list(ext.functions)) == 0:
                    continue
                f.write('  load_{}(load);\n'.format(ext.name))
            self.loader.write_end_load(f)
            f.write('\n')

    def write_func_definition(self, fobj, func):
        func_name = self.map_func_name(func)
        fobj.write('  {} = cast['.format(func_name))
        self.write_function_declaration(fobj, func)
        fobj.write('](load("{}"))\n'.format(func_name))

    def map_func_name(self, func):
        name = func.proto.name
        m = self.TYPE_DICT['SpecialFuncNames'][self.spec.NAME]
        return m[name] if name in m else name

    def generate_types(self, types):
        f = self._f_gl

        f.write('# Types\ntype\n')
        for ogl, d in sorted(self.TYPE_DICT[self.spec.NAME].items()):
            f.write('  {}* = {}\n'.format(ogl, d))
        self.TYPE_DICT['__other'][self.spec.NAME](self, f)
        f.write('\n')

    def generate_features(self, features):
        self.write_enums(features)
        self.write_funcs(features)

    def get_type_from_enum_name(self, name):
        return 'GLbitfield' if '_BIT' in name else 'GLenum'

    def write_enums(self, features):
        f = self._f_gl

        written = set()

        f.write('''
# Enums

proc `==`*(a, b: GLenum): bool {.borrow.}
proc `==`*(a, b: GLbitfield): bool {.borrow.}
proc `or`*(a, b: GLbitfield): GLbitfield {.borrow.}
proc hash*(x: GLenum): int = x.int

const
''')
        for v in sorted(self.TYPE_DICT['SpecialNumbers'][self.spec.NAME]):
            written.add(v[0])
            self.write_enum(f, *v)
        f.write('\n')

        for feature in features:
            for enum in feature.enums:
                if enum.group == 'SpecialNumbers':
                    written.add(enum.name)
                    continue
                if enum.name not in written:
                    type = self.get_type_from_enum_name(enum.name)
                    self.write_enum(f, enum.name, enum.value, type)
                written.add(enum.name)
        f.write('\n')

    def write_funcs(self, features):
        f = self._f_gl

        f.write('\n# Functions\nvar\n')
        if self.spec.NAME == 'gl':
            for feature in features:
                self.write_boolean(f, feature.name)
            f.write('\n')

        # TODO
        if self.spec.NAME in ('egl', 'wgl'):
            for feature in features:
                for func in feature.functions:
                    self.write_function_def(f, func) # TODO
                f.write('\n')
        else:
            self.write_functions(f, set(), set(), features)
        f.write('\n\n')

    # TODO
    def write_function_def(self, fobj, func):
        fobj.write('{} {}('.format(func.proto.ret.to_nim(), self.map_func_name(func)))
        fobj.write(', '.join(param.type.to_nim() for param in func.params))
        fobj.write(');\n')

    def generate_extensions(self, extensions, enums, functions):
        f = self._f_gl

        write = set()
        written = set(enum.name for enum in enums) | \
                  set(function.proto.name for function in functions)

        f.write('# Extensions\n')
        if extensions:
            f.write('var\n')

        for ext in extensions:
            if self.spec.NAME == 'gl' and not ext.name in written:
                self.write_boolean(f, ext.name)

            for enum in ext.enums:
                if not enum.name in written and not enum.group == 'SpecialNumbers':
                    type = (None if enum.group == 'TransformFeedbackTokenNV'
                                 else self.get_type_from_enum_name(enum.name))
                    self.write_enum(f, enum.name, enum.value, type)
                written.add(enum.name)
            written.add(ext.name)
            f.write('\n')

        self.write_functions(f, write, written, extensions)
        f.write('\n\n')


    def write_functions(self, f, write, written, extensions):
        for ext in extensions:
            for func in ext.functions:
                if not func.proto.name in written:
                    self.write_function_var(f, func)
                    write.add(func)
                written.add(func.proto.name)


    def write_function_var(self, fobj, func):
        fobj.write('  {}*: '.format(self.map_func_name(func)))
        self.write_function_declaration(fobj, func)
        fobj.write('\n')


    def write_function_declaration(self, fobj, func):
        fobj.write('proc ('.format(self.map_func_name(func)))
        fobj.write(', '.join('{}: {}'.format(self.to_nim_param_name(param.name),
                                             param.type.to_nim())
                   for param in func.params))
        fobj.write(')')

        ret = func.proto.ret.to_nim()
        if (ret != 'void'):
          fobj.write(': {}'.format(ret))

        fobj.write(' {.stdcall.}')

# TODO
#    def write_function_var(self, fobj, func):
#        fobj.write('  {} = cast[proc ('.format(func.proto.name))
#        fobj.write(', '.join('{}: {}'.format(self.to_nim_param_name(param.name),
#                                             param.type.to_nim())
#                   for param in func.params))
#        fobj.write(')')
#
#        ret = func.proto.ret.to_nim()
#        if (ret != 'void'):
#          fobj.write(': {}'.format(ret))
#
#        fobj.write(' {.cdecl.}]')
#        fobj.write(' (getProcAddress("{}"))\n'.format(func.proto.name))

    NIM_KEYWORDS = [   # as of Nim 1.0.2
      'addr', 'and', 'as', 'asm',
      'bind', 'block', 'break',
      'case', 'cast', 'concept', 'const', 'continue', 'converter',
      'defer', 'discard', 'distinct', 'div', 'do',
      'elif', 'else', 'end', 'enum', 'except', 'export',
      'finally', 'for', 'from', 'func',
      'if', 'import', 'in', 'include', 'interface', 'is', 'isnot', 'iterator',
      'let',
      'macro', 'method', 'mixin', 'mod',
      'nil', 'not', 'notin',
      'object', 'of', 'or', 'out',
      'proc', 'ptr',
      'raise', 'ref', 'return',
      'shl', 'shr', 'static',
      'template', 'try', 'tuple', 'type',
      'using',
      'var',
      'when', 'while',
      'xor',
      'yield'
    ]

    def to_nim_param_name(self, name):
        return '`{}`'.format(name) if name in self.NIM_KEYWORDS else name

    def make_path(self, name):
        path = os.path.join(self.path, self.MODULE.split('.')[-1],
                            name.split('.')[-1] + self.FILE_EXTENSION)
        makefiledir(path)
        return path

    def write_boolean(self, fobj, name):
        fobj.write('  {}{}*: bool\n'.format(self.EXT_PREFIX, name))

    def write_enum(self, fobj, name, value, type='GLenum'):
        fobj.write('  {}*'.format(self.map_enum_name(name)))
        if type:
          fobj.write(' = {1}.{0}'.format(type, value))
        else:
          fobj.write(' = {}'.format(value))
        fobj.write('\n')

    def map_enum_name(self, name):
        m = self.TYPE_DICT['SpecialEnumNames'][self.spec.NAME]
        return m[name] if name in m else name
