# -*- coding: utf-8 -*-
# Copyright (C) 2017  Irontec S.L.
#
# 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/>.
#

import threading
import time
import sys

from clint.textui import colored

from bbs.builder import StepBuilder
from bbs.session import Session
from bbs.settings import Settings


class Scenario(object):
    """
    Scenarios are containers for sessions that will be executed in a test
    and define the basic settings of the test itself.

    Instance attributes:
      conf_file: ConfFile
        Configuration file containing this scenario
      name: string
        Descriptive name for the test being executed.
      type: string
        Determines the way this scenario will be executed.
        In 'parallel' type scenarios, all sessions will be run at the same
        time. In 'sequential' type scenarios, steps will be run based on
        their creation order (step 'index').
      timeout: int = 180
        Max time in seconds this scenario will be running. This is handy for
        sessions with steps that wait indefinitely until an event has occurred.
        Scenarios that reach the timeout are considered failed.
      time_out: bool = false
        Determines if scenario has ended because timeout has been reached.
      sessions: [Session]
        List of sessions of the scenario. Sessions are used to group steps in a
        common execution environment.
      messages: [string]
        List of output messages generated by the steps of all sessions of the
        scenario.
      mutex: Lock
        Lock to ensure proper order of output messages of steps of the scenario.
      count: int
        Number of times this scenario will be run in sequence.
    """

    def __init__(self, conf_file, params):
        """Parse scenario sessions:
           Scenario can contain multiple sessions identified by a name.
           Based on scenario type, the steps will be run sequentially or
           parallel.

           This allows repeating the session name in case you want to split the
           steps execution.

           scenario:
               sessions:
                   - one:
                       - register
                       - wait
                   - two:
                       - register
                       - wait
                   - one:
                       - call: { number: "1000" }
                       - wait
                       - hangup
        """
        self.conf_file = conf_file
        self.name = params['name']
        self.type = 'simple'
        self.timeout = 180
        self.sessions = {}
        self.messages = []
        self.timed_out = False
        self.count = 1
        self.mutex = threading.Lock()

        if 'count' in params.keys():
            self.count = params['count']

        if 'type' in params.keys():
            self.type = params['type']

        if 'timeout' in params.keys():
            self.timeout = params['timeout']

        stepcnt = 0
        for session_conf in params['sessions']:
            # Get session name (str) and steps (list)
            (session_name, session_steps) = session_conf.popitem()

            # Check if we already have a session with that name
            if session_name in self.sessions.keys():
                session = self.sessions[session_name]
            else:
                session = Session(session_name, self)
                self.sessions[session_name] = session

            # Add steps to the session
            for step_conf in session_steps:

                if isinstance(step_conf, str):
                    (name, params) = (step_conf, None)

                if isinstance(step_conf, dict):
                    (name, params) = step_conf.popitem()

                # Get step type from the name
                step = StepBuilder.from_name(name)
                step.set_index(stepcnt)
                stepcnt += 1

                # Set step params (if any)
                if params:
                    step.set_params(params)

                # Attach this step to the session
                session.add_step(step)

    def __repr__(self, *args, **kwargs):
        self.log('Scenario(' + repr(self.name) + ', ' + repr(self.sessions) + ')')

    def log(self, msg):
        """Store step message in instance message list.
        If verbose level is high enough, also print message to output
        """
        self.mutex.acquire()
        settings = Settings()
        if settings.verbose:
            print msg
        self.messages.append(colored.clean(msg))
        self.mutex.release()

    def failed(self):
        """Handler for scenario timeout timer"""
        self.timed_out = True
        self.log(colored.red("Scenario has failed [%d timeout reached]" % self.timeout))

    def succeeded(self):
        """Determine if scenario has been successful based on completed sessions"""
        session_ok = 0
        for session in self.sessions.values():
            if session.completed():
                session_ok += 1
        return session_ok == len(self.sessions)

    def run(self):
        while (self.count):
            self.count -= 1
            self.run_parallel()

    def run_parallel(self):
        """Run scenario sessions in parallel threads.
        We also register a timer that will trigger after 'timeout' seconds unless
        scenario completes and cancels it.
        """
        self.log(colored.yellow("=============== %s (%s) =======================" % (self.name, self.conf_file)))

        t = threading.Timer(self.timeout, self.failed)
        t.daemon = True
        t.start()

        try:
            # Run scenario sessions in multiple threads
            for session in self.sessions.values():
                session.run_background()

            # Wait until finished or time out
            while not self.succeeded() and not self.timed_out:
                time.sleep(0.5)
                continue

            # On successful completion, avoid timeout handler to be triggered
            if not self.timed_out:
                t.cancel()

            if self.succeeded():
                self.log(colored.green("Scenario ended successfully"))
            else:
                self.log(colored.red("Scenario has failed"))
        except KeyboardInterrupt:
            print "\nInterrupt key pressed, quitting..."
            sys.exit(1)
        except:
            print self.log(colored.red("Scenario has failed"))


class ScenarioManager(list):

    def load(self, conf):
        for name in conf:
            self.append(Scenario(name))
