# coding=utf-8

"""
OpenSpace

Copyright (c) 2014-2020

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


This script traverses the file tree of OpenSpace and will check all files' include
guards for correctness. At the moment this includes:
 * Correctness (file has a #ifndef. #define, and #endif lines)
 * Equality (using the same name for the #ifdef and #define)
 * Styling 
   * no empty line between #ifndef and #define lines
   * Empty lines before and after #ifndef #define block
   * Files end with an empty line
   * Copyright header is correctly indented
   * Include guard correctly uses the filename
   * Include guard is all upper case
 * Correct usage of the name in the final comment of the file
 * Correct year of copyright notice
 * Naming convention
   * OpenSpace include guards start with OPENSPACE, Ghoul with GHOUL,
     module includes have the module name in it
   * The correct submodule is used
 * Checking for duplicates between all files
 * Checking that no file includes glm header directly
 * Checking whether any files starts with the UTF-8 Byte-order mark
 * Checking whether a file as empty-only lines
 * Checking whether the default assert macros are used anywhere instead of the
   ghoul_assert macro
 * Checking whether there are TABs in the file

If this script is executed from the base directory of OpenSpace, no arguments need to
be passed, otherwise the first and only argument has to point to the base directory.
Thus, the default value of the first argument is '.'
"""

import fnmatch
import glob
import os
import re
import sys

current_year = '2020'
is_strict_mode = False
is_silent_mode = False

def get_ifndef_symbol(lines):
    index = [i for i,s in enumerate(lines) if '#ifndef ' in s]

    if len(index) == 0:
        return '', -1

    result = re.search('#ifndef (.*)\n', lines[index[0]])
    return result.group(1), index[0]



def get_define_symbol(lines):
    index = [i for i,s in enumerate(lines) if '#define ' in s]

    if len(index) == 0:
        return '', -1

    result = re.search('#define (.*)\n', lines[index[0]])
    return result.group(1), index[0]



def check_correctness(lines):
    ifndef_symbol, line_number = get_ifndef_symbol(lines)
    if line_number == -1:
        return 'No #ifndef in file'

    define_symbol, line_number = get_define_symbol(lines)
    if (line_number == -1):
        return 'No #define in file'

    index = [i for i,s in enumerate(lines) if '#endif' in s]
    if len(index) == 0:
        return 'No #endif in file'

    return ''



def check_equality(lines):
    ifndef, _ = get_ifndef_symbol(lines)
    define, _ = get_define_symbol(lines)

    if ifndef == define:
        return ''
    else:
        return ifndef + ' ' + define



def check_styling(lines):
    ifndef_symbol, ifndef_line = get_ifndef_symbol(lines)
    _, define_line = get_define_symbol(lines)

    if abs(ifndef_line - define_line) != 1:
        return '#ifndef and #define lines are not subsequent'

    if lines[ifndef_line - 1].strip() != '':
        return 'Preceding line is not empty'

    if lines[define_line + 1].strip() != '':
        return 'Following line is not empty'

    if not lines[-1][-1] in ['\n', '\r']:
        return 'Last line must end with a newline'

    for l in lines[2:23]:
        if l[0] != ' ':
            return 'Copyright header must be indented'

    if ifndef_symbol != ifndef_symbol.upper():
        return 'Include guard is not all upper case'

    return ''



def check_styling_filename(lines, filename):
    ifndef_symbol, _ = get_ifndef_symbol(lines)
    file = os.path.splitext(os.path.basename(filename))[0].upper()

    if not (file in ifndef_symbol or file in ifndef_symbol.replace('_', '')):
        return 'Malformed include guard: ' + ifndef_symbol + ' || ' + file



def check_comment(lines):
    ifndef_symbol, _ = get_ifndef_symbol(lines)

    index = [i for i,s in enumerate(lines) if '#endif' in s]
    endif_line = lines[index[-1]].strip()

    if endif_line != '#endif // ' + ifndef_symbol:
        print(ifndef_symbol)
        print(endif_line)
        return '#endif line is not correctly formatted'
    else:
        return ''



def check_copyright(lines):
    index = [i for i,s in enumerate(lines[0:23]) if 'Copyright' in s]

    if len(index) == 0:
        return 'No copyright header found'

    beginning_string = ' * Copyright (c) 2012-'
    #  * Copyright (c) 2014-

    year = lines[index[0]][len(beginning_string) : len(beginning_string) + 4]

    if lines[index[0] + 1][0] != ' ':
        return 'Copyright header is not correctly indented'

    if year != current_year:
        return 'Out of date copyright notice ' + year + ' || ' + current_year


    return ''



def check_byte_order_mark_character(lines):
    c = lines[0][0]
    if c == 'ï':
        return 'File contains UTF-8 byte mark order character'

    return ''



def check_naming_convention_component(lines, component):
    ifndef_symbol, _ = get_ifndef_symbol(lines)

    component_part = ifndef_symbol[2:2 + len(component)]

    if component_part != component.upper():
        return '#ifndef naming convention broken: ' + ifndef_symbol + ' || ' + component.upper() 
    else:
        return ''



def check_naming_convention_subcomponent(lines, component, file):
    ifndef_symbol, _ = get_ifndef_symbol(lines)

    if component == "ghoul" or component == "openspace_core":
        return

    subcomponent_part = ifndef_symbol[2 + len(component) + 1 :]
    subcomponent_part = subcomponent_part[: subcomponent_part.find('_')]

    path_part = file.split('/')[1]
    second_path_part = file.split('/')[2]


    if (path_part.upper() != subcomponent_part) and (second_path_part.upper() != subcomponent_part):
        return 'Subcomponent naming convention broken: ' + ifndef_symbol
    else:
        return ''



def check_duplicates(lines, previousSymbols):
    ifndef_symbol, _ = get_ifndef_symbol(lines)

    if ifndef_symbol in previousSymbols:
        return False, ifndef_symbol
    else:
        return True, ifndef_symbol



def check_glm_header(lines, file):
    Allowed_Files = [
        'ghoul/glm.h'
    ]

    for f in Allowed_Files:
        if f in file.replace('\\', '/'):
            return ''

    index = [i for i,s in enumerate(lines)
                if '#include <glm/glm.hpp>' in s or 
                '#include "glm/glm.hpp>"' in s]

    if len(index) > 0:
        return 'File used wrong glm include. Use "#include <ghoul/glm.h>" instead'
    else:
        return ''



def check_core_dependency(lines, component):
    if component != "openspace_core":
        return ''

    index = [i for i,s in enumerate(lines) if 'OPENSPACE_MODULE_' in s]

    if len(index) > 0:
        return lines[index[0]][:-1]
    else:
        return ''



def check_using_namespace(lines):
    index = [i for i,s in enumerate(lines) if "using namespace" in s.strip()]

    if len(index) > 0:
        return lines[index[0]]
    else:
        return ''



def check_end_of_line(lines):
    if lines[-1][-1] != '\n':
        return lines[-1][-1]
    else:
        return ''



def check_empty_only_line(lines):
    # Disable this check in non-strict mode
    if not is_strict_mode:
        return ''

    index = [i + 1 for i, s in enumerate(lines) if s.translate({ord(c): None for c in '\n\r'}).isspace()]
    if len(index) > 0:
        return index
    else:
        return ''



def check_assert_usage(lines):
    # _assert checks for both ghoul_assert and static_assert, which are both reasonable
    index = [i + 1 for i,s in enumerate(lines) if ('assert(' in s and not '_assert(' in s) and s.strip()[0:2] != '//']
    if len(index) > 0:
        return index
    else:
        return '';



def check_line_length(lines):
    # Disable this check in non-strict mode
    if not is_strict_mode:
        return ''

    index = [i + 1 for i, s in enumerate(lines) if len(s) > (90 + 1)]
    if len(index) > 0:
        return index
    else:
        return ''



def check_empty_character_at_end(lines):
    # Disable this check in non-strict mode
    if not is_strict_mode:
        return ''

    index = [i + 1 for i, s in enumerate(lines) if len(s) > 1 and s[-2] == ' ' and not s.strip() == '']
    if len(index) > 0:
        return index
    else:
        return ''


def check_for_tab(lines):
    index = [i + 1 for i, s in enumerate(lines) if '\t' in s]
    if len(index) > 0:
        return index
    else:
        return ''

previousSymbols  = {}
def check_header_file(file, component):
    with open(file, 'r+', encoding="utf8") as f:
        lines = f.readlines()

        correctness = check_correctness(lines)
        if correctness:
            print(file, '\t', 'Correctness check failed', '\t', correctness)
            return

        equality = check_equality(lines)
        if equality:
            print(file, '\t', 'Equality check failed', '\t', equality)
            return

        styling = check_styling(lines)
        if styling:
            print(file, '\t',  'Styling check failed', '\t', styling)
            return

        styling_filename = check_styling_filename(lines, file)
        if styling_filename:
            print(file, '\t',  'Filename styling check failed', '\t', styling_filename)
            return

        comment = check_comment(lines)
        if comment:
            print(file, '\t',  'Comment check failed', '\t', comment)
            return

        copyright = check_copyright(lines)
        if copyright:
            print(file, '\t', 'Copyright check failed', '\t', copyright)
            return

        naming_component = check_naming_convention_component(lines, component)
        if naming_component:
            print(file, '\t', 'Naming convention broken', '\t', naming_component)
            return

        naming_subcomponent = check_naming_convention_subcomponent(lines, component, file)
        if naming_subcomponent:
            print(file, '\t', 'Naming convention broken', '\t', naming_subcomponent)
            return

        end_of_line = check_end_of_line(lines)
        if end_of_line:
            print(file, '\t', 'Last line does not contain a newline character: ', end_of_line)
            return

        duplicates, symbol = check_duplicates(lines, previousSymbols)
        if not duplicates:
            print(file, '\t', 'Duplicate include guard', symbol, 'first in', previousSymbols[symbol])
            return
        else:
            previousSymbols[symbol] = file

        header = check_glm_header(lines, file)
        if header:
            print(file, '\t', 'Illegal glm header include', header)
            return

        core_dependency = check_core_dependency(lines, component)
        if core_dependency:
            print(file, '\t', 'Wrong dependency (core depends on module)', core_dependency)

        if (not 'ghoul_gl.h' in file):
            # ghoul_gl.h is allowed to use 'using namespace' to pull the gl namespace in
            using_namespaces = check_using_namespace(lines)
            if using_namespaces:
                print(file, '\t', 'Using namespace found in header file')

        bom = check_byte_order_mark_character(lines)
        if bom:
            print(file, '\t', 'Byte order mark failed:', bom)

        empty_only_lines = check_empty_only_line(lines)
        if empty_only_lines:
            print(file, '\t', 'Empty only line: ', empty_only_lines)

        line_length = check_line_length(lines)
        if line_length:
            print(file, '\t', 'Line length exceeded: ', line_length)

        empty_character_at_end = check_empty_character_at_end(lines)
        if empty_character_at_end:
            print(file, '\t', 'Empty character at end: ', empty_character_at_end)

        assert_usage = check_assert_usage(lines)
        if assert_usage:
            print(file, '\t', 'Wrong assert usage: ', assert_usage)

        tabs = check_for_tab(lines)
        if tabs:
            print(file, '\t', 'TABs found: ', tabs)



def check_inline_file(file, component):
    with open(file, 'r+', encoding="utf8") as f:
        lines = f.readlines()

        copyright = check_copyright(lines)
        if copyright:
            print(file, '\t', 'Copyright check failed', '\t', copyright)

        header = check_glm_header(lines, file)
        if header:
            print(file, '\t', 'Illegal glm header include', header)

        core_dependency = check_core_dependency(lines, component)
        if core_dependency:
            print(file, '\t', 'Wrong dependency (core depends on module)', core_dependency)

        end_of_line = check_end_of_line(lines)
        if end_of_line:
            print(file, '\t', 'Last line does not contain a newline character: ', end_of_line)
            return

        bom = check_byte_order_mark_character(lines)
        if bom:
            print(file, '\t', 'Byte order mark failed:', bom)

        empty_only_lines = check_empty_only_line(lines)
        if empty_only_lines:
            print(file, '\t', 'Empty only line: ', empty_only_lines)

        line_length = check_line_length(lines)
        if line_length:
            print(file, '\t', 'Line length exceeded: ', line_length)

        if (not '_doc.inl' in file):
            # The _doc.inl files are allowed to use using namespace as they are inclued
            # from the cpp files and thus don't leak it
            using_namespaces = check_using_namespace(lines)
            if using_namespaces:
                print(file, '\t', 'Using namespace found in inline file')

        line_length = check_line_length(lines)
        if line_length:
            print(file, '\t', 'Line length exceeded: ', line_length)

        empty_character_at_end = check_empty_character_at_end(lines)
        if empty_character_at_end:
            print(file, '\t', 'Empty character at end: ', empty_character_at_end)

        assert_usage = check_assert_usage(lines)
        if assert_usage:
            print(file, '\t', 'Wrong assert usage: ', assert_usage)

        tabs = check_for_tab(lines)
        if tabs:
            print(file, '\t', 'TABs found: ', tabs)




def check_source_file(file, component):
    with open(file, 'r+', encoding="utf8") as f:
        lines = f.readlines()

        header = check_glm_header(lines, file)
        if header:
            print(file, '\t',  'Illegal glm header include', header)

        core_dependency = check_core_dependency(lines, component)
        if core_dependency:
            print(file, '\t' 'Wrong core dependency', core_dependency)

        end_of_line = check_end_of_line(lines)
        if end_of_line:
            print(file, '\t', 'Last line does not contain a newline character: ', end_of_line)
            return

        copyright = check_copyright(lines)
        if copyright:
            print(file, '\t', 'Copyright check failed', '\t', copyright)

        bom = check_byte_order_mark_character(lines)
        if bom:
            print(file, '\t', 'Byte order mark failed:', bom)

        empty_only_lines = check_empty_only_line(lines)
        if empty_only_lines:
            print(file, '\t', 'Empty only line: ', empty_only_lines)

        line_length = check_line_length(lines)
        if line_length:
            print(file, '\t', 'Line length exceeded: ', line_length)

        empty_character_at_end = check_empty_character_at_end(lines)
        if empty_character_at_end:
            print(file, '\t', 'Empty character at end: ', empty_character_at_end)

        assert_usage = check_assert_usage(lines)
        if assert_usage:
            print(file, '\t', 'Wrong assert usage: ', assert_usage)

        tabs = check_for_tab(lines)
        if tabs:
            print(file, '\t', 'TABs found: ', tabs)




def check_files(positiveList, negativeList, component, check_function):
    files = []
    for p in positiveList:
        f = glob.glob(p, recursive=True)
        f = [fi.replace('\\', '/') for fi in f]
        files.extend(f)

    negativeFiles = []
    for n in negativeList:
        f = glob.glob(n, recursive=True)
        f = [fi.replace('\\', '/') for fi in f]
        negativeFiles.extend(f)

    filtered_files = [f for f in files if f not in negativeFiles]

    for file in filtered_files:
        check_function(file, component)





basePath = './'
if len(sys.argv) > 1:
    if sys.argv[1] != "strict":
        basePath = sys.argv[1] + '/'

for a in sys.argv:
    if a == "strict":
        is_strict_mode = True
    if a == "silent":
        is_silent_mode = True


# Check header files
if not is_silent_mode:
    print("Checking header files")
    print("=====================")

check_files(
    [basePath + 'include/**/*.h'],
    [],
    'openspace_core',
    check_header_file
)
check_files(
    [basePath + 'apps/**/*.h'],
    [basePath + 'apps/**/ext/**/*.h'],
    'openspace_app',
    check_header_file
)
check_files(
    [basePath + 'modules/**/*.h'],
    [
        basePath + 'modules/**/ext/**/*.h',
        basePath + 'modules/**/node_modules/**/*.h',
        basePath + 'modules/webbrowser/resource.h'
    ],
    'openspace_module',
    check_header_file
)
check_files(
    [basePath + 'ext/ghoul/include/**/*.h'],
    [],
    'ghoul',
    check_header_file
)

if not is_silent_mode:
    print("")
    print("Checking inline files")
    print("=====================")

check_files(
    [basePath + 'include/**/*.inl'],
    [],
    'openspace_core',
    check_inline_file
)
check_files(
    [basePath + 'src/**/*.inl'],
    [],
    'openspace_core',
    check_inline_file
)
check_files(
    [basePath + 'apps/**/*.inl'],
    [basePath + 'apps/**/ext/**/*.inl'],
    'openspace_app',
    check_inline_file
)
check_files(
    [basePath + 'modules/**/*.inl'],
    [basePath + 'modules/**/ext/**/*.h'],
    'openspace_module',
    check_inline_file
)
check_files(
    [basePath + 'ext/ghoul/include/**/*.inl'],
    [],
    'ghoul',
    check_inline_file
)

if not is_silent_mode:
    print("")
    print("Checking source files")
    print("=====================")

check_files(
    [basePath + 'src/**/*.cpp'],
    [],
    'openspace_core',
    check_source_file
)
check_files(
    [basePath + 'apps/**/*.cpp'],
    [basePath + 'apps/**/ext/**/*.cpp'],
    'openspace_app',
    check_source_file
)
check_files(
    [basePath + 'modules/**/*.cpp'],
    [basePath + 'modules/**/ext/**/*.cpp', basePath + 'modules/**/node_modules/**/*.cpp'],
    'openspace_module',
    check_source_file
)
check_files(
    [basePath + 'ext/ghoul/src/**/*.cpp'],
    [],
    'ghoul',
    check_source_file
)
