#!/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/>.

"""cylc [control] broadcast|bcast [OPTIONS] REG

Override [runtime] config in targeted namespaces in a running suite.

Uses for broadcast include making temporary changes to task behaviour,
and task-to-downstream-task communication via environment variables.

A broadcast can target any [runtime] namespace for all cycles or for a
specific cycle.  If a task is affected by specific-cycle and all-cycle
broadcasts at once, the specific takes precedence. If a task is affected
by broadcasts to multiple ancestor namespaces, the result is determined
by normal [runtime] inheritance. In other words, it follows this order:

all:root -> all:FAM -> all:task -> tag:root -> tag:FAM -> tag:task

Broadcasts persist, even across suite restarts, until they expire when
their target cycle point is older than the oldest current in the suite,
or until they are explicitly cancelled with this command.  All-cycle
broadcasts do not expire.

For each task the final effect of all broadcasts to all namespaces is
computed on the fly just prior to job submission.  The --cancel and
--clear options simply cancel (remove) active broadcasts, they do not
act directly on the final task-level result. Consequently, for example,
you cannot broadcast to "all cycles except Tn" with an all-cycle
broadcast followed by a cancel to Tn (there is no direct broadcast to Tn
to cancel); and you cannot broadcast to "all members of FAMILY except
member_n" with a general broadcast to FAMILY followed by a cancel to
member_n (there is no direct broadcast to member_n to cancel).

To broadcast a variable to all tasks (quote items with internal spaces):
  % cylc broadcast -s "[environment]VERSE = the quick brown fox" REG
To cancel the same broadcast:
  % cylc broadcast --cancel "[environment]VERSE" REG

Use -d/--display to see active broadcasts. Multiple set or cancel
options can be used on the same command line. Broadcast cannot change
[runtime] inheritance.

See also 'cylc reload' - reload a modified suite definition at run time."""

import sys
if '--use-ssh' in sys.argv[1:]:
    sys.argv.remove('--use-ssh')
    from cylc.remote import remrun
    if remrun().execute(force_required=True):
        sys.exit(0)

import os
import re

import cylc.flags
from cylc.broadcast_report import (
    get_broadcast_change_report, get_broadcast_bad_options_report)
from cylc.option_parsers import CylcOptionParser as COP
from cylc.network.suite_broadcast import BroadcastClient
from cylc.print_tree import print_tree
from cylc.task_id import TaskID
from cylc.cfgspec.suite import SPEC, upg
from parsec.validate import validate


def get_padding(settings, level=0, padding=0):
    level += 1
    for key, val in settings.items():
        tmp = level * 2 + len(key)
        if tmp > padding:
            padding = tmp
        if isinstance(val, dict):
            padding = get_padding(val, level, padding)
    return padding


def get_rdict(left, right=None):
    # left is [section]item, or just item
    rdict = {}
    m = re.match('^\[(.*)\](.*)$', left)
    if m:
        # [sect]item = right
        sect, var = m.groups()
        if not var:
            rdict = {sect.strip(): right}
        else:
            rdict = {sect.strip(): {var.strip(): right}}
    else:
        # item = right
        rdict = {left: right}
    return rdict


def main():
    parser = COP(__doc__, pyro=True)

    parser.add_option(
        "-t", "--tag", metavar="CYCLE_POINT",
        help="(Deprecated). "
             "Target cycle point. More than one can be added. "
             "Defaults to '*' for all cycle points with --set and --cancel, "
             "and nothing with --clear.",
        action="append", dest="point_strings", default=[])

    parser.add_option(
        "-p", "--point", metavar="CYCLE_POINT",
        help="Target cycle point. More than one can be added. "
             "Defaults to '*' with --set and --cancel, "
             "and nothing with --clear.",
        action="append", dest="point_strings", default=[])

    parser.add_option(
        "-n", "--namespace", metavar="NAME",
        help="Target namespace. Defaults to 'root' with "
             "--set and --cancel, and nothing with --clear.",
        action="append", dest="namespaces", default=[])

    parser.add_option(
        "-s", "--set", metavar="[SEC]ITEM=VALUE",
        help="A [runtime] config item and value to broadcast.",
        action="append", dest="set", default=[])

    parser.add_option(
        "-c", "--cancel", metavar="[SEC]ITEM",
        help="An item-specific broadcast to cancel.",
        action="append", dest="cancel", default=[])

    parser.add_option(
        "-C", "--clear",
        help="Cancel all broadcasts, or with -p/--point, "
             "-n/--namespace, cancel all broadcasts to targeted "
             "namespaces and/or cycle points. Use \"-C -p '*'\" "
             "to cancel all all-cycle broadcasts without canceling "
             "all specific-cycle broadcasts.",
        action="store_true", dest="clear", default=False)

    parser.add_option(
        "-e", "--expire", metavar="CYCLE_POINT",
        help="Cancel any broadcasts that target cycle "
             "points earlier than, but not inclusive of, CYCLE_POINT.",
        action="store", default=None, dest="expire")

    parser.add_option(
        "-d", "--display",
        help="Display active broadcasts.",
        action="store_true", default=False, dest="show")

    parser.add_option(
        "-k", "--display-task", metavar="TASKID",
        help="Print active broadcasts for a given task "
             "(" + TaskID.SYNTAX + ").",
        action="store", default=None, dest="showtask")

    parser.add_option(
        "-b", "--box",
        help="Use unicode box characters with -d, -k.",
        action="store_true", default=False, dest="unicode")

    parser.add_option(
        "-r", "--raw",
        help="With -d/--display or -k/--display-task, write out "
             "the broadcast config structure in raw Python form.",
        action="store_true", default=False, dest="raw")

    (options, args) = parser.parse_args()
    suite = args[0]

    debug = False
    if cylc.flags.debug:
        debug = True
    else:
        try:
            # from task execution environment
            if os.environ['CYLC_DEBUG'] == 'True':
                debug = True
        except KeyError:
            pass

    pclient = BroadcastClient(
        suite, options.owner, options.host, options.pyro_timeout,
        options.port, options.db, my_uuid=options.set_uuid,
        print_uuid=options.print_uuid)

    if options.show or options.showtask:
        if options.showtask:
            try:
                name, point_string = TaskID.split(options.showtask)
            except ValueError:
                parser.error("TASKID must be " + TaskID.SYNTAX)
        settings = pclient.broadcast('get', options.showtask)
        padding = get_padding(settings) * ' '
        if options.raw:
            print str(settings)
        else:
            print_tree(settings, padding, options.unicode)
        sys.exit(0)

    if options.clear:
        modified_settings, bad_options = pclient.broadcast(
            'clear', options.point_strings, options.namespaces)
        if modified_settings:
            print get_broadcast_change_report(
                modified_settings, is_cancel=True)
        sys.exit(get_broadcast_bad_options_report(bad_options))

    if options.expire:
        modified_settings, bad_options = pclient.broadcast(
            'expire', options.expire)
        if modified_settings:
            print get_broadcast_change_report(
                modified_settings, is_cancel=True)
        sys.exit(get_broadcast_bad_options_report(bad_options))

    # implement namespace and cycle point defaults here
    namespaces = options.namespaces
    if not namespaces:
        namespaces = ["root"]
    point_strings = options.point_strings
    if not point_strings:
        point_strings = ["*"]

    if options.cancel:
        settings = []
        for option_item in options.cancel:
            if "=" in option_item:
                raise ValueError(
                    "ERROR: --cancel=[SEC]ITEM does not take a value")
            option_item = option_item.strip()
            if option_item == "inherit":
                raise ValueError(
                    "ERROR: Inheritance cannot be changed by broadcast")
            setting = get_rdict(option_item)
            upg({'runtime': {'__MANY__': setting}}, 'test')
            validate(setting, SPEC['runtime']['__MANY__'])
            settings.append(setting)
        modified_settings, bad_options = pclient.broadcast(
            'clear', point_strings, namespaces, settings)
        if modified_settings:
            print get_broadcast_change_report(
                modified_settings, is_cancel=True)
        sys.exit(get_broadcast_bad_options_report(bad_options))

    if options.set:
        settings = []
        for option_item in options.set:
            if "=" not in option_item:
                raise ValueError(
                    "ERROR: --set=[SEC]ITEM=VALUE requires a value")
            lhs, rhs = [s.strip() for s in option_item.split("=", 1)]
            if lhs == "inherit":
                raise ValueError(
                    "ERROR: Inheritance cannot be changed by broadcast")
            setting = get_rdict(lhs, rhs)
            upg({'runtime': {'__MANY__': setting}}, 'test')
            validate(setting, SPEC['runtime']['__MANY__'])
            settings.append(setting)
        modified_settings, bad_options = pclient.broadcast(
            'put', point_strings, namespaces, settings)
        print get_broadcast_change_report(modified_settings)
        sys.exit(get_broadcast_bad_options_report(bad_options, is_set=True))


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