#!/usr/bin/env python
""" suite builder example for 2013 ecFlow course """
# file deepcode ignore missing~close~open: legacy ignore
import sys, os

sys.path.append("/home/ma/emos/def/ecflow")

import ecf as ec
from ecf import *
import inc_emos as ie  # provides Seed class + system related dependencies

# cd ~map/course/201303/ecflow; python course.py # task wrappers underneath
# consume: choices for a family matching the producer-consumer pattern
#   local family + remote family + BEWARE ECF_OUT + log-server example
# barber:  an example of a "dynamical suite", with a "family producer task"
# perl + python: example that task wrapper may not be ksh/bash scripts

import time
from datetime import date
import argparse

today = str(date.today()).replace("-", "")

############################################################################


class GenericFamily(object):
    """provide structure for derived classes"""

    def make(self, node):
        return node

    def main(self, node):
        return BaseException  # return node

    def arch(self, node):
        return node


############################################################################


class NativePerl(GenericFamily):
    """ksh is not the only language for task wrappers"""

    def __init__(self):
        self.name = "perl"

    def main(self, node):
        tsk = Task(self.name).add(
            Variables(ECF_MICRO="^", ECF_JOB_CMD="^ECF_JOB^ > ^ECF_JOBOUT^ 2>&1"),
            Meter("step", -1, 100),
            Event("1"),
            Event("2"),
            Label("info", "none"),
        )
        node.add(tsk)


############################################################################


class NativePython(NativePerl):
    """ksh is not the only language for task wrappers"""

    def __init__(self):
        super(NativePerl, self).__init__()
        self.name = "python"


############################################################################


def _kind(prod=1, cons=1):
    return Variables(CONSUME=cons, PRODUCE=prod)


def _evt():
    return (Event("p"), Event("c"))


def _leaf(name="produce", init=0, stop=100, step=1):
    add = None
    if type(stop) == int:
        add = Meter("step", -1, int(stop))
    return Task("%s" % name).add(
        _evt(),
        add,
        Variables(INIT=init, STOP=stop, STEP=step),
    )


############################################################################
class FallBack(GenericFamily):
    """in some situation, user may want its family to continue, and
    repeat to increment, even when some tasks abort"""

    def main(self, node=None):
        return (
            Family("daily")
            .repeat(name="YMD", start=today, end=DATE_STOP)
            .add(
                Task("action").add(Time("10:00")),
                Family("loop").add(
                    Time("11:00"),
                    Task("dummy").add(TriggerImpossible(), Complete("1==1")),
                ),
                Task("fallback").add(
                    Label("info", "force complete when action is aborted"),
                    Time("10:55"),
                    Trigger("action eq aborted"),
                    Complete("action eq complete"),
                ),
            )
        )


class DailyInc(GenericFamily):
    """another method to have daily repeat increment, with aborted tasks"""

    def main(self, node=None):
        return (
            Family("daily_inc")
            .repeat(name="YMD", start=today, end=DATE_STOP)
            .add(
                Label("info", "requeue will reset repeat attribute!"),
                Complete("daily_inc/loop/dummy eq complete"),
                Task("action").add(Time("10:00")),
                Family("loop").add(
                    Time("11:00"),
                    Task("dummy").add(TriggerImpossible(), Complete("1==1")),
                ),
            )
        )


class Consume(GenericFamily):
    """producer-consumer pattern can be implemented in many ways"""

    def __init__(self):
        self.init = 0
        self.stop = 48
        self.step = 3

    def main(self, node):
        """pass the parent node, so that absolute paths can be used
        with triggers"""
        path = node.fullname()

        top = node.family("consume").add(
            Variables(SLEEP=10, PRODUCE=1, CONSUME=1),  # default: tasks will do both
            Family("limits").add(Defcomplete(), Limit("consume", 7)),
            Task("leader").add(
                Label("info", "set event to get produce1 leader"),
                Event("1"),  # set/cleared by user
                Defcomplete(),
            ),
            # task does both, ie serial ###########################
            _leaf("produce", self.init, self.stop, self.step).add(
                Label("info", "both produce and consume in a task")
            ),
            # meter will report about producer progress ###########
            Family("produce0").add(
                Label("info", "only produce"),
                _kind(1, 0),
                _leaf("produce", self.init, self.stop, self.step),
            ),
            # serially produced, create a new task for each step ###
            Family("produce1")
            .add(
                _kind(1, 0),
                Label("info", "repeat, one job per step"),
                _leaf("produce", init="%STEP%", stop="%STEP%", step=1).add(
                    Meter("step", -1, 100)
                ),
            )
            .repeat(
                kind="integer",
                name="STEP",
                start=self.init,
                end=self.stop,
                step=self.step,
            )
            .add(),
        )

        top.defstatus("suspended")
        fam = Family("produce2").add(  # parallel
            _kind(1, 0),
            Label("info", "limited, one task per step, step by 3"),
            Limit("prod", 5),
            InLimit("produce2:prod"),
        )
        top.add(fam)
        for step in xrange(self.init, self.stop, self.step):
            fam.add(
                Family("%02d" % step).add(
                    Variables(STEP=step), _leaf("produce", step, step, 1)
                )
            )

        ######################
        lead = path + "/consume/leader:1"
        prod = path + "/consume/produce"

        top.add(  ### trigger may be inside a task
            _leaf("consume", self.init, self.stop, self.step).add(
                Label("info", "trigger may be inside a task"),
                _kind(0, 1),
                InLimit("limits:consume"),
                Variables(
                    CALL_WAITER=1,
                    SLEEP=3,  # sleep less than producer
                    TRIGGER_EXPRESSION=prod
                    + ":step ge $step or "
                    + prod
                    + " eq complete",
                ),
            ),
            Family("consume1")
            .add(
                Label("info", "explicit trigger, follow faster"),
                _kind(0, 1),
                Trigger(
                    "(consume1:STEP lt %s1:STEP and %s) or " % (prod, lead)
                    + "(consume1:STEP lt %s0/produce:step and not %s) or "
                    % (prod, lead)
                    + "(%s1 eq complete and %s0 eq complete)"  # lt while both are repeat
                    % (prod, prod)
                ),
                InLimit("limits:consume"),
                _leaf("consume", "%STEP%", "%STEP%", 1),
            )
            .repeat(
                kind="integer",
                name="STEP",
                start=self.init,
                end=self.stop,
                step=self.step,
            ),
        )

        fam = Family("consume2").add(  # parallel
            Label("info", "one task per step, step by three"),
            _kind(0, 1),
            Limit("consume", 5),
            InLimit("consume2:consume"),
        )
        top.add(fam)
        for step in xrange(self.init, self.stop, self.step):
            fam.add(
                Family("%02d" % step).add(
                    Variables(STEP=step),
                    Trigger(
                        "(%02d:STEP le %s1:STEP and %s) or " % (step, prod, lead)
                        + "(%02d:STEP le %s0/produce:step and not %s)"
                        % (step, prod, lead)
                    ),
                    _leaf("consume", init=step, stop=step, step=1),
                )
            )


############################################################################


class Barber(GenericFamily):
    """a 'barber shop' example with families created by a task"""

    def _passer_by(self):
        """generator"""
        return Task("passby").add(
            Time("00:00 23:59 00:05"),
            Variables(ID=0),
            Label("info", ""),
            Label(
                "rem",
                "this task alters its variable ID, " + "aliases won't work natively",
            ),
            InLimit("limits:passby"),
        )

    def _client(self, node, position):
        """python version of the family created initially
        attention: raw definition file is located in passby task wrapper"""
        path = node.fullname() + "/limits"
        fam = (
            node.family("list")
            .family("%s" % position)
            .add(
                AutoCancel(1),
                Task("cut").inlimit(path + ":barbers"),
                Task("pay").add(
                    Trigger("cut eq complete"),
                    InLimit(path + ":barbers"),
                    InLimit(path + ":cashiers"),
                ),
                Task("leave").add(Label("info", ""), Trigger(["cut", "pay"])),
            )
        )

        fam.defstatus("complete")

    def _shop(self, node):
        fam = (
            node.family("shop")
            .defstatus("suspended")
            .add(
                Variables(NB_CHAIRS=4),
                Family("limits").add(
                    Defcomplete(),
                    Limit("passby", 1),
                    Limit("barbers", 2),
                    Limit("cashiers", 1),
                ),
                self._passer_by(),
            )
        )
        self._client(fam, 1),

    def main(self, node):
        self._shop(node)


############################################################################
def user():
    return os.getlogin()


def locate_scripts():
    pwd = os.getcwd()

    return Variables(
        ECF_HOME="/tmp/%s/ecflow/" % user(),  # pwd,
        ECF_FILES=pwd + "/scripts",
        ECF_INCLUDE=pwd + "/include",
    )


DATE_STOP = 20300115


class Course(ie.Seed):
    """host families together"""

    def __init__(self):
        super(Course, self).__init__()
        self.name = "course"

    def suite(self):
        """define limits (empty)"""

        node = Suite(user())
        node.defstatus("suspended").add(Variables(USER=user()), locate_scripts())  #
        self.top(node)

        fp = open("/tmp/%s/" % user() + self.name + ".def", "w")
        print >> fp, node
        return node

    def top(self, node):
        barber_shop = Barber()
        perl = NativePerl()
        python = NativePython()
        consume = Consume()

        with node.family(self.name) as node:
            node.add(FallBack().main(), DailyInc().main())
            barber_shop.main(node)
            perl.main(node)
            python.main(node)
            consume.main(node)
            return node


###############################################################################


class Admin(Course):
    """host newlog task + logsvr start/stop/check task
    -- server logs can be renewed with an ecFlowUI menu command also
    """

    def __init__(self):
        self.name = "admin"

    def top(self, node):
        with node.family("admin") as node:
            node.add(self.main()).repeat(name="YMD", start=today, end=DATE_STOP)
            node.defstatus("suspended")
            return node

    def main(self):
        """return self contained Family/Task, without absolute node references
        or with relative path triggers"""
        remote_submit = "rsh -l %USER% %HOST% %ECF_JOB% > %ECF_JOBOUT% 2>&1"
        logpath = "/home/ma/map/course/201303/ecflow"
        return (
            Task("newlog").add(Label("info", "renew server log-file"), Time("08:00")),
            Task("logsvr").add(
                Defcomplete(),
                Variables(
                    HOST="pikachu",
                    ECF_LOGPORT=9316,
                    ECF_LOGPATH=logpath,
                    ECF_LOGMAP=logpath + ":" + logpath,
                    ECF_JOB_CMD=remote_submit,
                ),
                Label("info", "(re)start the logsvr on HOST"),
                Time("08:00"),
            ),
            Family("loop").add(
                Time("08:30"),
                Family("dummy").add(Complete("1==1")),  # TriggerImpossible(),
            ),
        )


###############################################################################


class EcEvents(Admin):
    """connecting to third party software as event generator to update
    a suite variable, and enable daily family run
    """

    def top(self, node):
        node = node.family("ecevents")
        node.add(
            Label("info", "use web... menu"),
            Defcomplete(),
            Variables(
                URL="http://eccmd.ecmwf.int:8090/#Mainpanel",
                ECF_URL_CMD="${BROWSER:=firefox} -remote 'openURL(%URL%",
            ),
        )
        self.main(node)
        return node

    def main(self, node):
        for mode in ["list", "register", "delete", "register_all", "delete_all"]:
            added = None
            if "list" in mode:
                added = Label("regs", "")

            fam = Family(mode).add(
                Variables(MODE=mode), Task("ecjobs").add(Label("info", "none"), added)
            )
            node.add(fam)
            if "_all" in mode:
                fam.add(Variables(EVENT="_all_"))
            elif mode in ("register", "delete"):
                fam.add(
                    Variables(EVENT="an12h000"), Label("info", "update EVENT variable")
                )

        event = "an00h000"
        node.family("ref").defstatus("complete").add(
            Task(event).add(Variables(YMD=today), Label("YMD", today))
        )

        node.family("user").family(event).repeat(
            name="YMD", start=today, end=DATE_STOP
        ).add(
            Label(
                "info",
                "extern cannot be used anymore for " + "intra-suite reference triggers",
            ),
            Variables(SLEEP=1),
            Trigger(event + ":YMD le %s/ref/%s:YMD" % (node.fullname(), event)),
            _leaf("consume"),
        )
        return node


###############################################################################


class SerialTask(object):
    """add trigger on the previous task"""

    def __init__(self):
        self.prev = None

    def add(self, name):
        fam = Family(name).add(Variables(MODE=name), Task("to_ecflow"))
        if self.prev is not None:
            fam.add(Trigger("./%s eq complete" % self.prev))
        self.prev = name
        return fam


class Reload(Admin):
    """a simple task to download SMS content and translate it to ecFlow"""

    def top(self, node):
        node = node.family("reload")
        node.add(
            Label("info", "from sms to ecFlow"),
            Defcomplete(),
            Variables(
                URL="https://confluence.ecmwf.int/display/ECFLOW/ecflow+home",
                ECF_URL_CMD="${BROWSER:=firefox} -remote 'openURL(%URL%",
            ),
            self.main(),
        )
        return node

    def main(self, node=None):
        fam = Family("reload")
        serial = SerialTask()
        fam.add(
            serial.add("get").add(
                Variables(
                    SMS_SUITE="eras",
                    # SMS_PROG= 314159, SMS_NODE= "localhost",
                    # SMS_PROG= 314199, SMS_NODE= "marhaus", # eras
                    SMS_PROG=314197,
                    SMS_NODE="marhaus",  # eras2
                ),
            ),
            serial.add("translate"),
            serial.add("edit"),
            serial.add("mail"),
            serial.add("load"),
            serial.add("bench").add(Defcomplete()),
        )
        if node is not None:
            return node.add(fam)
        return fam


############################################################################

if __name__ == "__main__":
    import cli_proc, sys

    parser = argparse.ArgumentParser(
        description=DESC, formatter_class=argparse.RawDescriptionHelpFormatter
    )

    parser.add_argument("--host", default="localhost", help="server hostname")
    parser.add_argument("--port", default="3141", help="server port number")

    if len(sys.argv) > 1:
        suites = {
            "course": Course(),
        }
        argv = cli_proc.process(suites, compare=False)
        sys.exit(0)
    args = parser.parse_args()

    clt = ec.Client(args.host, args.port)
    try:
        clt.ping()
    except:
        clt = ec.Client("localhost", 1000 + os.geteuid())
    try:
        clt.ping()
    except:
        clt = ec.Client("localhost", 31415)
    try:
        clt.ping()
    except:
        clt = ec.Client("localhost", 3199)
    defs = ec.ecflow.Defs()

    course = Course()
    suite = course.suite().add(Reload().main())

    if 1:  # enable/disable remote version of 'consume'
        rem = suite.family("remote").add(
            Task("mk_remote").add(
                Defcomplete(),
                Label(
                    "info",
                    "do not forget to create directory structure"
                    + "on remote host for job output creation",
                ),
                ie.onws(host="class01"),
            ),
            Variables(ECF_HOME="/tmp/%s/ecflow" % user()),
            ie.onws(host="class02"),
        )
        course.top(rem)

    # print clt.stats()

    Admin().top(suite)

    EcEvents().top(suite)

    defs.add_suite(suite)

    path = "/%s" % user()
    # path = "/%s/course" % user()
    # path = "/%s/admin" % user()
    # path = "/%s/ecevents" % user()
    # print defs

    clt.replace(path, defs)

    sys.exit(0)
