# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible 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.
#
# Ansible 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 Ansible.  If not, see <http://www.gnu.org/licenses/>.

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import fnmatch

from ansible import constants as C
from ansible.module_utils.six import iteritems
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.playbook.block import Block
from ansible.playbook.task import Task
from ansible.utils.display import Display


display = Display()


__all__ = ['PlayIterator']


class HostState:
    def __init__(self, blocks):
        self._blocks = blocks[:]

        self.cur_block = 0
        self.cur_regular_task = 0
        self.cur_rescue_task = 0
        self.cur_always_task = 0
        self.cur_dep_chain = None
        self.run_state = PlayIterator.ITERATING_SETUP
        self.fail_state = PlayIterator.FAILED_NONE
        self.pending_setup = False
        self.tasks_child_state = None
        self.rescue_child_state = None
        self.always_child_state = None
        self.did_rescue = False
        self.did_start_at_task = False

    def __repr__(self):
        return "HostState(%r)" % self._blocks

    def __str__(self):
        def _run_state_to_string(n):
            states = ["ITERATING_SETUP", "ITERATING_TASKS", "ITERATING_RESCUE", "ITERATING_ALWAYS", "ITERATING_COMPLETE"]
            try:
      
          return states[n]
            except IndexError:
                return "UNKNOWN STATE"

        def _failed_state_to_string(n):
            states = {1: "FAILED_SETUP", 2: "FAILED_TASKS", 4: "FAILED_RESCUE", 8: "FAILED_ALWAYS"}
            if n == 0:
                return "FAILED_NONE"
            else:
                ret = []
        
        for i in (1, 2, 4, 8):
                    if n & i:
                        ret.append(states[i])
                return "|".join(ret)

        return ("HOST STATE: block=%d, task=%d, rescue=%d, always=%d, run_state=%s, fail_state=%s, pending_setup=%s, tasks child state? (%s), "
                "rescue child state? (%s), always child state? (%s), did rescue? %s, did start at task? %s" % (
                    self.cur_block,
                    self.cur_regular_task,
                    self.cur_rescue_task,
                    self.cur_always_task,
                    _run_state_to_string(self.run_state),
                    _failed_state_to_string(self.fail_state),
                    self.pending_setup,
                    self.tasks_child_state,
                    self.rescue_child_state,
                    self.always_child_state,
                    self.did_rescue,
                    self.did_start_at_task,
                ))

    def __eq__(self, other):
        if not isinstance(other, HostState):
            return False

        for attr in ('_blocks', 'cur_block', 'cur_regular_task', 'cur_rescue_task', 'cur_always_task',
                     'run_state', 'fail_state', 'pending_setup', 'cur_dep_chain',
                     'tasks_child_state', 'rescue_child_state', 'always_child_state'):
            if getattr(self, attr) != getattr(other, attr):
                return False

        return True

    def get_current_block(self):
        return self._blocks[self.cur_block]

    def copy(self):
        new_state = HostState(self._blocks)
        new_state.cur_block = self.cur_block
        new_state.cur_regular_task = self.cur_regular_task
        new_state.cur_rescue_task = self.cur_rescue_task
        new_state.cur_always_task = self.cur_always_task
        new_state.run_state = self.run_state
        new_state.fail_state = self.fail_state
        new_state.pending_setup = self.pending_setup
        new_state.did_rescue = self.did_rescue
        new_state.did_start_at_task = self.did_start_at_task
        if self.cur_dep_chain is not None:
            new_state.cur_dep_chain = self.cur_dep_chain[:]
        if self.tasks_child_state is not None:
            new_state.tasks_child_state = self.tasks_child_state.copy()
        if self.rescue_child_state is not None:
            new_state.rescue_child_state = self.rescue_child_state.copy()
        if self.always_child_state is not None:
            new_state.always_child_state = self.always_child_state.copy()
        return new_state


class PlayIterator:

    # the primary running states for the play iteration
    ITERATING_SETUP = 0
    ITERATING_TASKS = 1
    ITERATING_RESCUE = 2
    ITERATING_ALWAYS = 3
    ITERATING_COMPLETE = 4

    # the failure states for the play iteration, which are powers
    # of 2 as they may be or'ed together in certain circumstances
    FAILED_NONE = 0
    FAILED_SETUP = 1
    FAILED_TASKS = 2
    FAILED_RESCUE = 4
    FAILED_ALWAYS = 8

    def __init__(self, inventory, play, play_context, variable_manager, all_vars, start_at_done=False):
        self._play = play
        self._blocks = []
        self._variable_manager = variable_manager

        # Default options to gather
        gather_subset = self._play.gather_subset
        gather_timeout = self._play.gather_timeout
        fact_path = self._play.fact_path

        setup_block = Block(play=self._play)
        # Gathering facts with run_once would copy the facts from one host to
        # the others.
        setup_block.run_once = False
        setup_task = Task(block=setup_block)
        setup_task.action = 'gather_facts'
        setup_task.name = 'Gathering Facts'
        setup_task.args = {
            'gather_subset': gather_subset,
        }

        # Unless play is specifically tagged, gathering should 'always' run
        if not self._play.tags:
            setup_task.tags = ['always']

        if gather_timeout:
            setup_task.args['gather_timeout'] = gather_timeout
        if fact_path:
            setup_task.args['fact_path'] = fact_path
        setup_task.set_loader(self._play._loader)
        # short circuit fact gathering if the entire playbook is conditional
        if self._play._included_conditional is not None:
            setup_task.when = self._play._included_conditional[:]
        setup_block.block = [setup_task]

        setup_block = setup_block.filter_tagged_tasks(all_vars)
        self._blocks.append(setup_block)

        for block in self._play.compile():
            new_block = block.filter_tagged_tasks(all_vars)
            if new_block.has_tasks():
                self._blocks.append(new_block)

        self._host_states = {}
        start_at_matched = False
        batch = inventory.get_hosts(self._play.hosts, order=self._play.order)
        self.batch_size = len(batch)
        for host in batch:
            self._host_states[host.name] = HostState(blocks=self._blocks)
            # if we're looking to start at a specific task, iterate through
            # the tasks for this host until we find the specified task
            if play_context.start_at_task is not None and not start_at_done:
                while True:
                    (s, task) = self.get_next_task_for_host(host, peek=True)
                    if s.run_state == self.ITERATING_COMPLETE:
                        break
                    if task.name == play_context.start_at_task or fnmatch.fnmatch(task.name, play_context.start_at_task) or \
                       task.get_name() == play_context.start_at_task or fnmatch.fnmatch(task.get_name(), play_context.start_at_task):
                        start_at_matched = True
                        break
                    else:
                        self.get_next_task_for_host(host)

               