from ursina import Entity, camera, Text, Vec2, mouse, color, floor, clamp, time, held_keys, destroy
import pyperclip

from ursina.string_utilities import multireplace
# from tree_view import TreeView


class TextField(Entity):
    def __init__(self, max_lines=64, line_height=1.1, character_limit=None, **kwargs):
        super().__init__(parent=camera.ui, x=-.5, y=.4, ignore_paused=True)

        self.font = 'VeraMono.ttf'
        self.line_height = line_height
        self.max_lines = max_lines
        self.character_limit = character_limit

        self.scroll_parent = Entity(parent=self)
        self.text_entity = Text(parent=self.scroll_parent, start_tag='☾', end_tag='☽', font=self.font, text='', line_height=self.line_height, origin=(-.5, .5))
        self.line_numbers = Text(parent=self.scroll_parent, font=self.font, line_height=line_height, text='0', origin=(.5,.5), x=-.04, color=color.gray, enabled=False)
        self.character_width = Text.get_width('a', font=self.font)
        self.cursor_parent = Entity(parent=self.scroll_parent, scale=(self.character_width, -1*Text.size*self.line_height))
        self.cursor = Entity(name='text_field_cursor', parent=self.cursor_parent, model='cube', color=color.cyan, origin=(-.5, -.5), scale=(.1, 1, 0), enabled=False)
        self.cursor.blink(duration=1.2, loop=True)
        self.bg = Entity(name='text_field_bg', parent=self, model='quad', double_sided=True, color=color.dark_gray, origin=(-.5,.5), z=0.005, scale=(120, Text.size*self.max_lines*self.line_height), collider='box', visible=True)

        self.selection = [Vec2(0,0), Vec2(0,0)]
        self.selection_parent = Entity(name='text_field_selection_parent', parent=self.cursor_parent, scale=(1,1,0))
        self.register_mouse_input = False
        self.world_space_mouse = False

        self.triple_click_delay = 0.3
        self._last_double_click = 0
        self.scroll = 0
        self.scroll_amount = 2
        self._prev_scroll = self.scroll

        self.active = True
        self.highlight_color = color.hsv(120,1,1,.1)
        self.text = ''
        self.delimiters = ' .,!?;:(){}[]<>\'\"@#$%^&*+=-\\|/`~'
        self.replacements = dict()
        self.on_undo = []
        self.on_redo = []
        self.on_value_changed = None

        self.shortcuts = {
            'newline':          ('enter', 'enter hold'),
            'erase':            ('backspace', 'backspace hold'),
            'erase_word':       ('ctrl+backspace', 'ctrl+backspace hold'),
            'delete_line':      ('ctrl+shift+k',),
            'duplicate_line':   ('ctrl+shift+d',),
            'undo':             ('ctrl+z', 'ctrl+z hold'),
            'redo':             ('ctrl+y', 'ctrl+y hold', 'ctrl+shift+z', 'ctrl+shift+z hold'),
            # 'save':             ('ctrl+s',),
            # 'save_as':          ('ctrl+shift+s',),
            'indent':           ('tab',),
            'dedent':           ('shift+tab',),
            'move_line_down':   ('ctrl+down arrow', 'ctrl+down arrow hold'),
            'move_line_up':     ('ctrl+up arrow', 'ctrl+up arrow hold'),
            'scroll_up':        ('scroll up',),
            'scroll_down':      ('scroll down',),
            'cut':              ('ctrl+x',),
            'copy':             ('ctrl+c',),
            'paste':            ('ctrl+v',),
            'select_all':       ('ctrl+a',),
            'select_word':      ('double click',),
            'select_line':      ('triple click',),
            'scroll_to_bottom': ('shift+alt+e',),
            # 'toggle_comment':   ('ctrl+alt+c',),
            # 'find':             ('ctrl+f',),
            'move_operations' : {
                'move_left':                ('left arrow', 'left arrow hold', 'shift+left arrow', 'shift+left arrow hold'),
                'move_right':               ('right arrow', 'right arrow hold', 'shift+right arrow', 'shift+right arrow hold'),
                'move_up':                  ('up arrow', 'up arrow hold', 'shift+up arrow', 'shift+up arrow hold'),
                'move_down':                ('down arrow', 'down arrow hold', 'shift+down arrow', 'shift+down arrow hold'),
                'move_to_end_of_word' :     ('ctrl+right arrow', 'ctrl+right arrow hold', 'ctrl+shift+right arrow', 'ctrl+shift+right arrow hold'),
                'move_to_start_of_word' :   ('ctrl+left arrow', 'ctrl+left arrow hold', 'ctrl+shift+left arrow', 'ctrl+shift+left arrow hold'),
            },

            # 'select_word_left': ('ctrl+shift+left arrow', 'ctrl+shift+left arrow hold'),
            # 'select_word_right': ('ctrl+shift+right arrow', 'ctrl+shift+right arrow hold'),
        }
        def middle_click_input(key):
            if self.register_mouse_input:
                if key == 'middle mouse down':
                    self.middle_click_scroller.start_y = mouse.y
                    self._original_scroll_amount = self.scroll_amount
                elif key == 'middle mouse up' and hasattr(self, '_original_scroll_amount'):
                    self.middle_click_scroller.start_y = None
                    self.scroll_amount = self._original_scroll_amount

        def middle_click_update():
            if not mouse.middle or self.middle_click_scroller.start_y == None:
                return
            self.middle_click_scroller.t += time.dt
            if self.middle_click_scroller.t < self.middle_click_scroller.update_rate:
                return

            # self.middle_click_scroller.update_rate = 1-mouse.delta_drag[0]
            # print('-aaaaaaaaaaa', 1-mouse.delta_drag[0])
            dist = abs(mouse.y - self.middle_click_scroller.start_y)
            self.scroll_amount = 1
            if dist > .1:
                self.scroll_amount = 4
            elif dist > .2:
                self.scroll_amount = 6
            # elif dist > .4:
            #     self.scroll_amount = 12

            self.middle_click_scroller.t = 0

            if self.register_mouse_input:
                if mouse.y < self.middle_click_scroller.start_y:
                    self.input('scroll down')
                if mouse.y > self.middle_click_scroller.start_y:
                    self.input('scroll up')

        self.middle_click_scroller = Entity(parent=self, start_y=None, input=middle_click_input, update=middle_click_update, t=0, update_rate=.05)


        for key, value in kwargs.items():
            setattr(self, key, value)

        self._prev_text = ''



    @property
    def active(self):
        return self._active

    @active.setter
    def active(self, value):
        self._active = value
        self.cursor.enabled = value

        if not value:
            self.selection = [Vec2(0,0), Vec2(0,0)]
            self.draw_selection()


    def add_text(self, s, move_cursor=True, rerender=True):
        if self.character_limit and len(self.text) >= self.character_limit:
            return

        x, y = int(self.cursor.x), int(self.cursor.y)

        lines = self.text.split('\n')
        l = lines[y]
        lines[y] =  l[:x] + s + l[x:]
        self._append_undo(self.text, y, x)
        self.text = '\n'.join(lines)

        if move_cursor:
            self.cursor.x += len(s)

        if rerender:
            self.render()

    def _append_undo(self, text, y, x, clear_redo = True):
        if clear_redo:
            self.on_redo.clear()
        self.on_undo.append((text, y, x))


    def move_line(self, line_index, delta, move_cursor=True):
        x, y = int(self.cursor.x), int(self.cursor.y)

        start_y, end_y = self.cursor.Y, self.cursor.Y

        if self.selection != [Vec2(0,0), Vec2(0,0)]:
            start_y = int(self.selection[0].y)
            end_y = int(self.selection[1].y)

        lines = self.text.split('\n')
        # lines[line_index], lines[line_index+delta] = lines[line_index+delta], lines[line_index]
        # start = []
        # if start_y-1 > 0:

        #     start = lines[:int(start_y-1)]

        middle = lines[start_y:end_y+1]
        del lines[start_y:end_y+1]
        for l in middle[::delta]:
            lines.insert(start_y+delta, l)
        # end = lines[start_y+1:]

        # [print(e) for e in middle]
        self.cursor.y += delta * move_cursor
        for e in self.selection:
            e.y += delta
        self.draw_selection()

        # if delta == 1:
        #     _ = start.pop()
        #     end.insert(0, _)
        # elif delta == -1:
        #     _ = end.pop(0)
        #     start.append(_)
        #
        # lines = start + middle + end
        #
        self._append_undo(self.text, y, x)
        self.text = '\n'.join(lines)
        # print('moved line')


    def erase(self, rerender=True):
        # if not self.selection or self.selection[0] == self.selection[1]:
        x, y = int(self.cursor.x), int(self.cursor.y)
        if x+y == 0:
            return

        lines = self.text.split('\n')
        l = lines[y]
        # delete \n and go to line above
        if x == 0 and y > 0:
            new_x = len(lines[y-1])
            lines[y-1] += l
            lines.pop(y)
            self.cursor.x = new_x
            self.cursor.y -= 1
        # delete tab
        elif l[:x].lstrip() == '' and x >=4:
            l = l[4:]
            self.cursor.x -= 4
            lines[y] = l
        # normal erase
        else:
            l = l[:x-1] + l[x:]
            self.cursor.x -= 1

            lines[y] = l

        self._append_undo(self.text, y, x)

        # else:   # delete selected
        #     self.delete_selected()
        #     lines = self.text.split('\n')
        #
        #     start_y = int(self.selection[0][1])
        #     end_y = int(self.selection[1][1])
        #
        #     if start_y == end_y:    # one line
        #         l = lines[start_y]
        #         start_x, end_x = int(self.selection[0][0]), int(self.selection[1][0])
        #         l = l[:start_x] + l[end_x:]
        #         lines[start_y] = l
        #         self.selection = (self.selection[0], self.selection[0])
        #         self.cursor.position = self.selection[0][0]
                # self.selection = None
                # self.selection[0][0],start_y),
                # ignore=True, scale_x=self.selection[1][0]-self.selection[0][0])
            # for y in range(int(self.selection[0][1]), int(self.selection[1][1])):
            #     lines.pop(y)




        self.text = '\n'.join(lines)
        if rerender:
            self.render()

    def _ordered_selection(self):
        if not self.selection:
            return None
        if self.selection[1][1] < self.selection[0][1] or (self.selection[1][1] == self.selection[0][1] and self.selection[1][0] < self.selection[0][0]):
            return [self.selection[1], self.selection[0]]
        return self.selection


    def delete_selected(self):
        if not self.selection or self.selection[0] == self.selection[1]:
            return

        sel = self._ordered_selection()
        self.cursor.position = sel[0]
        start_y = int(sel[0][1])
        end_y = int(sel[1][1])
        lines = self.text.split('\n')

        lines[start_y] = lines[start_y][:int(sel[0][0])] + lines[end_y][int(sel[1][0]):]
        del lines[(start_y + 1) : (end_y + 1)]

        self._append_undo(self.text, self.cursor.y, self.cursor.x)
        self.text = '\n'.join(lines)
        self.selection = [Vec2(0,0), Vec2(0,0)]
        # self._append_undo(self.text, y, x)
        self.draw_selection()
        self.render()


    def get_selected(self):
        if not self.selection or self.selection[0] == self.selection[1]:
            return None

        sel = self._ordered_selection()
        start_y = int(sel[0][1])
        end_y = int(sel[1][1])
        lines = self.text.split('\n')

        selected_text = ''
        # selected_text = lines[start_y][]

        for y in range(start_y, end_y+1):
            if y > start_y:
                selected_text += '\n'
            selected_text += lines[y][(int(sel[0][0]) if y == start_y else 0) : (int(sel[1][0]) if y == end_y else len(lines[y])) ]

        return selected_text


    def get_mouse_position_unclamped(self):
        if self.world_space_mouse:
            if mouse.point:
                x = round(mouse.point[0] * self.bg.scale_x)
                y = floor(mouse.point[1] * self.bg.scale_y)
            else:
                x = int(self.cursor.x)
                y = int(self.cursor.y)

        else:
            mpos = mouse.position
            mpos.x = mpos.x / self.world_scale_x * camera.ui.world_scale_x
            mpos.y = mpos.y / self.world_scale_y * camera.ui.world_scale_y
            mpos += camera.ui.get_position(self.text_entity)

            x = round(mpos.x / self.cursor_parent.scale_x)
            y = floor(mpos.y / self.cursor_parent.scale_y)

        return (x, y)


    def get_mouse_position(self):
        (x, y) = self.get_mouse_position_unclamped()

        lines = self.text.split('\n')
        y = clamp(y, 0, len(lines) - 1)
        x = clamp(x, 0, len(lines[y]))

        return (x, y)

    def set_scroll(self, value, render=True):
        self.scroll = value
        if self.max_lines < 9999:
            if self.scroll < 9999:
                self.scroll = clamp(self.scroll, 0, 9999)
                self.render()
        else:
            self.scroll = clamp(self.scroll, 0, 9999)
            self.scroll_parent.y = (self.scroll * Text.size * self.line_height)

        if render:
            self.render()


    def input(self, key):
        # print('-------------', key)
        text, cursor, on_undo, add_text, erase = self.text, self.cursor, self.on_undo, self.add_text, self.erase

        if self.register_mouse_input and self.bg.hovered and key == 'left mouse up':
            self.active = True

        elif self.register_mouse_input and not self.bg.hovered and key == 'left mouse down':    # clicked outside
            self.active = False

        if not self.active:
            return

        if mouse.hovered_entity == self.bg:
            if key in self.shortcuts['scroll_down']:
                self.set_scroll(self.scroll + self.scroll_amount)

            elif key in self.shortcuts['scroll_up']:
                self.set_scroll(self.scroll - self.scroll_amount)

        if key == 'double click':
            t = time.time()
            if t - self._last_double_click < self.triple_click_delay:
                key = 'triple click'
            self._last_double_click = t

        # key = f'{"ctrl" if held_keys["control"]} + {key}'
        ctrl, shift, alt = '', '', ''
        if held_keys['control'] and key != 'control':
            ctrl = 'ctrl+'
        if held_keys['shift'] and key != 'shift':
            shift = 'shift+'
        if held_keys['alt'] and key != 'alt':
            alt = 'alt+'

        key = ctrl+shift+alt+key
        x, y = int(cursor.x), int(cursor.y)
        lines = text.split('\n')
        l = lines[y]

        if key in self.shortcuts['move_operations']['move_up']:
            if y > 0:
                cursor.y -= 1
                cursor.x = min(x, len(lines[y-1]))
        if key in self.shortcuts['move_operations']['move_down']:
            if y < len(lines)-1:
                cursor.y += 1
                cursor.x = min(x, len(lines[y+1]))
        if key in self.shortcuts['move_operations']['move_right']:
            if x == len(l) and y < len(lines)-1:        # end of line, move to beginning of next
                cursor.y += 1
                cursor.x = 0
            elif x < len(l):
                cursor.x += 1
        if key in self.shortcuts['move_operations']['move_left']:
            if x > 0:
                cursor.x -= 1
            elif y > 0:                                 # move to end of line above
                cursor.y -= 1
                cursor.x = len(lines[y-1])


        delimiters = self.delimiters

        if key in self.shortcuts['move_operations']['move_to_start_of_word']:
            self.move_to_start_of_word()
            if x < len(l) and (l[x] in delimiters or x == 0):
                self.move_to_start_of_word()
            if held_keys['shift']:
                self.selection[1] = self.cursor.position
                self.draw_selection()

        if key in self.shortcuts['move_operations']['move_to_end_of_word']:
            self.move_to_end_of_word()
            if x == len(l) or l[x] in delimiters:
                self.move_to_end_of_word()

        moved_cursor = False
        for move in self.shortcuts['move_operations']:
            if key in self.shortcuts['move_operations'][move]:
                moved_cursor = True
                if not held_keys['shift']:
                    self.selection = [Vec2(self.cursor.X, self.cursor.Y), Vec2(self.cursor.X, self.cursor.Y)]
                    self.draw_selection()
                break

        if moved_cursor and held_keys['shift']:
            self.selection[1] = self.cursor.position
            self.draw_selection()


        if key in self.shortcuts['newline'] and (self.cursor.y < self.max_lines-1 or self.max_lines == 9999):
            if self.selection:
                self.delete_selected()

            if l.startswith('class ') and not l.endswith(':'):
                add_text(':', rerender=False)
            if l.startswith('def ') and not l.endswith(':'):
                add_text(':', rerender=False)

            add_text('\n', rerender=False)

            if l.strip() == '':
                indent = len(l)
            else:
                indent = 0
                if l.lstrip().startswith('class ') or l.lstrip().startswith('def '):
                    indent += 4

            self.cursor.y += 1
            self.cursor.x = 0
            add_text(' '*indent)
            return
            # self.cursor.x = indent

        if key in self.shortcuts['indent']:
            if not self.selection or self.selection[0] == self.selection[1]:
                add_text(' '*4, move_cursor=True)
                return

            else:
                for y in range(int(self.selection[0][1]), int(self.selection[1][1])+1):
                    # print('indent', y)
                    lines[y] = (' '*4) + lines[y]
                self.cursor.x += 4
                self._append_undo(self.text, y, x)
                self.text = '\n'.join(lines)
                self.render()
                return

        if key in self.shortcuts['dedent']:
            if not self.selection or self.selection[0] == self.selection[1]:
                if l.startswith(' '*4):
                    lines[y] = l[4:]
            else:
                for y in range(int(self.selection[0][1]), int(self.selection[1][1])+1):
                    l = lines[y]
                    if l.startswith(' '*4):
                        # print('dedent')
                        lines[y] = l[4:]

            self.cursor.x = max(self.cursor.x - 4, 0)
            self._append_undo(self.text, y, x)
            self.text = '\n'.join(lines)
            self.render()
            return

        if key in self.shortcuts['scroll_to_bottom'] and mouse.hovered_entity == self.bg:
            self.scroll_to_bottom()


        if key in self.shortcuts['erase']:
            if not self.selection or self.selection[1] == self.selection[0]:
                erase()
                return
            else:
                self.delete_selected()
                return

        if key in self.shortcuts['erase_word']:
            # print('delete word')
            if x == 0:
                erase()
                return

            if ' ' not in l:
                l = l[x:]
                self.cursor.x = 0

            else:
                removed = ''
                beginning = l[:x]


                if beginning.endswith('  '): # delete whitespace
                    beginning = beginning.rstrip()
                    removed = ' ' * (x-len(beginning))

                for delimiter in ('.', '\'', '\"', '(', '"', '\''):
                    beginning = beginning.replace(delimiter, ' ')

                if ' ' not in beginning: # first word of the line
                    beginning = ''
                else:
                    beginning = beginning.rstrip().rsplit(' ', 1)[0]

                removed = l[len(beginning):x]

                l = beginning + l[x:]
                self.cursor.x -= len(removed)

            lines[y] = l
            self._append_undo(self.text, y, x)
            self.text = '\n'.join(lines)
            self.render()
            return

        if key in self.shortcuts['move_line_down'] and self.cursor.y < self.max_lines:
            # print('move down')
            if y+1 == len(lines): # if at last line
                self.text += '\n'

            self.move_line(y, 1)
            self.render()
            return

        if key in self.shortcuts['move_line_up'] and y > 0:
            self.move_line(y, -1)
            self.render()
            return

        if key in self.shortcuts['undo']:
            if not on_undo:
                return

            self.on_redo.append((self.text, y, x))
            self.text = on_undo[-1][0]
            cursor.y = on_undo[-1][1]
            cursor.x = on_undo[-1][2]
            on_undo.pop()
            self.render()
            return

        if key in self.shortcuts['redo']:
            if not self.on_redo:
                return

            on_undo.append((self.text, y, x))
            self.text = self.on_redo[-1][0]
            cursor.y = self.on_redo[-1][1]
            cursor.x = self.on_redo[-1][2]
            self.on_redo.pop()
            self.render()
            return

        if key in self.shortcuts['delete_line']:
            self._append_undo(self.text, y, 0)
            lines.pop(y)

            if y == 0:
                self.cursor.x = 0
            else:
                self.cursor.x = len(lines[y-1])

            if y >= len(lines) and y > 0:
                self.cursor.y -= 1

            self.text = '\n'.join(lines)
            self.render()
            return

        if key in self.shortcuts['duplicate_line']:
            if len(lines) < self.max_lines:
                self._append_undo(self.text, y, 0)
                lines.insert(y, lines[y])
                self.text = '\n'.join(lines)
                cursor.y += 1


        if key in self.shortcuts['select_word']:
            if x >= len(l):
                return

            for start_x in range(x, -1, -1):    # go left until end of word
                if l[start_x] in delimiters:
                    break
            if start_x > 0:
                start_x += 1

            for end_x in range(x, len(l)):
                if l[end_x] in delimiters:
                    break
            if end_x == len(l)-1:
                end_x += 1


            self.selection = [Vec2(start_x, y), Vec2(end_x, y)]
            self.draw_selection()
            return

        if key in self.shortcuts['select_line']:
            self.selection = [Vec2(0, y), Vec2(len(lines[y]), y)]
            self.cursor.position = self.selection[1]
            self.draw_selection()
            return

        if key in self.shortcuts['copy']:# TODO
            selectedText = self.get_selected()
            if selectedText:
                pyperclip.copy(selectedText)
                # print('-----copy:', selectedText)

        if key in self.shortcuts['paste']:# TODO
            if self.selection:
                self.delete_selected()
            self.add_text(pyperclip.paste())

        if key in self.shortcuts['cut']:# TODO
            selectedText = self.get_selected()
            if selectedText:
                pyperclip.copy(selectedText)
                self.delete_selected()
                # print('-----cut:', selectedText)

        if key in self.shortcuts['select_all']:
            self.select_all()
            return

        if self.register_mouse_input:
            if key == 'left mouse down' and mouse.hovered_entity == self.bg and mouse.point is not None:
                cursor.position = self.get_mouse_position()
                self.selection = [self.cursor.position, self.cursor.position]
                self.draw_selection()

            if key == 'left mouse up':
                    cursor.position = self.get_mouse_position()

                    if self.selection:
                        self.selection[1] = self.cursor.position



    def move_to_start_of_word(self):
        cursor = self.cursor
        x, y = int(cursor.x), int(cursor.y)
        lines = self.text.split('\n')
        l = lines[y]
        delimiters = self.delimiters

        if x == 0 and y == 0:
            pass

        elif x == 0 and y > 0:                        # move to end of line above
            cursor.y -= 1
            cursor.x = len(lines[y-1])

        elif l[x-1] not in delimiters:              # move left to closest delimiter
            for x in range(x-1, 0, -1):
                if l[x] in delimiters:
                    cursor.x = x+1
                    return
            cursor.x = 0
        else:                                       # move left to closest word
            for x in range(x-1, 0, -1):
                if l[x] not in delimiters:
                    cursor.x = x+1
                    return
            cursor.x = 0

    def move_to_end_of_word(self):
        cursor = self.cursor
        x, y = int(cursor.x), int(cursor.y)
        lines = self.text.split('\n')
        l = lines[y]
        delimiters = self.delimiters

        if x == len(l):
            if y < len(lines)-1:        # end of line, move to beginning of next
                cursor.y += 1
                cursor.x = 0
            return

        elif l[x] not in delimiters:                # move right to closest delimiter
            for x in range(x, len(l)):
                if l[x] in delimiters:
                    cursor.x = x
                    return
            cursor.x = len(l)
        else:                                       # move right to closest word
            for x in range(x, len(l)):
                if l[x] not in delimiters:
                    cursor.x = x
                    return
            cursor.x = len(l)

    def scroll_to_bottom(self, blank_lines_at_bottom=0):
        # self.scroll = min(len(self.text.split('\n')), self.max_lines)
        self.set_scroll(len(self.text.split('\n'))-self.max_lines+blank_lines_at_bottom)
        # print('scrolled to bottom', min(len(self.text.split('\n')), self.max_lines))


    def text_input(self, key):
        cursor, add_text = self.cursor, self.add_text

        if not self.active:
            return

        if held_keys['shift'] and held_keys['alt']:
            return

        if self.selection:  # delete selected text
            self.delete_selected()

        add_text(key)

        if key == '(':
            add_text(')')
            cursor.x -= 1
        elif key == '[':
            add_text(']')
            cursor.x -= 1
        elif key == '{':
            add_text('}')
            cursor.x -= 1

        self.render()


    def render(self):
        lines = self.text.split('\n')
        if self.max_lines < 9999:
            text = '\n'*self.scroll + '\n'.join(lines[self.scroll : self.max_lines + self.scroll])
        else:
            text = self.text

        if not hasattr(self.text_entity, 'raw_text') or self._prev_text != text or self.scroll != self._prev_scroll:
            # print(self.scroll)

            if self.replacements:
                _lines = text.split('\n')
                for i in range(len(_lines)):
                    if _lines[i].lstrip().startswith('#'):
                        _lines[i] = f'☾gray☽{lines[i]}☾default☽'
                    else:
                        _lines[i] = multireplace(_lines[i], self.replacements)
                self.text_entity.text = '\n'.join(_lines)

                # self.text_entity.text = multireplace(text, self.replacements)
            else:
                self.text_entity.text = text

            self.line_numbers.text = ('     \n'*self.scroll) + '\n'.join([str(e + self.scroll).rjust(3, ' ') for e in range(min(len(lines), self.max_lines))])

            self._prev_text = text
            self._prev_scroll = self.scroll
            self.scroll_parent.y = (self.scroll * Text.size * self.line_height)
            self.cursor.visible = self.cursor.y >= self.scroll and self.cursor.y < self.scroll + self.max_lines
            self.draw_selection()

        if self.on_value_changed:
            self.on_value_changed()



    def update(self):
        if self.active and self.register_mouse_input and mouse.left and mouse.moving:
            self.cursor.position = self.get_mouse_position()
            if self.selection:
                self.selection[1] = self.cursor.position

            self.draw_selection()


    def select_all(self):
        lines = self.text.split('\n')
        if lines:
            self.selection = [Vec2(0,0), Vec2(len(lines[-1]), len(lines) - 1)]

        self.draw_selection()


    def draw_selection(self):
        # print(self.selection)
        [destroy(c) for c in self.selection_parent.children]

        if not self.selection or self.selection[0] == self.selection[1]:
            return

        sel = self._ordered_selection()
        # print('---', self.selection, self.scroll)

        start_y = int(sel[0].y)
        end_y = int(sel[1].y)
        lines = self.text.split('\n')
        if start_y == end_y:
            e = Entity(parent=self.selection_parent, model='quad', origin=(-.5,-.5), color=self.highlight_color, double_sided=True, ignore=True, y=start_y)
            e.x = sel[0].x
            e.scale_x = sel[1].x - sel[0].x
        else:
            # first line
            if start_y >= self.scroll and start_y < self.scroll+self.max_lines:
                e = Entity(parent=self.selection_parent, model='quad', origin=(-.5, -.5),
                color=self.highlight_color, double_sided=True, position=(sel[0].x,start_y), ignore=True)
                e.scale_x = len(lines[start_y]) - sel[0].x

            # middle lines
            for y in range(max(start_y+1, self.scroll), min(end_y, self.scroll+self.max_lines)):
                e = Entity(parent=self.selection_parent, model='quad', origin=(-.5, -.5),
                    color=self.highlight_color, double_sided=True, position=(0,y), ignore=True, scale_x=len(lines[y]))

            # last line
            if end_y >= self.scroll and end_y < self.scroll+self.max_lines:
                e = Entity(parent=self.selection_parent, model='quad', origin=(-.5, -.5),
                color=self.highlight_color, double_sided=True, position=(0,end_y), ignore=True)
                e.scale_x = sel[1].x

        for e in self.selection_parent.children:
            e.enabled = e.y >= self.scroll and e.y < self.scroll+self.max_lines


if __name__ == '__main__':
    from ursina import Ursina, window, Button
    app = Ursina(vsync=60)

    # camera.orthographic = True
    # camera.fov = 1
    # window.size = window.fullscreen_size
    # window.x = 200

    window.color = color.hsv(0, 0, .1)
    Button.default_color = color._20
    window.color = color._25

    # Text.size = 1/window.fullscreen_size[1]*16
    # Text.default_font = 'consola.ttf'
    # Text.default_resolution = 16*2
    # TreeView()
    te = TextField(max_lines=30, scale=1, register_mouse_input = True, text='1234')
    #te = TextField(max_lines=300, scale=1, register_mouse_input = True, scroll_size = (50,3))
    # te.line_numbers.enabled = True
    # for name in color.color_names:
    #     if name == 'black':
    #         continue
    #     te.replacements[f' {name}'] = f'☾{name}☽ {name}☾default☽'
    # te.replacements = {
    #     'class ':    '☾orange☽class ☾default☽',
    #     'def ':      '☾azure☽def ☾default☽',
    #     '__init__':  '☾cyan☽__init__☾default☽',
    #     'Entity':    '☾lime☽Entity☾default☽',
    #     'self.':     '☾orange☽self☾default☽.',
    #     '(self)':     '(☾orange☽self☾default☽)',
    #     'self,':     '☾orange☽self☾default☽,',
    #     '    ':      '☾dark_gray☽----☾default☽',
    #     }
    #
    from textwrap import dedent
    te.text = dedent('''
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        Aliquam sapien tellus, venenatis sit amet ante et, malesuada porta risus.
        Etiam et mi luctus, viverra urna at, maximus eros. Sed dictum faucibus purus,
        nec rutrum ipsum condimentum in. Mauris iaculis arcu nec justo rutrum euismod.
        Suspendisse dolor tortor, congue id erat sit amet, sollicitudin facilisis velit.
        Aliquam sapien tellus, venenatis sit amet ante et, malesuada porta risus.
        Etiam et mi luctus, viverra urna at, maximus eros. Sed dictum faucibus purus,
        nec rutrum ipsum condimentum in. Mauris iaculis arcu nec justo rutrum euismod.
        Suspendisse dolor tortor, congue id erat sit amet, sollicitudin facilisis velit.
        '''*30
        )[1:]
    te.render()

    def input(key):
        if key == '3':
            te.input('scroll down')


    app.run()
