# Copyright 2017 Tom Rothamel <pytom@bishoujo.us>

# Permission to use, copy, modify, and/or distribute this software for
# non-commerical purposes is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# For the purpose of this license, when using this software to develop a
# another software program, this program is being used commerically if
# payment is required to distribute that program, to use that program, or
# to access any feature in that program, or if the program presents
# advertising to its user.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.

# TODO:
#
# - Allow the creator to specify a list of tags they are interested in,
#   disabling the auto-detection.

init offset = -1101

init python in director:
    from store import Action, config
    import store

    # A set of tags that will not be show to the user.
    tag_blacklist = { "black", "text", "vtext", "side" }

    # A set of tags that should only be used with the scene statement.
    scene_tags = { "bg" }

    # The set of tags that should only be used with the show statement.
    show_tags = set()

    # A list of transforms to use.
    transforms = [ "left", "center", "right" ]

    # A list of transitions to use.
    transitions = [ "dissolve", "pixellate" ]

    # A list of audio channels we know about.
    audio_channels = [ "music", "sound", "audio" ]

    # The name of the voice channel.
    voice_channel = "voice"

    # A list of audio patterns to use for channels that do not have a
    # more specific list of patterns already defined.
    audio_patterns = [ "*.opus", "*.ogg", "*.mp3" ]

    # A map from channel name to the audio patterns to use for that channel.
    audio_channel_patterns = { }

    # A map from channel name to the audio files available on that channel.
    audio_files = { }

    # The spacing between a non-display line and a display line, or vice
    # versa.
    spacing = 1

    # The spacing between two display lines.
    director_spacing = 0

    # The spacing between two non-display lines.
    other_spacing = 0

    # The maximum height of viewports containing scrolling information.
    viewport_height = 280

    # Is the director enabled? Used by the tutorial to protect itself.
    enable = True

    state = renpy.session.get("director", None)

    # A list of statements we find too uninteresting to present to the
    # creator.
    UNINTERESTING_NODES = (
        renpy.ast.Translate,
        renpy.ast.EndTranslate,
    )

    # Nodes that are only interesting when on a line by itself.
    ALONE_NODES = (
        renpy.ast.Label,
        renpy.ast.Pass,
    )

    def audio_code_to_filename(channel, fn):
        return fn

    def audio_filename_to_code(channel, fn):
        return fn

    def audio_filename_to_display(channel, fn):
        return fn

    def is_play(n):
        return isinstance(n, renpy.ast.UserStatement) and n.get_name().startswith("play ")

    def is_queue(n):
        return isinstance(n, renpy.ast.UserStatement) and n.get_name().startswith("queue ")

    def is_stop(n):
        return isinstance(n, renpy.ast.UserStatement) and n.get_name().startswith("stop ")

    def is_voice(n):
        return isinstance(n, renpy.ast.UserStatement) and (n.get_name() == "voice")


    def is_interesting(n):

        if isinstance(n, (
            renpy.ast.Show,
            renpy.ast.Hide,
            renpy.ast.Scene,
            renpy.ast.With,
        )):

            return True

        if is_play(n) or is_queue(n) or is_stop(n) or is_voice(n):
            return True

        return False


    # Initialize the state object if it doesn't exist.
    if state is None:

        state = store.NoRollback()

        # Is the directory currently active.
        state.active = False

        # Should the director screen be shown.
        state.show_director = False

        # The list of lines we've seen recently.
        state.lines = [ ]

        # The mode we're in.
        state.mode = "lines"

        # The filename and linenumber of the line we're editing,
        state.filename = ""
        state.linenumber = 0

        # What tags are currently showing?
        state.showing = set()

        # What kind of statement is this?
        state.kind = None
        state.original_kind = None

        # The tag we're updating.
        state.tag = ""
        state.original_tag = ""

        # The attributes of the image we're updating.
        state.attributes = [ ]
        state.original_attributes = [ ]

        # The transforms applied to the image.
        state.transforms = [ ]
        state.original_transforms = [ ]

        # A list of image tags the current image is behind.
        state.behind = [ ]
        state.original_behind = [ ]

        # The transition in a with statement.
        state.transition = None
        state.original_transition = None

        # The audio channel.
        state.channel = None
        state.original_channel = None

        # The audio file.
        state.audio = None
        state.original_audio = None

        # NOTE: Remember to add new states to the Cancel function.

        # Has the new line been added to ast.
        state.added_statement = None

        # Are we changing the script? (Does the node needed to be removed?)
        state.change = False

        # The position of the first line.
        state.old_pos = None

        renpy.session["director"] = state

    def interact_base():
        """
        This is called by interact to update our data structures.
        """

        if renpy.game.interface.trans_pause:
            return False

        show_director = False

        # Update the line log.
        lines = renpy.get_line_log()
        renpy.clear_line_log()

        if lines:
            pos = (lines[0].filename, lines[0].line)
            if pos != state.old_pos:
                state.old_pos = pos
                renpy.hide_screen("director")

        # Update state.line to the current line.
        state.line = renpy.get_filename_line()

        # State.lines is the list of lines we've just seen, along with
        # the actions used to edit those lines.
        for lle in lines[-30:]:
            if isinstance(lle.node, renpy.ast.Say):
                state.lines = [ ]
                break

        for lle in lines[-30:]:

            filename = lle.filename
            line = lle.line
            node = lle.node

            if isinstance(node, UNINTERESTING_NODES):
                continue

            if isinstance(node, ALONE_NODES):
                if [ i for i in renpy.scriptedit.nodes_on_line(filename, line) if not isinstance(i, ALONE_NODES) ]:
                    continue

            if filename.startswith("renpy/"):
                show_director = False
                continue
            else:
                show_director = True

            text = renpy.scriptedit.get_line_text(filename, line)

            if text is None:
                text = ""

            text = text.strip()

            short_fn = filename.rpartition('/')[2]
            pos = "{}:{:04d}".format(short_fn, line)

            if lle.abnormal:
                add_action = None
            else:
                add_action = AddStatement(lle)

            if is_interesting(node):
                change_action = ChangeStatement(lle, node)
            else:
                change_action = None

            state.lines.append((
                pos,
                text,
                add_action,
                change_action,
            ))

        return show_director


    def interact():
        """
        This is called once per interaction, to update the list of lines
        being displayed, and also to show or hide the director as appropriate.
        """

        show_director = interact_base()

        # Show the director screen.
        if show_director and state.show_director and (renpy.context_nesting_level() == 0):
            if not renpy.get_screen("director"):
                renpy.show_screen("director")
        else:
            if renpy.get_screen("director"):
                renpy.hide_screen("director")

    def line_log_callback(lle):
        """
        This annotates a line log entry with more information.
        """
        lle.showing = set(renpy.get_showing_tags())


    def init():
        """
        This is called once at game start, to reconfigured Ren'Py to
        support the ID.
        """

        config.clear_lines = False
        config.line_log = True
        config.line_log_callbacks = [ line_log_callback ]

        config.start_interact_callbacks.append(interact)

    if state.active:
        init()

    def command():
        """
        This can be used to invoke the ID from the command line.
        """

        if not state.active:
            state.active = True
            init()

        return True

    renpy.arguments.register_command("director", command)


    def get_scene_show_hide_statement():


        if state.kind == "scene" and state.tag is None:
            return "scene"

        if state.tag is None:
            return None

        if state.kind == "hide":

            attributes = state.attributes

        else:

            if state.change and (state.attributes == state.original_attributes):
                attributes = state.attributes
            else:

                attributes = get_image_attributes()

                if attributes is None:
                    return None

        rv = [ state.kind ]

        rv.append(state.tag)
        rv.extend(attributes)

        rv = " ".join(rv)

        if state.transforms:
            rv += " at " + ", ".join(state.transforms)

        if state.behind:
            rv += " behind " + ",".join(state.behind)

        return rv

    def quote_audio():
        """
        Returns the quoted audio filename.
        """

        if state.audio is None:
            return None

        if state.channel is not None:
            audio = audio_filename_to_code(state.channel, state.audio)
        else:
            audio = state.audio

        return "'" + audio.replace("\\", "\\\\").replace("'", "\\'") + "'"


    def get_play_queue_statement():
        if state.channel is None:
            return None

        if state.audio is None:
            return None

        return "{} {} {}".format(state.kind, state.channel, quote_audio())

    def get_stop_statement():
        if state.channel is None:
            return None

        return "{} {}".format(state.kind, state.channel)

    def get_voice_statement():
        if state.audio is None:
            return None

        return "voice {}".format(quote_audio())

    def get_statement():
        """
        If a statement is defined enough to implement, returns the text
        that can be added to the AST. Otherwise, returns None.
        """

        if state.kind is None:
            return None

        elif state.kind == "with":
            return "with {}".format(state.transition)

        elif state.kind in ("play", "queue"):
            return get_play_queue_statement()

        elif state.kind == "stop":
            return get_stop_statement()

        elif state.kind == "voice":
            return get_voice_statement()

        else:
            return get_scene_show_hide_statement()


        return rv

    def update_ast(force=False):
        """
        Updates the abstract syntax tree to match the current state, forcing
        a rollback if something significant has changed. This always forces
        an interaction restart.
        """

        statement = get_statement()

        if (state.added_statement == statement) and (not force):
            renpy.restart_interaction()
            return

        linenumber = state.linenumber

        if statement:
            renpy.scriptedit.add_to_ast_before(statement, state.filename, linenumber)
            linenumber += 1

        if state.added_statement is not None:
            renpy.scriptedit.remove_from_ast(state.filename, linenumber)

        state.added_statement = statement

        renpy.rollback(checkpoints=0, force=True, greedy=True)

    def dump_script():
        for i in range(0, 100):
            key = ('game/script.rpy', i)
            if key in renpy.scriptedit.lines:
                print(key, renpy.scriptedit.lines[key].text)

    def pick_tag():
        """
        If there is only one valid tag, choose it and move to attribute mode.
        """

        tags = get_tags()

        if state.mode != "tag":
            return

        if len(tags) != 1:
            return

        state.tag = tags[0]

        if state.kind == "hide":
            return

        state.mode = "attributes"


    def find_statement(filename, line, delta, limit=10):
        """
        Tries to find a statement near `line`. If it can't find it on `line`
        itself, it searches forward (if `delta`) is positive or back (if `delta`)
        is negative.

        Returns the line number and nodes for the statement, or (None, None) if the
        statement is not found after searching `limit` lines.
        """

        for _i in range(limit):

            nodes = renpy.scriptedit.nodes_on_line(filename, line)
            if nodes:
                return line, nodes

            line += delta

        return None, None

    def needs_space(filename, line):
        """
        Returns True if there should be a space between (filename, line-1) and
        (filename, line).
        """


        previous, previous_nodes = find_statement(filename, line-1, -1)

        if previous is None:
            return None

        next, next_nodes = find_statement(filename, line, 1)

        if next is None:
            return None


        def display(nodes):
            for n in nodes:
                if is_interesting(n):
                    return True

            return False

        previous_display = display(previous_nodes)
        next_display = display(next_nodes)

        if previous_display ^ next_display:
            return spacing
        elif previous_display:
            return director_spacing
        else:
            return other_spacing

    def is_spacing(filename, line):

        line = renpy.scriptedit.get_full_text(filename, line)

        if line is None:
            return False

        return not line.strip()


    def adjust_spacing_before(filename, line):
        """
        Adjusts the spacing between (filename, line) and the line before it.
        """

        line, _ = find_statement(filename, line, 1)

        if line is None:
            return

        space = needs_space(filename, line)

        if space is None:
            return

        previous, _ = find_statement(filename, line - 1, -1)

        blanks = [ ]

        for i in range(previous + 1, line):
            if not is_spacing(filename, i):
                break

            blanks.append(i)

        delta_space = space - len(blanks)

        if delta_space > 0:
            for _ in range(delta_space):
                renpy.scriptedit.adjust_ast_linenumbers(filename, previous + 1, 1)
                renpy.scriptedit.insert_line_before('', filename, previous + 1)

        elif delta_space < 0:
            blanks.reverse()
            blanks = blanks[space:]

            for i in blanks:
                renpy.scriptedit.adjust_ast_linenumbers(filename, i, -1)
                renpy.scriptedit.remove_line(filename, i)


    def add_spacing(filename, line):
        adjust_spacing_before(filename, line + 1)
        adjust_spacing_before(filename, line)

    def remove_spacing(filename, line):
        adjust_spacing_before(filename, line)


    # Screen support functions #################################################

    def get_tags():
        """
        Returns a list of tags that are valid for the current statement kind.
        """

        if state.kind == "scene":
            rv = [ i for i in renpy.get_available_image_tags() if not i.startswith("_") if i not in tag_blacklist if i in scene_tags ]
        elif state.kind == "show":
            if show_tags:
                rv = [ i for i in renpy.get_available_image_tags() if not i.startswith("_") if i not in tag_blacklist if i in show_tags ]
            else:
                rv = [ i for i in renpy.get_available_image_tags() if not i.startswith("_") if i not in tag_blacklist if i not in scene_tags ]
        elif state.kind == "hide":
            rv = [ i for i in renpy.get_available_image_tags() if i in state.showing if not i.startswith("_") if i not in tag_blacklist if i not in scene_tags ]
        else:
            rv = [ ]

        rv.sort(key=component_key)
        return rv

    def get_attributes():
        """
        Returns a list of attributes that are valid for the current tag.
        """

        if not state.tag:
            return [ ]

        return renpy.get_ordered_image_attributes(state.tag, [ ], component_key)

    def get_transforms():
        """
        Returns a list of transforms that are valid for the current tag.
        """

        return transforms

    def get_image_attributes():
        """
        Returns the list of attributes in the current image, or None if
        no image is known.
        """

        if state.tag is None:
            return None

        return renpy.check_image_attributes(state.tag, state.attributes)

    def get_ordered_attributes():
        """
        Returns the list of attributes in the order they appear in the
        current image (if known), or in the order they were selected
        otherwise.
        """

        attrs = get_image_attributes()

        if attrs is not None:
            return attrs

        return state.attributes

    def get_behind_tags(exclude=None):
        """
        Get a list of tags the current tag can be placed behind.
        """

        rv = [ ]

        for t in state.showing:
            if t in scene_tags:
                continue

            if t == exclude:
                continue

            rv.append(t)

        return rv

    # Actions ##################################################################

    class Start(Action):
        """
        This action starts the director and displays the lines screen.
        """

        def __call__(self):

            if not state.show_director:

                if store._menu:
                    return None

                if not config.developer:
                    return None

                if not enable:
                    renpy.notify(_("The interactive director is not enabled here."))
                    return None

            if state.show_director:
                return None

            state.show_director = True
            state.mode = "lines"

            if state.active:

                renpy.show_screen("director")
                renpy.restart_interaction()

                return

            # renpy.session["compile"] = True
            renpy.session["_greedy_rollback"] = True

            state.active = True

            store._reload_game()

        def get_sensitive(self):
            return (not state.active) or (not state.show_director)

    class Stop(Action):
        """
        This hides the director interface.
        """

        def __call__(self):
            state.show_director = False

            if renpy.get_screen("director"):
                renpy.hide_screen("director")

            renpy.restart_interaction()


    class AddStatement(Action):
        """
        An action that adds a new statement before `filename`:`linenumber`.
        """

        def __init__(self, lle):
            self.lle = lle

            self.sensitive = renpy.scriptedit.can_add_before(self.lle.filename, self.lle.line)

        def get_sensitive(self):
            return self.sensitive

        def __call__(self):

            state.filename = self.lle.filename
            state.linenumber = self.lle.line
            state.showing = self.lle.showing

            state.kind = None
            state.mode = "kind"

            state.tag = None
            state.original_tag = None

            state.attributes = [ ]
            state.original_attributes = [ ]

            state.transforms = [ ]
            state.original_transforms = [ ]

            state.transition = None
            state.original_transition = None

            state.behind = [ ]
            state.original_behind = [ ]

            state.channel = None
            state.original_channel = None

            state.audio = None
            state.original_audio = None

            state.added_statement = None
            state.change = False

            update_ast()


    def is_scene_show_hide_editable(node):
        """
        Return true if a Scene, Show, or Hide ATL node is editable, or
        False otherwise.
        """

        if not node.imspec:
            return True

        if node.imspec[1]: # expression
            return False

        if node.imspec[2]: # tag
            return False

        if node.imspec[4]: # layer
            return False

        if node.imspec[5]: # zorder
            return False

        if getattr(node, "atl", None):
            return False

        return True


    class ChangeStatement(Action):
        """
        An action that changes the statement at `filename`:`linenumber`.

        `node`
            Should be the AST node corresponding to that statement.
        """

        def __init__(self, lle, node):
            self.lle = lle

            self.tag = None
            self.attributes = [ ]
            self.transforms = [ ]
            self.transition = None
            self.behind = [ ]

            self.channel = None
            self.audio = None

            self.sensitive = True

            def audio(n):

                self.sensitive = False

                name = n.parsed[0]
                p = n.parsed[1]

                self.kind = name[0]
                self.channel = p["channel"] or name[1]

                if "file" in p:
                    try:
                        self.audio = eval(p["file"])
                    except:
                        return

                    self.audio = audio_code_to_filename(self.channel, self.audio)
                else:
                    self.audio = None

                if p.get("loop", None):
                    return

                fadeout = p.get("fadeout", None)

                if (fadeout is not None) and (fadeout != "None"):
                    return

                if p.get("if_changed", False):
                    return

                if p.get("fadein", "0") != "0":
                    return

                self.sensitive = True


            if isinstance(node, renpy.ast.Show):
                self.kind = "show"

                self.tag = node.imspec[0][0]
                self.attributes = list(node.imspec[0][1:])
                self.transforms = list(node.imspec[3])
                self.behind = list(node.imspec[6])

                self.sensitive = is_scene_show_hide_editable(node)

            elif isinstance(node, renpy.ast.Scene):
                self.kind = "scene"

                # The scene statement does not need to show an image.
                if node.imspec is not None:

                    self.tag = node.imspec[0][0]
                    self.attributes = list(node.imspec[0][1:])
                    self.transforms = list(node.imspec[3])
                    self.behind = list(node.imspec[6])

                else:

                    self.tag = None
                    self.attributes = [ ]
                    self.transforms = [ ]
                    self.behind = [ ]

                self.sensitive = is_scene_show_hide_editable(node)

            elif isinstance(node, renpy.ast.Hide):
                self.kind = "hide"

                self.tag = node.imspec[0][0]
                self.attributes = list(node.imspec[0][1:])
                self.transforms = list(node.imspec[3])

                self.sensitive = is_scene_show_hide_editable(node)

            elif isinstance(node, renpy.ast.With):
                self.kind = "with"
                self.transition = node.expr

            elif is_play(node) or is_queue(node) or is_stop(node):
                audio(node)

            elif is_voice(node):
                p = node.parsed[1]

                self.kind = "voice"
                self.sensitive = False
                self.channel = voice_channel

                try:
                    self.audio = eval(p)
                except:
                    return

                self.audio = audio_code_to_filename(self.channel, self.audio)
                self.sensitive = True

        def get_sensitive(self):
            return self.sensitive

        def __call__(self):
            state.filename = self.lle.filename
            state.linenumber = self.lle.line
            state.showing = self.lle.showing

            state.kind = self.kind
            state.original_kind = self.kind

            if self.kind == "with":
                state.mode = "with"
            elif self.kind == "hide":
                state.mode = "tag"
            elif self.kind == "play" or self.kind == "queue":
                state.mode = "audio"
            elif self.kind == "stop":
                state.mode = "channel"
            elif self.kind == "voice":
                state.mode = "audio"
            else:
                if self.tag is None:
                    state.mode = "tag"
                else:
                    state.mode = "attributes"

            state.tag = self.tag
            state.original_tag = self.tag

            state.attributes = self.attributes
            state.original_attributes = list(self.attributes)

            state.transforms = list(self.transforms)
            state.original_transforms = list(self.transforms)

            state.transition = self.transition
            state.original_transition = self.transition

            state.behind = self.behind
            state.original_behind = list(self.behind)

            state.channel = self.channel
            state.original_channel = self.channel

            state.audio = self.audio
            state.original_audio = self.audio

            state.added_statement = True
            state.change = True

            update_ast()


    class SetKind(Action):

        def __init__(self, kind):
            self.kind = kind

        def __call__(self):

            if self.kind != state.kind:
                state.kind = self.kind
                state.tag = None
                state.attributes = [ ]

            if self.kind in ("scene", "show", "hide"):
                state.mode = "tag"
                pick_tag()

            if self.kind == "with":
                state.mode = "with"

            if self.kind in ("play", "queue", "stop"):
                state.mode = "channel"

            if self.kind == "voice":
                state.channel = "voice"
                state.mode = "audio"

            update_ast()


    class SetTag(Action):
        """
        An action that sets the image tag.
        """

        def __init__(self, tag):
            self.tag = tag

        def __call__(self):

            if state.tag != self.tag:

                state.tag = self.tag
                state.attributes = [ ]

            if state.kind != "hide":
                state.mode = "attributes"

            update_ast()

        def get_selected(self):
            return self.tag == state.tag


    class ToggleAttribute(Action):
        """
        This action toggles on and off an attribute. If an attribute being
        toggled on conflicts with other attributes, those attributes are
        removed.

        Then the AST is updated.
        """

        def __init__(self, attribute):
            self.attribute = attribute

        def __call__(self):
            if self.attribute in state.attributes:
                state.attributes.remove(self.attribute)
            else:

                state.attributes.append(self.attribute)

                compatible = set()

                for i in renpy.get_ordered_image_attributes(state.tag, [ self.attribute ]):
                    compatible.add(i)

                state.attributes = [ i for i in state.attributes if i in compatible ]

            update_ast()

        def get_selected(self):
            return self.attribute in state.attributes


    class SetList(Action):
        """
        When clicked once, sets l to [ v ]. When clicked again, sets l to [ ]
        """

        def __init__(self, l, v):
            self.l = l
            self.v = v

        def __call__(self):
            if self.v in self.l:
                self.l.remove(self.v)
            else:
                self.l[:] = [ self.v ]

            update_ast()

        def get_selected(self):
            return self.v in self.l


    class ToggleList(Action):
        """
        Toggles the presence or absence of v in l, appending it to the end
        of the list when necessary.
        """

        def __init__(self, l, v):
            self.l = l
            self.v = v

        def __call__(self):
            if self.v in self.l:
                self.l.remove(self.v)
            else:
                self.l.append(self.v)

            update_ast()

        def get_selected(self):
            return self.v in self.l


    class SetTransition(Action):
        """
        This sets the transition used by a with statement.
        """

        def __init__(self, transition):
            self.transition = transition

        def __call__(self):
            state.transition = self.transition

            update_ast()

        def get_selected(self):
            return self.transition == state.transition


    class SetChannel(Action):
        """
        This sets the channel used by an audio statement.
        """

        def __init__(self, channel):
            self.channel = channel

        def __call__(self):
            if state.kind != "stop":
                state.mode = "audio"

            state.channel = self.channel

            update_ast()

        def get_selected(self):
            return self.channel == state.channel


    class SetAudio(Action):
        """
        This sets the audio file played by a statement.
        """

        def __init__(self, filename):
            self.filename = filename

        def __call__(self):
            state.audio = self.filename

            update_ast()

        def get_selected(self):
            return state.audio == self.filename


    class Commit(Action):
        """
        Commits the current statement to the .rpy files.
        """

        def __call__(self):
            statement = get_statement()

            if statement:
                renpy.scriptedit.insert_line_before(statement, state.filename, state.linenumber)

            if state.change:
                if statement:
                    renpy.scriptedit.remove_line(state.filename, state.linenumber + 1)
                else:
                    renpy.scriptedit.remove_line(state.filename, state.linenumber)

            if not state.change and statement:
                add_spacing(state.filename, state.linenumber)

            if state.change and not statement:
                remove_spacing(state.filename, state.linenumber)

            state.mode = "lines"
            renpy.clear_line_log()
            renpy.rollback(checkpoints=0, force=True, greedy=True)

        def get_sensitive(self):
            return get_statement()

    class Reset(Action):
        """
        This action resets the AST to what it was when we started adjusting the
        statement.
        """

        def __call__(self):

            state.kind = state.original_kind
            state.tag = state.original_tag
            state.attributes = state.original_attributes
            state.transforms = state.original_transforms

            if state.kind is None:
                state.mode = "kind"
            elif state.tag is None:
                state.mode = "tag"

            update_ast()

    class Cancel(Action):
        """
        This action cancels the operation, resetting the AST and returning to
        the lines screen.
        """

        def __call__(self):

            state.kind = state.original_kind
            state.tag = state.original_tag
            state.attributes = state.original_attributes
            state.transforms = state.original_transforms

            state.transition = state.original_transition

            state.channel = state.original_channel
            state.audio = state.original_audio

            state.mode = "lines"

            update_ast()

    class Remove(Action):
        """
        This action removes the current line from the AST and the script.
        """

        def __call__(self):

            state.kind = None
            state.tag = None
            state.mode = "lines"

            try:
                update_ast(force=True)
            finally:
                if state.change:
                    renpy.scriptedit.remove_line(state.filename, state.linenumber)


    # Displayables #############################################################

    class SemiModal(renpy.Displayable):
        """
        This wraps a displayable, and ignores
        """

        def __init__(self, child):
            renpy.Displayable.__init__(self)

            self.child = child
            self.w = 0
            self.h = 0

        def per_interact(self):
            renpy.display.render.redraw(self, 0)

        def render(self, width, height, st, at):

            if renpy.get_screen("confirm"):
                return renpy.Render(0, 0)

            surf = renpy.render(self.child, width, height, st, at)
            w, h = surf.get_size()

            self.w = w
            self.h = h

            rv = renpy.Render(w, h)
            rv.blit(surf, (0, 0))

            return rv

        def event(self, ev, x, y, st):

            if renpy.get_screen("confirm"):
                return

            rv = self.child.event(ev, x, y, st)
            if rv is not None:
                return rv

            if state.mode != "lines":

                if renpy.map_event(ev, "rollback") or renpy.map_event(ev, "rollforward"):
                    raise renpy.IgnoreEvent()

                raise renpy.display.layout.IgnoreLayers()

            if (0 <= x < self.w) and (0 <= y < self.h):
                raise renpy.display.layout.IgnoreLayers()

            return None

        def get_placement(self):
            return self.child.get_placement()

        def visit(self):
            return [ self.child ]

    # Sort #####################################################################

    import re

    def component_key(s):
        """
        Sorts l in a way that groups numbers together and treats them as
        numbers (so c10 comes after c9, not c1.)
        """
        rv = [ ]

        for i, v in enumerate(re.split(r'(\d+)', s.lower())):
            if not v:
                continue

            if i & 1:
                v = int(v)

            rv.append(v)

        return tuple(rv)

init 1101 python hide in director:

    if state.active:

        for name, file, _line in renpy.dump.transforms:
            if file.startswith("renpy/common/"):
                continue

            if file == "game/screens.rpy":
                continue

            transforms.append(name)
            transforms.sort(key=component_key)

        import fnmatch

        for c in audio_channels + [ voice_channel ]:

            patterns = audio_channel_patterns.get(c, audio_patterns)

            if c not in audio_files:
                audio_files[c] = [ ]

            for fn in renpy.list_files():

                for p in patterns:
                    if fnmatch.fnmatch(fn, p):
                        break
                else:
                    continue

                audio_files[c].append(fn)

            audio_files[c].sort()



# Styles and screens ###########################################################

style director_frame is _frame:
    xfill True
    yfill False
    background "#d0d0d0d0"
    ypadding 0
    yalign 0.0

style director_text is _text:
    size 18

style director_label

style director_label_text is director_text:
    bold True

style director_button is empty

style director_button_text is director_text:
    color "#405060"
    hover_color "#048"
    insensitive_color "#00000020"
    selected_color "#0099cc"


style director_edit_button is director_button:
    xsize 18

style director_edit_button_text is director_button_text:
    font "DejaVuSans.ttf"
    xalign 0.5

style director_action_button is director_button

style director_action_button_text is director_button_text:
    size 26

style director_statement_text is director_text:
    size 20

style director_statement_button is director_button

style director_statement_button_text is director_button_text:
    size 20

style director_vscrollbar is _vscrollbar


screen director_lines(state):

    frame:
        style "empty"
        background Solid("#fff8", xsize=20, xpos=gui._scale(300))

        has vbox:
            xfill True

        viewport:
            scrollbars "vertical"
            ymaximum director.viewport_height
            mousewheel True
            yinitial 1.0
            viewport_yfill False

            has vbox:
                xfill True

            for line_pos, line_text, add_action, change_action in state.lines:

                fixed:
                    yfit True
                    textbutton "+":
                        xpos gui._scale(300)
                        action add_action
                        style "director_edit_button"
                        alt ("add before " + line_text)

                fixed:
                    yfit True

                    text "[line_pos]":
                        xpos (gui._scale(300) - 10)
                        xalign 1.0
                        text_align 1.0
                        style "director_text"

                    if change_action:
                        textbutton "✎":
                            action change_action
                            xpos gui._scale(300)
                            style "director_edit_button"
                            alt ("change " + line_text)

                    frame:
                        style "empty"
                        left_padding (gui._scale(300) + 30)

                        text "[line_text!q]" style "director_text"

        null height 14

        hbox:
            xpos (gui._scale(300) + 30)
            yalign 1.0

            textbutton _("Done"):
                action director.Stop()
                style "director_action_button"



screen director_statement(state):

    $ kind = state.kind or __("(statement)")
    $ tag = state.tag or __("(tag)")
    $ attributes =  " ".join(director.get_ordered_attributes()) or __("(attributes)")
    $ transforms = ", ".join(state.transforms) or __("(transform)")
    $ behind = ", ".join(state.behind) or __("(tag)")
    $ behind_tags = director.get_behind_tags(state.tag)

    hbox:
        style_prefix "director_statement"

        textbutton "[kind] " action SetField(state, "mode", "kind")
        textbutton "[tag] " action SetField(state, "mode", "tag")

        if state.attributes or state.kind in { "scene", "show"}:
            textbutton "[attributes] " action SetField(state, "mode", "attributes")

        if state.transforms or state.kind in { "scene", "show"}:
            text "at "
            textbutton "[transforms] " action SetField(state, "mode", "transform")

        if behind_tags and (state.kind in { "show" }):
            text "behind "
            textbutton "[behind]" action SetField(state, "mode", "behind")

    null height 14

screen director_with_statement(state):

    $ transition = state.transition or __("(transition)")

    hbox:
        style_prefix "director_statement"

        text "with "
        textbutton "[transition]" action SetField(state, "mode", "with")

    null height 14

screen director_audio_statement(state):

    $ channel = state.channel or __("(channel)")
    $ audio = director.quote_audio() or __("(filename)")

    hbox:
        style_prefix "director_statement"

        textbutton "[state.kind] " action SetField(state, "mode", "kind")

        if state.kind != "voice":
            textbutton "[channel] " action SetField(state, "mode", "channel")

        if state.kind != "stop":
            textbutton "[audio!q]" action SetField(state, "mode", "audio")

    null height 14

screen director_footer(state):

    null height 14

    hbox:
        style_prefix "director_action"

        spacing 26

        if state.change:
            textbutton _("Change") action director.Commit()
        else:
            textbutton _("Add") action director.Commit()


        textbutton _("Cancel") action director.Cancel()

        if state.change:
            textbutton _("Remove") action director.Remove()


# Formats the choices.
screen director_choices(title):

    text title size 20

    frame:
        style "empty"
        left_margin 10

        viewport:
            scrollbars "vertical"
            ymaximum director.viewport_height
            mousewheel True
            yinitial 0.0
            viewport_yfill False

            hbox:
                box_wrap True
                spacing 20

                transclude


screen director_kind(state):

    vbox:
        xfill True

        use director_statement(state)

        use director_choices(_("Statement:")):

            textbutton "scene" action director.SetKind("scene")
            textbutton "show" action director.SetKind("show")
            textbutton "hide" action director.SetKind("hide")
            textbutton "with" action director.SetKind("with")
            textbutton "play" action director.SetKind("play")
            textbutton "queue" action director.SetKind("queue")
            textbutton "stop" action director.SetKind("stop")
            textbutton "voice" action director.SetKind("voice")

        use director_footer(state)


screen director_tag(state):

    vbox:
        xfill True

        use director_statement(state)

        use director_choices(_("Tag:")):

            for t in director.get_tags():
                textbutton "[t]":
                    action director.SetTag(t)

        use director_footer(state)


screen director_attributes(state):

    vbox:
        xfill True

        use director_statement(state)

        use director_choices(_("Attributes:")):

            for t in director.get_attributes():
                textbutton "[t]":
                    action director.ToggleAttribute(t)
                    style "director_button"
                    ypadding 0

        use director_footer(state)


screen director_transform(state):

    vbox:
        xfill True

        use director_statement(state)

        use director_choices(_("Transforms:")):

            for t in director.get_transforms():
                textbutton "[t]":
                    action director.SetList(state.transforms, t)
                    alternate director.ToggleList(state.transforms, t)
                    style "director_button"
                    ypadding 0

        use director_footer(state)


screen director_behind(state):

    vbox:
        xfill True

        use director_statement(state)

        use director_choices(_("Behind:")):

            for t in director.get_behind_tags(state.tag):
                textbutton "[t]":
                    action director.SetList(state.behind, t)
                    alternate director.ToggleList(state.behind, t)
                    style "director_button"
                    ypadding 0

        use director_footer(state)


screen director_with(state):

    vbox:
        xfill True

        use director_with_statement(state)

        use director_choices(_("Transition:")):

            for t in director.transitions:
                textbutton "[t]":
                    action director.SetTransition(t)
                    style "director_button"
                    ypadding 0

        use director_footer(state)


screen director_channel(state):

    vbox:
        xfill True

        use director_audio_statement(state)

        use director_choices(_("Channel:")):

            for c in director.audio_channels:
                textbutton "[c]":
                    action director.SetChannel(c)
                    style "director_button"
                    ypadding 0

        use director_footer(state)


screen director_audio(state):

    vbox:
        xfill True

        use director_audio_statement(state)

        use director_choices(_("Audio Filename:")):

            for fn in director.audio_files.get(state.channel, [ ]):
                $ elided_fn = director.audio_filename_to_display(state.channel, fn)

                textbutton "[elided_fn]":
                    action director.SetAudio(fn)
                    style "director_button"
                    ypadding 0

        use director_footer(state)



screen director():
    zorder 1400

    $ state = director.state

    if renpy.loadable("id/" + state.mode + ".png"):
        add ("id/" + state.mode + ".png")

    frame:
        style_prefix "director"
        xpadding ( 0 if state.mode == "lines" else gui._scale(20) )

        at director.SemiModal

        has fixed:
            fit_first True

        if state.mode == "lines":
            use director_lines(state)
        elif state.mode == "kind":
            use director_kind(state)
        elif state.mode == "tag":
            use director_tag(state)
        elif state.mode == "attributes":
            use director_attributes(state)
        elif state.mode == "transform":
            use director_transform(state)
        elif state.mode == "behind":
            use director_behind(state)
        elif state.mode == "with":
            use director_with(state)
        elif state.mode == "channel":
            use director_channel(state)
        elif state.mode == "audio":
            use director_audio(state)

    if state.mode == "lines":
        key "director" action director.Stop()
