# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
    "name": "Class Viewer",
    "author": "Mackraken", "batFinger"
    "version": (0, 1, 3),
    "blender": (2, 80, 0),
    "location": "Text Editor > Toolbar, Text Editor > Right Click",
    "warning": "",
    "description": "List classes and definitions of a text block",
    "doc_url": "https://sites.google.com/site/aleonserra/home/scripts/class-viewer",
    "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
    "category": "Development"}


import bpy
from bpy.types import (
        Operator,
        Menu,
        )
from bpy.props import IntProperty

# lists all defs, comment or classes
# space = current text editor
# sort = sort result
# tipo = kind of struct to look for
# escape = character right next to the class or def name
# note: GPL license block is skipped from the comment entries now


def getfunc(space, sort, tipo="def ", escape=""):
    defs, license_clean, return_lists = [], [], []

    if space.type == "TEXT_EDITOR" and space.text is not None:
        txt = space.text
        line_block_start, line_block_end = None, None

        for i, l in enumerate(txt.lines):
            try:
                line = l.body
            except:
                line = ""
            if tipo == "# ":
                linex, line_c = "", ""
                if tipo in line:
                    # find the license block
                    block_start = line.find("BEGIN GPL LICENSE BLOCK")
                    if block_start != -1:
                        line_block_start = i + 1

                    block_end = line.find("END GPL LICENSE BLOCK")
                    if block_end != -1:
                        line_block_end = i + 1

                    end = line.find(escape)
                    # use line partition, get the last tuple element as it is the comment
                    line_c = line.partition("#")[2]
                    # strip the spaces from the left if any and
                    # cut the comment at the 60 characters length max
                    linex = line_c[:60].lstrip() if len(line_c) > 60 else line_c.lstrip()
                    if end == 0:
                        func = linex
                    else:
                        func = linex.rstrip(escape)

                    defs.append([func, i + 1])

            elif line[0: len(tipo)] == tipo:
                end = line.find(escape)
                if end == 0:
                    func = line[len(tipo)::]
                else:
                    func = line[len(tipo):end]
                defs.append([func, i + 1])

        if line_block_start and line_block_end:
            # note : the slice should keep the first line of the license block
            license_clean = defs[line_block_start: line_block_end]

        return_lists = [line for line in defs if line not in license_clean] if license_clean else defs
        if sort:
            return_lists.sort()

    return return_lists


def draw_menus(layout, space, tipo, escape):

    items = getfunc(space, 1, tipo, escape)
    if not items:
        layout.label(text="Not found", icon="INFO")
        return

    for it in items:
        layout.operator("text.jumptoline", text="{}".format(it[0])).line = it[1]


class BaseCheckPoll():
    @classmethod
    def poll(cls, context):
        if context.area.spaces[0].type != "TEXT_EDITOR":
            return False
        else:
            return context.area.spaces[0].text is not None


class TEXT_OT_Jumptoline(Operator, BaseCheckPoll):
    bl_label = "Jump"
    bl_idname = "text.jumptoline"
    bl_description = "Jump to line containing the text"
    __doc__ = "Jump to line containing the passed line integer. Used by the Class Viewer add-on"

    line: IntProperty(default=-1)

    def execute(self, context):
        if self.line == -1:
            self.report({'WARNING'}, "No valid line found. Operation Cancelled")
            return {'CANCELLED'}

        bpy.ops.text.jump(line=self.line)
        self.report({'INFO'}, "Jump to line: {}".format(self.line))

        return {'FINISHED'}


class CommentsMenu(Menu, BaseCheckPoll):
    bl_idname = "OBJECT_MT_select_comments"
    bl_label = "Comments"

    def draw(self, context):
        layout = self.layout
        space = context.area.spaces[0]
        draw_menus(layout, space, "# ", "\n")


class DefsMenu(Menu, BaseCheckPoll):
    bl_idname = "OBJECT_MT_select_defs"
    bl_label = "Definitions"

    def draw(self, context):
        layout = self.layout
        space = context.area.spaces[0]
        draw_menus(layout, space, "def ", "(")


class ClassesMenu(Menu, BaseCheckPoll):
    bl_idname = "OBJECT_MT_select_classes"
    bl_label = "Classes"

    def draw(self, context):
        layout = self.layout
        space = context.area.spaces[0]
        draw_menus(layout, space, "class ", "(")


def menu_development_class_view(self, context):
    self.layout.separator()

    self.layout.menu("OBJECT_MT_select_comments", icon='SHORTDISPLAY')
    self.layout.menu("OBJECT_MT_select_defs", icon='FILE_TEXT')
    self.layout.menu("OBJECT_MT_select_classes", icon='SCRIPTPLUGINS')


classes = (
    TEXT_OT_Jumptoline,
    ClassesMenu,
    DefsMenu,
    CommentsMenu,
    )


def register():
    for cls in classes:
        bpy.utils.register_class(cls)

    bpy.types.TEXT_MT_context_menu.append(menu_development_class_view)


def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)

    bpy.types.TEXT_MT_context_menu.remove(menu_development_class_view)


if __name__ == "__main__":
    register()
