# -*- Mode: Python -*-
# GObject-Introspection - a framework for introspecting GObject libraries
# Copyright (C) 2008  Johan Dahlin
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#

from __future__ import with_statement
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import os
import subprocess
import tempfile

from .libtoolimporter import LibtoolImporter
from .message import Position
from .ccompiler import CCompiler

with LibtoolImporter(None, None):
    if 'UNINSTALLED_INTROSPECTION_SRCDIR' in os.environ:
        from _giscanner import SourceScanner as CSourceScanner
    else:
        from giscanner._giscanner import SourceScanner as CSourceScanner

HEADER_EXTS = ['.h', '.hpp', '.hxx']
SOURCE_EXTS = ['.c', '.cpp', '.cc', '.cxx']
ALL_EXTS = SOURCE_EXTS + HEADER_EXTS

(CSYMBOL_TYPE_INVALID,
 CSYMBOL_TYPE_ELLIPSIS,
 CSYMBOL_TYPE_CONST,
 CSYMBOL_TYPE_OBJECT,
 CSYMBOL_TYPE_FUNCTION,
 CSYMBOL_TYPE_STRUCT,
 CSYMBOL_TYPE_UNION,
 CSYMBOL_TYPE_ENUM,
 CSYMBOL_TYPE_TYPEDEF,
 CSYMBOL_TYPE_MEMBER) = range(10)

(CTYPE_INVALID,
 CTYPE_VOID,
 CTYPE_BASIC_TYPE,
 CTYPE_TYPEDEF,
 CTYPE_STRUCT,
 CTYPE_UNION,
 CTYPE_ENUM,
 CTYPE_POINTER,
 CTYPE_ARRAY,
 CTYPE_FUNCTION) = range(10)

STORAGE_CLASS_NONE = 0
STORAGE_CLASS_TYPEDEF = 1 << 1
STORAGE_CLASS_EXTERN = 1 << 2
STORAGE_CLASS_STATIC = 1 << 3
STORAGE_CLASS_AUTO = 1 << 4
STORAGE_CLASS_REGISTER = 1 << 5
STORAGE_CLASS_THREAD_LOCAL = 1 << 6

TYPE_QUALIFIER_NONE = 0
TYPE_QUALIFIER_CONST = 1 << 1
TYPE_QUALIFIER_RESTRICT = 1 << 2
TYPE_QUALIFIER_VOLATILE = 1 << 3
TYPE_QUALIFIER_EXTENSION = 1 << 4

FUNCTION_NONE = 0
FUNCTION_INLINE = 1 << 1

(UNARY_ADDRESS_OF,
 UNARY_POINTER_INDIRECTION,
 UNARY_PLUS,
 UNARY_MINUS,
 UNARY_BITWISE_COMPLEMENT,
 UNARY_LOGICAL_NEGATION) = range(6)


def symbol_type_name(symbol_type):
    return {
        CSYMBOL_TYPE_INVALID: 'invalid',
        CSYMBOL_TYPE_ELLIPSIS: 'ellipsis',
        CSYMBOL_TYPE_CONST: 'const',
        CSYMBOL_TYPE_OBJECT: 'object',
        CSYMBOL_TYPE_FUNCTION: 'function',
        CSYMBOL_TYPE_STRUCT: 'struct',
        CSYMBOL_TYPE_UNION: 'union',
        CSYMBOL_TYPE_ENUM: 'enum',
        CSYMBOL_TYPE_TYPEDEF: 'typedef',
        CSYMBOL_TYPE_MEMBER: 'member'}.get(symbol_type)


def ctype_name(ctype):
    return {
        CTYPE_INVALID: 'invalid',
        CTYPE_VOID: 'void',
        CTYPE_BASIC_TYPE: 'basic',
        CTYPE_TYPEDEF: 'typedef',
        CTYPE_STRUCT: 'struct',
        CTYPE_UNION: 'union',
        CTYPE_ENUM: 'enum',
        CTYPE_POINTER: 'pointer',
        CTYPE_ARRAY: 'array',
        CTYPE_FUNCTION: 'function'}.get(ctype)


class SourceType(object):
    __members__ = ['type', 'base_type', 'name', 'type_qualifier',
                   'child_list', 'is_bitfield']

    def __init__(self, scanner, stype):
        self._scanner = scanner
        self._stype = stype

    def __repr__(self):
        return "<%s type='%s' name='%s'>" % (
            self.__class__.__name__,
            ctype_name(self.type),
            self.name)

    @property
    def type(self):
        return self._stype.type

    @property
    def base_type(self):
        if self._stype.base_type is not None:
            return SourceType(self._scanner, self._stype.base_type)

    @property
    def name(self):
        return self._stype.name

    @property
    def type_qualifier(self):
        return self._stype.type_qualifier

    @property
    def child_list(self):
        for symbol in self._stype.child_list:
            if symbol is None:
                continue
            yield SourceSymbol(self._scanner, symbol)

    @property
    def is_bitfield(self):
        return self._stype.is_bitfield


class SourceSymbol(object):
    __members__ = ['const_int', 'const_double', 'const_string', 'const_boolean',
                   'ident', 'type', 'base_type']

    def __init__(self, scanner, symbol):
        self._scanner = scanner
        self._symbol = symbol

    def __repr__(self):
        src = self.source_filename
        if src:
            line = self.line
            if line:
                src += ":'%s'" % (line, )
        return "<%s type='%s' ident='%s' src='%s'>" % (
            self.__class__.__name__,
            symbol_type_name(self.type),
            self.ident,
            src)

    @property
    def const_int(self):
        return self._symbol.const_int

    @property
    def const_double(self):
        return self._symbol.const_double

    @property
    def const_string(self):
        return self._symbol.const_string

    @property
    def const_boolean(self):
        return self._symbol.const_boolean

    @property
    def ident(self):
        return self._symbol.ident

    @property
    def type(self):
        return self._symbol.type

    @property
    def base_type(self):
        if self._symbol.base_type is not None:
            return SourceType(self._scanner, self._symbol.base_type)

    @property
    def source_filename(self):
        return self._symbol.source_filename

    @property
    def line(self):
        return self._symbol.line

    @property
    def private(self):
        return self._symbol.private

    @property
    def position(self):
        return Position(self._symbol.source_filename,
                        self._symbol.line)


class SourceScanner(object):

    def __init__(self):
        self._scanner = CSourceScanner()
        self._filenames = []
        self._cpp_options = []

    # Public API

    def set_cpp_options(self, includes, defines, undefines, cflags=[]):
        self._cpp_options.extend(cflags)
        for prefix, args in [('-I', [os.path.realpath(f) for f in includes]),
                             ('-D', defines),
                             ('-U', undefines)]:
            for arg in (args or []):
                opt = prefix + arg
                if opt not in self._cpp_options:
                    self._cpp_options.append(opt)

    def parse_files(self, filenames):
        for filename in filenames:
            # self._scanner expects file names to be canonicalized and symlinks to be resolved
            filename = os.path.realpath(filename)
            self._scanner.append_filename(filename)
            self._filenames.append(filename)

        headers = []
        for filename in self._filenames:
            if os.path.splitext(filename)[1] in SOURCE_EXTS:
                self._scanner.lex_filename(filename)
            else:
                headers.append(filename)

        self._parse(headers)

    def parse_macros(self, filenames):
        self._scanner.set_macro_scan(True)
        # self._scanner expects file names to be canonicalized and symlinks to be resolved
        self._scanner.parse_macros([os.path.realpath(f) for f in filenames])
        self._scanner.set_macro_scan(False)

    def get_symbols(self):
        for symbol in self._scanner.get_symbols():
            yield SourceSymbol(self._scanner, symbol)

    def get_comments(self):
        return self._scanner.get_comments()

    def dump(self):
        print('-' * 30)
        for symbol in self._scanner.get_symbols():
            print(symbol.ident, symbol.base_type.name, symbol.type)

    # Private

    def _parse(self, filenames):
        if not filenames:
            return

        defines = ['__GI_SCANNER__']
        undefs = []

        cc = CCompiler()

        tmp_fd_cpp, tmp_name_cpp = tempfile.mkstemp(prefix='g-ir-cpp-',
                                                    suffix='.c',
                                                    dir=os.getcwd())
        with os.fdopen(tmp_fd_cpp, 'wb') as fp_cpp:
            self._write_preprocess_src(fp_cpp, defines, undefs, filenames)

        tmpfile_basename = os.path.basename(os.path.splitext(tmp_name_cpp)[0])

        # Output file name of the preprocessor, only really used on non-MSVC,
        # so we want the name to match the output file name of the MSVC preprocessor
        tmpfile_output = tmpfile_basename + '.i'

        cc.preprocess(tmp_name_cpp,
                      tmpfile_output,
                      self._cpp_options)

        os.unlink(tmp_name_cpp)
        fp = open(tmpfile_output, 'r')

        self._scanner.parse_file(fp.fileno())
        fp.close()
        os.unlink(tmpfile_output)

    def _write_preprocess_src(self, fp, defines, undefs, filenames):
        # Write to the temp file for feeding into the preprocessor
        for define in defines:
            fp.write(('#ifndef %s\n' % (define, )).encode())
            fp.write(('# define %s\n' % (define, )).encode())
            fp.write('#endif\n'.encode())
        for undef in undefs:
            fp.write(('#undef %s\n' % (undef, )).encode())
        for filename in filenames:
            fp.write(('#include <%s>\n' % (filename, )).encode())
