class Workflow(models.Model):
    objects = WorkflowManager()

    name = models.CharField(max_length=200, unique=True, verbose_name=ugettext_lazy("Name"))
    object_type = models.CharField(max_length=200, verbose_name=ugettext_lazy("Object_Type"))
    initial_prefetch = models.CharField(max_length=4000, null=True, blank=True,
        verbose_name=ugettext_lazy("Object_Type"))

    @property
    def initial_prefetch_dict(self):
        dict = json.loads(self.initial_prefetch) if self.initial_prefetch else None
        for k, v in dict.items():
            if "today" in v:
                sum_operands = [x.strip() for x in v.split("+")]
                res = None
                for sum_op in sum_operands:
                    sub_operands = [x.strip() for x in sum_op.split("-")]
                    sub_res = None
                    for sub_op in sub_operands:
                        mul_operands = [x.strip() for x in sub_op.split("*")]
                        mul_res = None
                        for mul_op in mul_operands:
                            div_res = None
                            div_operands = [x.strip() for x in mul_op.split("/")]
                            for op in div_operands:
                                if div_res is None:
                                    div_res = datetime.now().date() if op == "today" else float(op)
                                else:
                                    div_res = div_res / float(op)  # you cannot divide by today
                            if mul_res is None:
                                mul_res = div_res
                            else:
                                mul_res = mul_res * div_res
                        # perform multiplications and divisions in float, then convert to timedelta for the rest
                        if isinstance(mul_res, float):
                            mul_res = timedelta(seconds=mul_res)
                        if sub_res is None:
                            sub_res = mul_res
                        else:
                            sub_res = sub_res - mul_res
                    if res is None:
                        res = sub_res
                    else:
                        res = res + sub_res
                res = res.strftime("%Y-%m-%dT%H:%M:%SZ")
                dict.update({k: res})
        return dict

    @property
    def initial_state(self):
        return State.objects.get(workflow=self, initial=True)

    @property
    def initial_transition(self):
        return Transition.objects.filter(workflow=self, initial_state=None, final_state=self.initial_state).first()

    def prefetch_initial_objects(self):
        objects = self \
            .object_class().objects.filter(**self.initial_prefetch_dict) \
            .exclude(id__in=CurrentObjectState.objects.filter(workflow=self).values_list("object_id",
            flat=True)) if self.initial_prefetch else []
        return objects

    def __unicode__(self):
        return self.name

    def is_initial_transition_available(self, *, user, object_id, object_state_id=None, automatic=False):
        if object_state_id:
            return not CurrentObjectState.objects.filter(id=object_state_id, workflow=self).exists()
        else:
            last = CurrentObjectState.objects.filter(object_id=object_id, workflow=self).order_by('-id').first()
            if last and last.state.active:
                return False
            elif self.initial_transition is not None:
                conditions = self.initial_transition.condition_set.all()
                if len(conditions) == 0:
                    if automatic:
                        return self.initial_transition.automatic
                    else:
                        return not self.initial_transition.automatic
                else:
                    root_condition = conditions.first()
                    if root_condition.check_condition(user=user, object_id=object_id, object_state=last):
                        if automatic:
                            return self.initial_transition.automatic
                        else:
                            return not self.initial_transition.automatic
                    else:
                        return False
            else:
                return False

    def natural_key(self):
        return (self.name,)

    def add_object(self, object_id, execute_async=True):
        process = CurrentObjectState.objects.create(object_id=object_id, state=self.initial_state)
        _execute_atomatic_transitions(self.initial_state, object_id, process.id, execute_async=execute_async)
        return process

    def object_class(self):
        return import_from_path(self.object_type)

    def transition_by_name(self, name):
        transition = Transition.objects.filter(workflow=self, name__iexact=name)
        if len(transition) > 0:
            return transition.first()
        return None

    def clone(self, *, name, **defaults) -> ('Workflow', 'Workflow', dict, dict):
        state_map = dict()
        transition_map = dict()
        new_wf, old_wf = clone(self, name=name, **defaults)
        for state in old_wf.state_set.all():
            new_state, old_state = state.clone(workflow=new_wf)
            state_map.update({old_state.id: new_state.id})
        for transition in old_wf.transition_set.all():
            new_transition, old_transition = transition.clone(workflow=new_wf, state_map=state_map)
            transition_map.update({old_transition.id: new_transition.id})

        return new_wf, old_wf, state_map, transition_map class State(models.Model):
    objects = StateManager()

    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"))
    name = models.CharField(max_length=200, verbose_name=ugettext_lazy("Name"))
    active = models.BooleanField(verbose_name=ugettext_lazy("Active"))
    initial = models.BooleanField(default=False, verbose_name=ugettext_lazy("Initial"))

    class Meta:
        unique_together = (('name', 'workflow'),)

    def __unicode__(self):
        return "{}: {}".format(self.workflow.name, self.name)

    @property
    def is_final_state(self):
        return self.outgoing_transitions.count() == 0

    def natural_key(self):
        return (self.name, self.workflow.name)

    def available_transitions(self, user, object_id, automatic=False):
        return [t for t in self.outgoing_transitions.all() if
            t.is_available(user=user, object_id=object_id, automatic=automatic)]

    def clone(self, *, workflow: Workflow, **defaults) -> ('State', 'State'):
        new_state, old_state = clone(self, workflow=workflow, **defaults)
        for variableDef in old_state.variable_definitions.all():
            new_var, _ = clone(variableDef, state=new_state, workflow=workflow, **defaults)
        return new_state, old_state class CurrentObjectState(models.Model):
    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    object_id = models.CharField(max_length=200, verbose_name=ugettext_lazy("Object Id"))
    state = models.ForeignKey(State, on_delete=PROTECT, verbose_name=ugettext_lazy("State"))
    updated_ts = models.DateTimeField(auto_now=True, verbose_name=ugettext_lazy("Last Updated"))

    class Meta:
        indexes = [
            models.Index(fields=['workflow', 'object_id']),
        ]

    def __unicode__(self):
        return "{} in state {} since {}".format(self.object_id, self.state, self.updated_ts)

    def save(self, **qwargs):
        self.workflow = self.state.workflow
        super(CurrentObjectState, self).save(**qwargs) class Transition(models.Model):
    objects = TransitionManager()

    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    name = models.CharField(max_length=50, verbose_name=ugettext_lazy("Name"))
    label = models.CharField(max_length=50, verbose_name=ugettext_lazy("Label"), null=True, blank=True)
    description = models.CharField(max_length=400, null=True, blank=True, verbose_name=ugettext_lazy("Description"))
    initial_state = models.ForeignKey(State, on_delete=SET_NULL, null=True, blank=True,
        verbose_name=ugettext_lazy("Initial State"),
        related_name="outgoing_transitions")
    final_state = models.ForeignKey(State, on_delete=PROTECT, verbose_name=ugettext_lazy("Final State"),
        related_name="incoming_transitions")
    priority = models.IntegerField(null=True, blank=True, verbose_name=ugettext_lazy("Priority"))
    automatic = models.BooleanField(verbose_name=ugettext_lazy("Automatic"))
    automatic_delay = models.FloatField(null=True, blank=True,
        verbose_name=ugettext_lazy("Automatic Delay in Days"))

    class Meta:
        ordering = ["priority"]
        unique_together = (('name', 'final_state'),)

    @property
    def is_initial(self):
        return self.initial_state is None

    def __unicode__(self):
        return "{}{}: {} to {}".format(self.final_state.workflow.name, "(" + self.name + ")" if self.name else "",
            self.initial_state.name if self.initial_state else "START", self.final_state.name)

    def natural_key(self):
        return (self.name, self.workflow.name, self.initial_state.name if self.initial_state else None,
        self.final_state.name)

    def save(self, **qwargs):
        if self.initial_state:
            self.workflow = self.initial_state.workflow
        else:
            self.workflow = self.final_state.workflow
        super(Transition, self).save(**qwargs)

    def is_available(self, *, user, object_id, object_state_id=None, automatic=False, last_transition=None):
        return _is_transition_available(self, user, object_id, object_state_id=object_state_id, automatic=automatic,
            last_transition=last_transition)

    def execute(self, user, object_id, object_state_id=None, execute_async=False, automatic=False):
        if execute_async:
            thr = threading.Thread(target=_execute_transition, args=(self, user, object_id, object_state_id),
                kwargs={"automatic": automatic})
            thr.start()
            return thr
        else:
            return _execute_transition(transition=self, user=user, object_id=object_id,
                object_state_id=object_state_id, automatic=automatic)

    def clone(self, *, workflow: Workflow, state_map: dict, **defaults) -> ('Transition', 'Transition'):
        final_state = State.objects.get(workflow=workflow, id=state_map[self.final_state.id])
        initial_state = None
        if self.initial_state:
            initial_state = State.objects.get(workflow=workflow,
                id=state_map[self.initial_state.id])
        new_transition, old_transition = clone(self, workflow=workflow, initial_state=initial_state, final_state=final_state)
        for condition in old_transition.condition_set.all():
            new_condition, old_condition = condition.clone(workflow=workflow, transition=new_transition)
        for callback in old_transition.callback_set.all():
            new_condition, old_condition = callback.clone(workflow=workflow, transition=new_transition)
        return new_transition, old_transition class Condition(models.Model):
    """CONDITION_TYPES = [
        ("function", "Function Call"),
        ("and", "Boolean AND"),
        ("or", "Boolean OR"),
        ("not", "Boolean NOT"),
    ]"""
    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    condition_opt = models.CharField(max_length=10, #choices=CONDITION_TYPES,
        verbose_name=ugettext_lazy("Type"))
    parent_condition = models.ForeignKey("Condition", on_delete=SET_NULL, null=True, blank=True,
        verbose_name=ugettext_lazy("Parent Condition"),
        related_name="child_conditions")
    transition = models.ForeignKey(Transition, on_delete=SET_NULL, null=True, blank=True,
        verbose_name=ugettext_lazy("Transition"))

    def clean(self):
        if self.transition and self.parent_condition:
            raise ValidationError("cannot specifiy both transition and parent condition")
        elif not self.transition and not self.parent_condition:
            raise ValidationError("at least one of transition and parent condition must be not null")

    def save(self, **qwargs):
        if self.transition:
            self.workflow = self.transition.workflow
        else:
            self.workflow = self.parent_condition.workflow
        super(Condition, self).save(**qwargs)

    def __unicode__(self):
        ancestors = []
        transition = self.transition
        p = self.parent_condition
        while p is not None:
            transition = p.transition
            ancestors.insert(0, p.condition_opt)
            p = p.parent_condition
        return "{}: {} -> {}".format(transition, ancestors, self.condition_opt)

    def check_condition(self, *, object_id, user, object_state, transition=None):
        if self.condition_opt == "function":
            func = self.function_set.first()
            call = func.function
            params = {p.name: p.value for p in func.parameters.all()}
            wf = self.workflow
            result = call(workflow=wf, user=user, object_id=object_id, object_state=object_state, transition=transition, **params)
            if result is None:
                result = False
            # #print("{}.{} ({}, {}, {}, **{}) = {}".format(func.function_module,func.function_name, wf, user,
            # object_id, params, result))
            return result
            # Not recursive
        elif self.condition_opt == "not":
            return not self.child_conditions.first().check_condition(user=user, object_id=object_id,
                object_state=object_state, transition=transition)
            # Recursive
        elif self.condition_opt == "and":
            #print([c.check_condition(user=user, object_id=object_id, object_state=object_state, transition=transition) for c in
                #self.child_conditions.all()])
            return all([c.check_condition(user=user, object_id=object_id, object_state=object_state, transition=transition) for c in
                self.child_conditions.all()])
            # Recursive
        elif self.condition_opt == "or":
            return any([c.check_condition(user=user, object_id=object_id, object_state=object_state, transition=transition) for c in
                self.child_conditions.all()])

    def clone(self, *, workflow: Workflow, **defaults) -> ('Condition', 'Condition'):
        #old_condition: Condition
        new_condition, old_condition = clone(self, workflow=workflow, **defaults)
        for condition in old_condition.child_conditions.all():
            new_child, old_child = condition.clone(workflow=workflow, parent_condition=new_condition)
        for function in old_condition.function_set.all():
            new_function, old_function = function.clone(workflow=workflow, condition=new_condition)
        return new_condition, old_condition class Function(models.Model):
    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    function_name = models.CharField(max_length=200, verbose_name=ugettext_lazy("Function"))
    function_module = models.CharField(max_length=400, verbose_name=ugettext_lazy("Module"))
    condition = models.ForeignKey(Condition, on_delete=PROTECT, verbose_name=ugettext_lazy("Condition"))

    def __unicode__(self):
        return "{} - {}.{}".format(self.condition, self.function_module, self.function_name)

    def save(self, **qwargs):
        self.workflow = self.condition.workflow
        super(Function, self).save(**qwargs)

    @property
    def function(self):
        return import_from(self.function_module, self.function_name)

    def clone(self, *, workflow: Workflow, **defaults) -> ('Function', 'Function'):
        #old_function: Function
        new_function, old_function = clone(self, workflow=workflow, **defaults)
        for param in old_function.parameters.all():
            new_param, old_param = param.clone(workflow=workflow, function=new_function)
        return new_function, old_function class FunctionParameter(models.Model):
    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    function = models.ForeignKey(Function, on_delete=PROTECT, verbose_name=ugettext_lazy("Function"),
        related_name="parameters")
    name = models.CharField(max_length=100, verbose_name=ugettext_lazy("Name"))
    value = models.CharField(max_length=4000, verbose_name=ugettext_lazy("Value"))

    def __unicode__(self):
        return "{} ({}: {})".format(self.function, self.name, self.value)

    def save(self, **qwargs):
        self.workflow = self.function.workflow
        super(FunctionParameter, self).save(**qwargs)

    def clone(self, *, workflow: Workflow, **defaults) -> ('FunctionParameter', 'FunctionParameter'):
        new_param, old_param = clone(self, workflow=workflow, **defaults)
        return new_param, old_param class Callback(models.Model):
    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    function_name = models.CharField(max_length=200, verbose_name=ugettext_lazy("Name"))
    function_module = models.CharField(max_length=400, verbose_name=ugettext_lazy("Module"))
    transition = models.ForeignKey(Transition, on_delete=PROTECT, verbose_name=ugettext_lazy("Transition"))
    order = models.IntegerField(verbose_name=ugettext_lazy("Order"))
    execute_async = models.BooleanField(verbose_name=ugettext_lazy("Execute Asynchronously"), default=False)

    def save(self, **qwargs):
        self.workflow = self.transition.workflow
        super(Callback, self).save(**qwargs)

    @property
    def function(self):
        return import_from(self.function_module, self.function_name)

    class Meta:
        ordering = ["order"]

    def clone(self, *, workflow: Workflow, **defaults) -> ('Callback', 'Callback'):
        #old_function: Function
        new_callback, old_callback = clone(self, workflow=workflow, **defaults)
        for param in old_callback.parameters.all():
            new_param, old_param = param.clone(workflow=workflow, callback=new_callback)
        return new_callback, old_callback class CallbackParameter(models.Model):
    workflow = models.ForeignKey(Workflow, on_delete=PROTECT, verbose_name=ugettext_lazy("Workflow"),
        editable=False)
    callback = models.ForeignKey(Callback, on_delete=PROTECT, verbose_name=ugettext_lazy("Callback"),
        related_name="parameters")
    name = models.CharField(max_length=100, verbose_name=ugettext_lazy("Name"))
    value = models.CharField(max_length=4000, verbose_name=ugettext_lazy("Value"))

    def save(self, **qwargs):
        self.workflow = self.callback.workflow
        super(CallbackParameter, self).save(**qwargs)

    def clone(self, *, workflow: Workflow, **defaults) -> ('CallbackParameter', 'CallbackParameter'):
        new_param, old_param = clone(self, workflow=workflow, **defaults)
        return new_param, old_param def _execute_atomatic_transitions(state, object_id, object_state_id, execute_async=False, last_transition=None):
    if not state.active:
        return None
    automatic_transitions = state.outgoing_transitions.filter(automatic=True)
    for t in automatic_transitions:
        if t.is_available(user=None, object_id=object_id, object_state_id=object_state_id, automatic=True):
            return _execute_transition(transition=t, user=None, object_id=object_id,
                object_state_id=object_state_id, automatic=True, last_transition=last_transition)
from django.core import serializers
from django_workflow.models import Workflow, State, CurrentObjectState, Transition, Condition, Function, \
    FunctionParameter, Callback, CallbackParameter, _execute_atomatic_transitions



def get_workflow(name):
    return Workflow.objects.get(name=name)


def get_available_transitions(workflow_name, user, object_id):
    state = get_object_state(workflow_name, object_id)
    wf = get_workflow(workflow_name)
    transitions = []
    if state:
        transitions = state.available_transitions(user, object_id)
        if state.is_final_state:
            transitions = transitions + [wf.initial_transition,]

    else:
        transitions = [wf.initial_transition,]
    transitions = [t for t in transitions if t is not None]
    return transitions


def get_object_state(workflow_name: str, object_id: str) -> State:
    wf = get_workflow(workflow_name)
    if not wf:
        raise ValueError("wokflow {} not found!".format(workflow_name))
    cos = CurrentObjectState.objects.filter(object_id=object_id, state__workflow__id=wf.id).order_by('-id').first()
    # do not raise exceptions because it rollbacks transactions in django 2 and this is not wished if you are just
    # checking if the object has already a workflow. So return None is not found and let the handling to the caller
    if cos is not None:
        return cos.state
    else:
        return None


def execute_transition(workflow_name, transition_name, user, object_id, execute_async=False):
    state = get_object_state(workflow_name, object_id)
    # silently fail if no state found or action not available
    wf = get_workflow(workflow_name)
    transition = wf.trasition_by_name(transition_name)
    if transition and transition.is_available(user, object_id):
        transition.execute(user, object_id)


def is_transition_available(workflow_name, transition_name, user, object_id):
    return len(list(filter(lambda x: x.name == transition_name,
        get_available_transitions(workflow_name, user, object_id)))) == 1


def is_object_in_workflow(workflow_name, object_id):
    return CurrentObjectState.objects.filter(object_id=object_id, workflow__name=workflow_name).exists()


def export_workflow(workflow_name):
    objects = Workflow.objects.all()
    if workflow_name:
        objects = Workflow.objects.filter(name=workflow_name)

    data = serializers.serialize('json',
        list(objects)
        + list(State.objects.filter(workflow__in=objects))

        + list(Transition.objects.filter(workflow__in=objects))