#!/usr/bin/env python2

# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
# Copyright (C) 2008-2018 NIWA & British Crown (Met Office) & Contributors.
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

"""1/ cylc [prep] graph [OPTIONS] SUITE [START[STOP]]
     Plot the suite.rc dependency graph for SUITE.
       2/ cylc [prep] graph [OPTIONS] -f,--file FILE
     Plot the specified dot-language graph file.
       3/ cylc [prep] graph [OPTIONS] --reference SUITE [START[STOP]]
     Print out a reference format for the dependencies in SUITE.
       4/ cylc [prep] graph [OPTIONS] --output-file FILE SUITE
     Plot SUITE dependencies to a file FILE with a extension-derived format.
     If FILE endswith ".png", output in PNG format, etc.

Plot suite dependency graphs in an interactive graph viewer.

If START is given it overrides "[visualization] initial cycle point" to
determine the start point of the graph, which defaults to the suite initial
cycle point. If STOP is given it overrides "[visualization] final cycle point"
to determine the end point of the graph, which defaults to the graph start
point plus "[visualization] number of cycle points" (which defaults to 3).
The graph start and end points are adjusted up and down to the suite initial
and final cycle points, respectively, if necessary.

The "Save" button generates an image of the current view, of format (e.g. png,
svg, jpg, eps) determined by the filename extension. If the chosen format is
not available a dialog box will show those that are available.

If the optional output filename is specified, the viewer will not open and a
graph will be written directly to the file.

GRAPH VIEWER CONTROLS:
    * Center on a node: left-click.
    * Pan view: left-drag.
    * Zoom: +/- buttons, mouse-wheel, or ctrl-left-drag.
    * Box zoom: shift-left-drag.
    * "Best Fit" and "Normal Size" buttons.
    * Left-to-right graphing mode toggle button.
    * "Ignore suicide triggers" button.
    * "Save" button: save an image of the view.
  Family (namespace) grouping controls:
    Toolbar:
    * "group" - group all families up to root.
    * "ungroup" - recursively ungroup all families.
    Right-click menu:
    * "group" - close this node's parent family.
    * "ungroup" - open this family node.
    * "recursive ungroup" - ungroup all families below this node."""

import sys
import StringIO

import cylc.flags
from cylc.remote import remrun
from cylc.suite_srv_files_mgr import SuiteSrvFilesManager
from cylc.task_id import TaskID
from cylc.templatevars import load_template_vars

if remrun(forward_x11=True):
    sys.exit(0)

from cylc.option_parsers import CylcOptionParser as COP

# DEVELOPER NOTE: family grouping controls via the viewer toolbar and
# right-click menu have been rather hastily stuck on to the original
# viewer, via changes to this file and to lib/cylc/cylc_xdot.py - all
# of which could stand some refactoring to streamline the code a bit.

# TODO - clarify what it means to choose visualization boundaries (by CLI
# or in-suite) outside of the defined suite initial and final cycle points.


def on_url_clicked(_, task_id, event, window):
    """Display right click menu for a graph node."""
    if event.button != 3:
        return False
    # URL is node ID
    name = TaskID.split(task_id)[0]

    import gtk
    menu = gtk.Menu()
    menu_root = gtk.MenuItem(task_id)
    menu_root.set_submenu(menu)

    group_item = gtk.MenuItem('Group')
    group_item.connect('activate', grouping, name, window, False, False)
    ungroup_item = gtk.MenuItem('UnGroup')
    ungroup_item.connect('activate', grouping, name, window, True, False)
    ungroup_rec_item = gtk.MenuItem('Recursive UnGroup')
    ungroup_rec_item.connect('activate', grouping, name, window, True, True)

    title_item = gtk.MenuItem(task_id)
    title_item.set_sensitive(False)
    menu.append(title_item)

    menu.append(gtk.SeparatorMenuItem())

    menu.append(group_item)
    menu.append(ungroup_item)
    menu.append(ungroup_rec_item)

    menu.show_all()
    menu.popup(None, None, None, event.button, event.time)

    # TODO - popup menus are not automatically destroyed and can be
    # reused if saved; however, we need to reconstruct or at least
    # alter ours dynamically => should destroy after each use to
    # prevent a memory leak? But I'm not sure how to do this as yet.)

    return True


def grouping(_, name, window, ungroup, recursive):
    """Control graph grouping."""
    if ungroup:
        window.get_graph(ungroup_nodes=[name], ungroup_recursive=recursive)
    else:
        window.get_graph(group_nodes=[name])


def main():
    """CLI."""
    parser = COP(
        __doc__, jset=True, prep=True,
        argdoc=[
            ('[SUITE]', 'Suite name or path'),
            ('[START]', 'Initial cycle point '
             '(default: suite initial point)'),
            ('[STOP]', 'Final cycle point '
             '(default: initial + 3 points)')])

    parser.add_option(
        "-u", "--ungrouped",
        help="Start with task families ungrouped (the default is grouped).",
        action="store_true", default=False, dest="start_ungrouped")

    parser.add_option(
        "-n", "--namespaces",
        help="Plot the suite namespace inheritance hierarchy "
             "(task run time properties).",
        action="store_true", default=False, dest="namespaces")

    parser.add_option(
        "-f", "--file",
        help="View a specific dot-language graphfile.",
        metavar="FILE", action="store", default=None, dest="filename")

    parser.add_option(
        "--filter", help="Filter out one or many nodes.",
        metavar="NODE_NAME_PATTERN", action="append", dest="filter_patterns")

    parser.add_option(
        "-O", "--output-file",
        help="Output to a specific file, with a format given by "
             "--output-format or extrapolated from the extension. "
             "'-' implies stdout in plain format.",
        metavar="FILE", action="store", default=None, dest="output_filename")

    parser.add_option(
        "--output-format",
        help="Specify a format for writing out the graph to --output-file "
             "e.g. png, svg, jpg, eps, dot. 'ref' is a special sorted plain "
             "text format for comparison and reference purposes.",
        metavar="FORMAT", action="store", default=None, dest="output_format")

    parser.add_option(
        "-r", "--reference",
        help="Output in a sorted plain text format for comparison purposes. "
             "If not given, assume --output-file=-.",
        action="store_true", default=False, dest="reference")

    parser.add_option(
        "--show-suicide",
        help="Show suicide triggers.  They are not shown by default, unless "
             "toggled on with the tool bar button.",
        action="store_true", default=False, dest="show_suicide")

    (options, args) = parser.parse_args()

    try:
        import gtk
        import gobject
        from xdot import DotWindow
        from cylc.cylc_xdot import (
            MyDotWindow, MyDotWindow2, get_reference_from_plain_format)
    except (ImportError, RuntimeError) as error:
        # Allow command help generation without a graphical environment.
        sys.exit('ERROR: no X environment? %s' % error)

    if options.filename:
        if len(args) != 0:
            parser.error(
                "file graphing arguments: '-f FILE' or '--file=FILE'")
            sys.exit(1)
        if options.output_filename:
            sys.exit("ERROR: output-file not supported for dot files."
                     " Use 'dot' command instead.")
        window = DotWindow()
        window.update(options.filename)
        window.connect('destroy', gtk.main_quit)
        # checking periodically for file changed
        gobject.timeout_add(1000, window.update, options.filename)
        gtk.main()
        sys.exit(0)

    if not args:
        parser.error('Argument 1 SUITE is required except for usage 2.')
    suite, suiterc = SuiteSrvFilesManager().parse_suite_arg(options, args[0])

    start_point_string = stop_point_string = None
    if len(args) >= 2:
        start_point_string = args[1]
    if len(args) == 3:
        stop_point_string = args[2]

    interactive = not (options.reference or options.output_filename)
    template_vars = load_template_vars(
        options.templatevars, options.templatevars_file)
    should_hide_gtk_window = (options.output_filename is not None)
    if options.namespaces:
        window = MyDotWindow2(suite, suiterc, template_vars,
                              should_hide=should_hide_gtk_window,
                              interactive=interactive)
    else:
        hide_suicide = not options.show_suicide
        window = MyDotWindow(suite, suiterc, template_vars,
                             start_point_string, stop_point_string,
                             should_hide=should_hide_gtk_window,
                             ignore_suicide=hide_suicide,
                             interactive=interactive)

    if options.start_ungrouped:
        window.ungroup_all(None)

    window.widget.connect('clicked', on_url_clicked, window)
    if options.filter_patterns:
        window.set_filter_graph_patterns(options.filter_patterns)
    window.get_graph()

    if options.reference and options.output_filename is None:
        options.output_filename = "-"

    if options.output_filename:
        if (options.reference or options.output_filename.endswith(".ref") or
                options.output_format == "ref"):
            dest = StringIO.StringIO()
            window.graph.draw(dest, format="plain", prog="dot")
            output_text = get_reference_from_plain_format(dest.getvalue())
            if options.output_filename == "-":
                sys.stdout.write(output_text)
            else:
                with open(options.output_filename, 'w') as handle:
                    handle.write(output_text)
        else:
            if options.output_filename == "-":
                window.graph.draw(sys.stdout, format="plain", prog="dot")
            elif options.output_format:
                window.graph.draw(
                    options.output_filename, format=options.output_format,
                    prog="dot"
                )
            else:
                window.graph.draw(options.output_filename, prog="dot")
        sys.exit(0)

    window.connect('destroy', gtk.main_quit)
    gtk.main()


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:
        if cylc.flags.debug:
            raise
        sys.exit(str(exc))
