#!/usr/bin/env python

# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
# Copyright (C) 2008-2016 NIWA
#
# 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.task_id import TaskID
from cylc.templatevars import load_template_vars

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

if remrun().execute():
    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 times.


def on_url_clicked(widget, url, event, window):
    if event.button != 3:
        return False
    # URL is node ID
    right_click_menu(event, url, type='live task', window=window)


def right_click_menu(event, task_id, type='live task', window=None):
    name, point_string = TaskID.split(task_id)

    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(w, name, window, un, recursive):
    if not un:
        window.get_graph(group_nodes=[name])
    else:
        if recursive:
            window.get_graph(ungroup_nodes=[name], ungroup_recursive=True)
        else:
            window.get_graph(ungroup_nodes=[name], ungroup_recursive=False)


def main():
    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()

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

    should_hide_gtk_window = (options.output_filename is not None)

    suite, suiterc = parser.get_suite()

    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]

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

    if options.start_ungrouped:
        window.ungroup_all(None)

    window.widget.connect('clicked', on_url_clicked, window)
    if options.filter_patterns:
        filter_patterns = set(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:
                open(options.output_filename).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))
