import datetime
import logging
import math
import os
import signal
import subprocess
import time
import traceback
from argparse import ArgumentParser, _ArgumentGroup
from queue import Empty, Queue
from threading import Event, Lock, Thread
from typing import Dict, List, Optional, Set, Sequence, Tuple, Union

import toil
from toil import worker as toil_worker
from toil.batchSystems.abstractBatchSystem import (EXIT_STATUS_UNAVAILABLE_VALUE,
                                                   BatchSystemSupport,
                                                   ResourcePool,
                                                   ResourceSet,
                                                   UpdatedBatchJobInfo,
                                                   InsufficientSystemResources)

from toil.bus import ExternalBatchIdMessage
from toil.batchSystems.options import OptionSetter

from toil.common import SYS_MAX_SIZE, Config, Toil, make_open_interval_action
from toil.job import JobDescription, AcceleratorRequirement, accelerator_satisfies, Requirer
from toil.lib.accelerators import get_individual_local_accelerators, get_restrictive_environment_for_local_accelerators
from toil.lib.threading import cpu_count

logger = logging.getLogger(__name__)

class SingleMachineBatchSystem(BatchSystemSupport):
    """
    The interface for running jobs on a single machine, runs all the jobs you
    give it as they come in, but in parallel.

    Uses a single "daddy" thread to manage a fleet of child processes.

    Communication with the daddy thread happens via two queues: one queue of
    jobs waiting to be run (the input queue), and one queue of jobs that are
    finished/stopped and need to be returned by getUpdatedBatchJob (the output
    queue).

    When the batch system is shut down, the daddy thread is stopped.

    If running in debug-worker mode, jobs are run immediately as they are sent
    to the batch system, in the sending thread, and the daddy thread is not
    run. But the queues are still used.
    """

    @classmethod
    def supportsAutoDeployment(cls):
        return False

    @classmethod
    def supportsWorkerCleanup(cls):
        return True

    numCores = cpu_count()

    minCores = 0.1
    """
    The minimal fractional CPU. Tasks with a smaller core requirement will be rounded up to this
    value.
    """
    physicalMemory = toil.physicalMemory()

    def __init__(
        self, config: Config, maxCores: float, maxMemory: int, maxDisk: int, max_jobs: Optional[int] = None
    ) -> None:
        self.config = config

        if max_jobs is None:
            # Use the main limit for jobs from the config
            self.max_jobs = self.config.max_jobs
        else:
            # Use the override (probably because we are the local batch system
            # and not the main one).
            self.max_jobs = max_jobs

        # Limit to the smaller of the user-imposed limit and what we actually
        # have on this machine for each resource.
        #
        # If we don't have up to the limit of the resource (and the resource
        # isn't the inlimited sentinel), warn.
        if maxCores > self.numCores:
            if maxCores != SYS_MAX_SIZE and maxCores != float('inf'):
                # We have an actually specified limit and not the default
                logger.warning('Not enough cores! User limited to %i but we only have %i.', maxCores, self.numCores)
            maxCores = self.numCores
        if maxMemory > self.physicalMemory:
            if maxMemory < SYS_MAX_SIZE:  # todo: looks like humans2bytes converts SYS_MAX_SIZE to SYS_MAX_SIZE+1
                # We have an actually specified limit and not the default
                logger.warning('Not enough memory! User limited to %i bytes but we only have %i bytes.', maxMemory, self.physicalMemory)
            maxMemory = self.physicalMemory

        workdir = Toil.getLocalWorkflowDir(config.workflowID, config.workDir)  # config.workDir may be None; this sets a real directory
        self.physicalDisk = toil.physicalDisk(workdir)
        if maxDisk > self.physicalDisk:
            if maxDisk < SYS_MAX_SIZE:  # same as maxMemory logger.warning
                # We have an actually specified limit and not the default
                logger.warning('Not enough disk space! User limited to %i bytes but we only have %i bytes.', maxDisk, self.physicalDisk)
            maxDisk = self.physicalDisk

        super().__init__(config, maxCores, maxMemory, maxDisk)
        assert self.maxCores >= self.minCores
        assert self.maxMemory >= 1

        # The scale allows the user to apply a factor to each task's cores requirement, thereby
        # squeezing more tasks onto each core (scale < 1) or stretching tasks over more cores
        # (scale > 1).
        self.scale = config.scale

        if config.badWorker > 0 and config.debugWorker:
            # We can't throw SIGUSR1 at the worker because it is also going to
            # be the leader and/or test harness.
            raise RuntimeError("Cannot use badWorker and debugWorker together; "
                "worker would have to kill the leader")

        self.debugWorker = config.debugWorker

        # A counter to generate job IDs and a lock to guard it.
        # We make sure to start this at 1 so that job IDs are never falsy.
        self.jobIndex = 1
        self.jobIndexLock = Lock()

        # A dictionary mapping batch system IDs of submitted jobs to the command line
        self.jobs: Dict[int, JobDescription] = {}

        # A queue of jobs waiting to be executed. Consumed by the daddy thread.
        self.inputQueue = Queue()

        # A queue of finished jobs. Produced by the daddy thread.
        self.outputQueue = Queue()

        # A dictionary mapping batch system IDs of currently running jobs to their Info objects
        self.runningJobs: Dict[int, Info] = {}

        # These next two are only used outside debug-worker mode

        # A dict mapping PIDs to Popen objects for running jobs.
        # Jobs that don't fork are executed one at a time in the main thread.
        self.children: Dict[int, subprocess.Popen] = {}
        # A dict mapping child PIDs to the Job IDs they are supposed to be running.
        self.childToJob: Dict[int, str] = {}

        # For accelerators, we need a collection of what each accelerator is, and an acquirable set of them.
        self.accelerator_identities = get_individual_local_accelerators()

        # Put them all organized by resource type
        self.resource_sources = [
            # A pool representing available job slots
            ResourcePool(self.max_jobs, 'job slots'),
            # A pool representing available CPU in units of minCores
            ResourcePool(int(self.maxCores / self.minCores), 'cores'),
            # A pool representing available memory in bytes
            ResourcePool(self.maxMemory, 'memory'),
            # A pool representing the available space in bytes
            ResourcePool(self.maxDisk, 'disk'),
            # And a set for acquiring individual accelerators
            ResourceSet(set(range(len(self.accelerator_identities))), 'accelerators')
        ]

        # If we can't schedule something, we fill this in with a reason why
        self.schedulingStatusMessage = None

        # We use this event to signal shutdown
        self.shuttingDown = Event()

        # A thread in charge of managing all our child processes.
        # Also takes care of resource accounting.
        self.daddyThread = None
        # If it breaks it will fill this in
        self.daddyException: Optional[Exception] = None

        if self.debugWorker:
            logger.debug('Started batch system %s in worker debug mode.', id(self))
        else:
            self.daddyThread = Thread(target=self.daddy, daemon=True)
            self.daddyThread.start()
            logger.debug('Started batch system %s in normal mode.', id(self))

    def daddy(self):
        """
        Be the "daddy" thread.

        Our job is to look at jobs from the input queue.

        If a job fits in the available resources, we allocate resources for it
        and kick off a child process.

        We also check on our children.

        When a child finishes, we reap it, release its resources, and put its
        information in the output queue.
        """

        try:
            logger.debug('Started daddy thread for batch system %s.', id(self))

            while not self.shuttingDown.is_set():
                # Main loop

                while not self.shuttingDown.is_set():
                    # Try to start as many jobs as we can try to start
                    try:
                        # Grab something from the input queue if available.
                        args = self.inputQueue.get_nowait()
                        jobCommand, jobID, jobCores, jobMemory, jobDisk, job_accelerators, environment = args

                        coreFractions = int(jobCores / self.minCores)

                        # Try to start the child
                        result = self._startChild(jobCommand, jobID,
                            coreFractions, jobMemory, jobDisk, job_accelerators, environment)

                        if result is None:
                            # We did not get the resources to run this job.
                            # Requeue last, so we can look at the next job.
                            # TODO: Have some kind of condition the job can wait on,
                            # but without threads (queues for jobs needing
                            # cores/memory/disk individually)?
                            self.inputQueue.put(args)
                            break
                        elif result is not False:
                            #Result is a PID

                            if self._outbox is not None:
                                # Annotate the job with the PID generated.
                                self._outbox.publish(
                                   ExternalBatchIdMessage(jobID, str(result), self.__class__.__name__))

                        # Otherwise False

                    except Empty:
                        # Nothing to run. Stop looking in the queue.
                        break

                # Now check on our children.
                for done_pid in self._pollForDoneChildrenIn(self.children):
                    # A child has actually finished.
                    # Clean up after it.
                    self._handleChild(done_pid)

                # Then loop again: start and collect more jobs.
                # TODO: It would be good to be able to wait on a new job or a finished child, whichever comes first.
                # For now we just sleep and loop.
                time.sleep(0.01)

            # When we get here, we are shutting down.
            logger.debug('Daddy thread cleaning up %d remaining children for batch system %s...', len(self.children), id(self))

            self._stop_and_wait(self.children.values())

            logger.debug('Daddy thread for batch system %s finishing because no children should now exist', id(self))

            # Then exit the thread.
            return
        except Exception as e:
            logger.critical('Unhandled exception in daddy thread for batch system %s: %s', id(self), traceback.format_exc())
            # Pass the exception back to the main thread so it can stop the next person who calls into us.
            self.daddyException = e
            raise

    def _checkOnDaddy(self):
        if self.daddyException is not None:
            # The daddy thread broke and we cannot do our job
            logger.critical('Propagating unhandled exception in daddy thread to main thread')
            exc = self.daddyException
            self.daddyException = None
            if isinstance(exc, Exception):
                raise exc
            else:
                raise TypeError(f'Daddy thread failed with non-exception: {exc}')

    def _stop_now(self, popens: Sequence[subprocess.Popen]) -> List[int]:
        """
        Stop the given child processes and all their children. Does not reap them.

        Returns a list of PGIDs killed, where processes may exist that have not
        yet received their kill signals.
        """

        # We will potentially need to poll these PGIDs to ensure that all
        # processes in them are gone.
        pgids = []

        for popen in popens:
            # Kill all the children

            if popen.returncode is None:
                # Process is not known to be dead. Try and grab its group.
                try:
                    pgid = os.getpgid(popen.pid)
                except OSError:
                    # It just died. Assume the pgid was its PID.
                    pgid = popen.pid
            else:
                # It is dead. Try it's PID as a PGID and hope we didn't re-use it.
                pgid = popen.pid

            if pgid != os.getpgrp():
                # The child process really is in its own group, and not ours.

                # Kill the group, which hopefully hasn't been reused
                logger.debug('Send shutdown kill to process group %s known to batch system %s', pgid, id(self))
                try:
                    os.killpg(pgid, signal.SIGKILL)
                    pgids.append(pgid)
                except ProcessLookupError:
                    # It is dead already
                    pass
                except PermissionError:
                    # It isn't ours actually. Ours is dead.
                    pass
            else:
                # Kill the subprocess again through popen in case it somehow
                # never managed to make the group.
                popen.kill()

        return pgids

    def _stop_and_wait(self, popens: Sequence[subprocess.Popen], timeout: int = 5) -> None:
        """
        Stop the given child processes and all their children. Blocks until the
        processes are gone or timeout is passed.

        :param popens: The processes to stop and wait on.
        :param timeout: The number of seconds to wait for all process groups to
                        be gone.
        """

        pgids = self._stop_now(popens)

        for popen in popens:
            # Wait on all the children
            popen.wait()
            logger.debug('Process %s known to batch system %s is stopped; it returned %s',
                         popen.pid, id(self), popen.returncode)

        # Make sure all child processes have received their kill signal
        self._wait_for_death(pgids, timeout)

    def _wait_for_death(self, pgids: List[int], timeout: int = 5):
        """
        Wait for the process groups to be killed. Blocks until the processes
        are gone or timeout is passed.

        :param pgids: The list of process group ids.
        :param timeout: The number of seconds to wait for all process groups to
                        be gone.
        """
        # TODO: this opens a PGID reuse risk; someone else might've reaped the
        #  process and its PGID may have been re-used.

        start = datetime.datetime.now()
        while len(pgids) > 0 and (datetime.datetime.now() - start).total_seconds() < timeout:
            new_pgids: List[int] = []
            for pgid in pgids:
                try:
                    # Send a kill to the group again, to see if anything in it
                    # is still alive. Our first kill might not have been
                    # delivered yet.
                    os.killpg(pgid, signal.SIGKILL)

                    # If we reach here, something in the process group still
                    # exists.
                    new_pgids.append(pgid)
                except ProcessLookupError:
                    # The group is actually gone now.
                    pass
                except PermissionError:
                    # The group is not only gone but reused
                    pass

            pgids = new_pgids
            if len(pgids) > 0:
                time.sleep(0.1)

        if len(pgids) > 0:
            # If any processes are still alive, let user know that we may leave
            # behind dead but unreaped processes.
            logger.warning('Processes were not reaped in groups: %s.', str(pgids))
            logger.warning('Make sure your jobs are cleaning up child processes appropriately to avoid zombie '
                           'processes possibly being left behind.')

    def _pollForDoneChildrenIn(self, pid_to_popen):
        """
        See if any children represented in the given dict from PID to Popen
        object have finished.

        Return a collection of their PIDs.

        Guarantees that each child's exit code will be gettable via wait() on
        the child's Popen object (i.e. does not reap the child, unless via
        Popen).
        """

        # We keep our found PIDs in a set so we can work around waitid showing
        # us the same one repeatedly.
        ready = set()

        # Find the waitid function
        waitid = getattr(os, 'waitid', None)

        if callable(waitid):
            # waitid exists (not Mac)

            while True:
                # Poll for any child to have exit, but don't reap it. Leave reaping
                # to the Popen.
                # TODO: What if someone else in Toil wants to do this syscall?
                # TODO: Is this one-notification-per-done-child with WNOHANG? Or
                # can we miss some? Or do we see the same one repeatedly until it
                # is reaped?
                try:
                    siginfo = waitid(os.P_ALL, -1, os.WEXITED | os.WNOWAIT | os.WNOHANG)
                except ChildProcessError:
                    # This happens when there is nothing to wait on right now,
                    # instead of the weird C behavior of overwriting a field in
                    # a pointed-to struct.
                    siginfo = None
                if siginfo is not None and siginfo.si_pid in pid_to_popen and siginfo.si_pid not in ready:
                    # Something new finished
                    ready.add(siginfo.si_pid)
                else:
                    # Nothing we own that we haven't seen before has finished.
                    return ready
        else:
            # On Mac there's no waitid and no way to wait and not reap.
            # Fall back on polling all the Popen objects.
            # To make this vaguely efficient we have to return done children in
            # batches.
            for pid, popen in pid_to_popen.items():
                if popen.poll() is not None:
                    # Process is done
                    ready.add(pid)
                    logger.debug('Child %d has stopped', pid)

            # Return all the done processes we found
            return ready

    def _runDebugJob(self, jobCommand, jobID, environment):
        """
        Run the jobCommand right now, in the current thread.
        May only be called in debug-worker mode.
        Assumes resources are available.
        """
        assert self.debugWorker
        # TODO: It is not possible to kill running jobs in forkless mode,
        # because they are run immediately in the main thread.
        info = Info(time.time(), None, None, killIntended=False)
        self.runningJobs[jobID] = info

        if jobCommand.startswith("_toil_worker "):
            # We can actually run in this thread
            jobName, jobStoreLocator, jobStoreID = jobCommand.split()[1:4] # Parse command
            jobStore = Toil.resumeJobStore(jobStoreLocator)
            toil_worker.workerScript(jobStore, jobStore.config, jobName, jobStoreID,
                                     redirectOutputToLogFile=not self.debugWorker) # Call the worker
        else:
            # Run synchronously. If starting or running the command fails, let the exception stop us.
            subprocess.check_call(jobCommand,
                                  shell=True,
                                  env=dict(os.environ, **environment))

        self.runningJobs.pop(jobID)
        if not info.killIntended:
            self.outputQueue.put(UpdatedBatchJobInfo(jobID=jobID, exitStatus=0, wallTime=time.time() - info.time, exitReason=None))

    def getSchedulingStatusMessage(self):
        # Implement the abstractBatchSystem's scheduling status message API
        return self.schedulingStatusMessage

    def _setSchedulingStatusMessage(self, message):
        """
        If we can't run a job, we record a short message about why not. If the
        leader wants to know what is up with us (for example, to diagnose a
        deadlock), it can ask us for the message.
        """

        self.schedulingStatusMessage = message

    def check_resource_request(self, requirer: Requirer) -> None:
        try:
            super().check_resource_request(requirer)
        except InsufficientSystemResources as e:
            # Tack the scale onto the exception
            e.details.append(f'Scale is set to {self.scale}.')
            raise e

    def _check_accelerator_request(self, requirer: Requirer) -> None:
        _, problem = self._identify_sufficient_accelerators(requirer.accelerators, set(range(len(self.accelerator_identities))))
        if problem is not None:
            # We can't get the accelerators
            raise InsufficientSystemResources(requirer, 'accelerators', self.accelerator_identities, details=[
                f'The accelerator {problem} could not be provided.'
            ])

    def _release_acquired_resources(self, resources: List[Union[int, Set[int]]]) -> None:
        """
        Release all resources acquired for a job.
        Assumes resources are in the order: core fractions, memory, disk, accelerators.
        """

        # What pools and sets do we want resources from

        for resource, request in zip(self.resource_sources, resources):
            assert ((isinstance(resource, ResourcePool) and isinstance(request, int)) or
                    (isinstance(resource, ResourceSet) and isinstance(request, set)))
            resource.release(request)

    def _identify_sufficient_accelerators(self, needed_accelerators: List[AcceleratorRequirement], available_accelerator_ids: Set[int]) -> Tuple[Optional[Set[int]], Optional[AcceleratorRequirement]]:
        """
        Given the accelerator requirements of a job, and the set of available
        accelerators out of our associated collection of accelerators, find a
        set of the available accelerators that satisfies the job's
        requirements.

        Returns that set and None if the set exists, or None and an unsatisfied
        AcceleratorRequirement if it does not.

        TODO: Uses a simple greedy algorithm and not a smart matching
        algorithm, so if the job requires different kinds of accelerators, and
        some accelerators available can match multiple requirements, then it is
        possible that a solution will not be found.

        Ignores accelerator model constraints.
        """
        accelerators_needed: Set[int] = set()
        accelerators_still_available = set(available_accelerator_ids)
        for requirement in needed_accelerators:
            for i in range(requirement['count']):
                # For each individual accelerator we need
                satisfied = False
                for candidate_index in accelerators_still_available:
                    # Check all the ones we haven't grabbed yet
                    # TODO: We'll re-check early ones against this requirement if it has a count of more than one.
                    candidate = self.accelerator_identities[candidate_index]
                    if accelerator_satisfies(candidate, requirement, ignore=['model']):
                        # If this accelerator can satisfy one unit of this requirement.
                        # We ignore model constraints because as a single
                        # machine we can't really determine the models of
                        # installed accelerators in a way consistent with how
                        # workflows are going to name them, and usually people
                        # use one model anyway.

                        # Say we want it
                        accelerators_needed.add(candidate_index)
                        accelerators_still_available.remove(candidate_index)
                        # And move on to the next required unit
                        satisfied = True
                        break
                if not satisfied:
                    # We can't get the resources we need to run right now.
                    return None, requirement
        # If we get here we satisfied everything
        return accelerators_needed, None

    def _startChild(self, jobCommand, jobID, coreFractions, jobMemory, jobDisk, job_accelerators: List[AcceleratorRequirement], environment):
        """
        Start a child process for the given job.

        Allocate its required resources and save it and save it in our bookkeeping structures.

        If the job is started, returns its PID.
        If the job fails to start, reports it as failed and returns False.
        If the job cannot get the resources it needs to start, returns None.
        """

        # We fill this in if we manage to actually start the child.
        popen = None

        # This is when we started working on the job.
        startTime = time.time()

        # And what do we want from each resource in self.resource_sources?
        # We know they go job slot, cores, memory, disk, accelerators.
        resource_requests: List[Union[int, Set[int]]] = [1, coreFractions, jobMemory, jobDisk]

        # Keep a reference to the accelerators separately
        accelerators_needed = None

        if job_accelerators:
            # Try and find some accelerators to use.
            # Start with all the accelerators that are free right now
            accelerator_set : ResourceSet = self.resource_sources[-1]
            snapshot = accelerator_set.get_free_snapshot()
            # And build a plan of the ones we want
            accelerators_needed, problem = self._identify_sufficient_accelerators(job_accelerators, snapshot)
            if accelerators_needed is not None:
                # Now we have a plan to get the accelerators we need.
                resource_requests.append(accelerators_needed)
            else:
                # We couldn't make a plan; the accelerators are busy
                assert problem is not None
                logger.debug('Accelerators are busy: %s', problem)
                self._setSchedulingStatusMessage('Not enough accelerators to run job %s' % jobID)
                return None

        acquired = []
        for source, request in zip(self.resource_sources, resource_requests):
            # For each kind of resource we want, go get it
            assert ((isinstance(source, ResourcePool) and isinstance(request, int)) or
                    (isinstance(source, ResourceSet) and isinstance(request, set)))
            if source.acquireNow(request):
                acquired.append(request)
            else:
                # We can't get everything
                self._setSchedulingStatusMessage('Not enough {} to run job {}'.format(source.resource_type, jobID))
                self._release_acquired_resources(acquired)
                return None

        # Now we have all the resources!

        # Prepare the environment
        child_environment = dict(os.environ, **environment)

        # Communicate the accelerator resources, if any, to the child process
        # by modifying the environemnt
        accelerators_acquired: Set[int] = accelerators_needed if accelerators_needed is not None else set()
        child_environment.update(get_restrictive_environment_for_local_accelerators(accelerators_acquired))

        # Actually run the job.
        # When it finishes we will release what it was using.
        # So it is important to not lose track of the child process.

        try:
            # Launch the job.
            # Make sure it is in its own session (and thus its own
            # process group) so that, if the user signals the
            # workflow, Toil will be responsible for killing the
            # job. This also makes sure that we can signal the job
            # and all its children together. We assume that the
            # process group ID will equal the PID of the process we
            # are starting.
            popen = subprocess.Popen(jobCommand,
                                     shell=True,
                                     env=child_environment,
                                     start_new_session=True)
        except Exception:
            # If the job can't start, make sure we release resources now
            self._release_acquired_resources(acquired)

            logger.error('Could not start job %s: %s', jobID, traceback.format_exc())

            # Report as failed.
            self.outputQueue.put(UpdatedBatchJobInfo(jobID=jobID, exitStatus=EXIT_STATUS_UNAVAILABLE_VALUE, wallTime=0, exitReason=None))

            # Complain it broke.
            return False
        else:
            # If the job did start, record it
            self.children[popen.pid] = popen
            # Make sure we can look it up by PID later
            self.childToJob[popen.pid] = jobID
            # Record that the job is running, and the resources it is using
            info = Info(startTime, popen, acquired, killIntended=False)
            self.runningJobs[jobID] = info

            logger.debug('Launched job %s as child %d', jobID, popen.pid)

            # Report success starting the job
            # Note that if a PID were somehow 0 it would look like False
            assert popen.pid != 0
            return popen.pid

    def _handleChild(self, pid: int) -> None:
        """
        Handle a child process PID that has finished.
        The PID must be for a child job we started.
        Not thread safe to run at the same time as we are making more children.

        Remove the child from our bookkeeping structures and free its resources.
        """

        # Look up the child
        popen = self.children[pid]
        jobID = self.childToJob[pid]
        info = self.runningJobs[jobID]

        # Get the job resources reserved by the job
        acquired = info.resources

        # Clean up our records of the job.
        self.runningJobs.pop(jobID)
        self.childToJob.pop(pid)
        self.children.pop(pid)

        if popen.returncode is None or not callable(getattr(os, 'waitid', None)):
            # It isn't reaped yet, or we have to reap all children to see if thay're done.
            # Before we reap it (if possible), kill its PID as a PGID to make sure
            # it isn't leaving children behind.
            # TODO: This is a PGID re-use risk on Mac because the process is
            #  reaped already and the PGID may have been reused.
            try:
                os.killpg(pid, signal.SIGKILL)
            except ProcessLookupError:
                # It is dead already
                pass
            except PermissionError:
                # It isn't ours actually. Ours is dead.
                pass

        # See how the child did, and reap it.
        statusCode = popen.wait()
        if statusCode != 0 and not info.killIntended:
            logger.error("Got exit code %i (indicating failure) "
                      "from job %s.", statusCode, self.jobs[jobID])
        if not info.killIntended:
            # Report if the job failed and we didn't kill it.
            # If we killed it then it shouldn't show up in the queue.
            self.outputQueue.put(UpdatedBatchJobInfo(jobID=jobID, exitStatus=statusCode, wallTime=time.time() - info.time, exitReason=None))

        # Last attempt to make sure all processes in the group have received
        # their kill signals.
        self._wait_for_death([pid])

        # Free up the job's resources.
        self._release_acquired_resources(acquired)

        logger.debug('Child %d for job %s succeeded', pid, jobID)

    def issueBatchJob(self, jobDesc: JobDescription, job_environment: Optional[Dict[str, str]] = None) -> int:
        """Adds the command and resources to a queue to be run."""

        self._checkOnDaddy()

        # Apply scale in cores
        scaled_desc = jobDesc.scale('cores', self.scale)
        # Round cores up to multiples of minCores
        scaled_desc.cores = max(math.ceil(scaled_desc.cores / self.minCores) * self.minCores, self.minCores)

        # Don't do our own assertions about job size vs. our configured size.
        # The abstract batch system can handle it.
        self.check_resource_request(scaled_desc)
        logger.debug(f"Issuing the command: {jobDesc.command} with {scaled_desc.requirements_string()}")
        with self.jobIndexLock:
            jobID = self.jobIndex
            self.jobIndex += 1
        self.jobs[jobID] = jobDesc.command

        environment = self.environment.copy()
        if job_environment:
            environment.update(job_environment)

        if self.debugWorker:
            # Run immediately, blocking for return.
            # Ignore resource requirements; we run one job at a time
            self._runDebugJob(jobDesc.command, jobID, environment)
        else:
            # Queue the job for later
            self.inputQueue.put((jobDesc.command, jobID, scaled_desc.cores, scaled_desc.memory,
                                scaled_desc.disk, scaled_desc.accelerators, environment))

        return jobID

    def killBatchJobs(self, jobIDs: List[int]) -> None:
        """Kills jobs by ID."""

        self._checkOnDaddy()

        logger.debug(f'Killing jobs: {jobIDs}')

        # Collect the popen handles for the jobs we have to stop
        popens: List[subprocess.Popen] = []

        for jobID in jobIDs:
            if jobID in self.runningJobs:
                info = self.runningJobs[jobID]
                info.killIntended = True
                if info.popen is not None:
                    popens.append(info.popen)
                else:
                    # No popen if running in forkless mode currently
                    assert self.debugWorker
                    logger.critical("Can't kill job: %s in debug mode" % jobID)

        # Stop them all in a batch. Don't reap, because we need the daddy
        # thread to reap them to mark the jobs as not running anymore.
        self._stop_now(popens)

        for jobID in jobIDs:
            while jobID in self.runningJobs:
                # Wait for the daddy thread to collect them.
                time.sleep(0.01)

    def getIssuedBatchJobIDs(self) -> List[int]:
        """Just returns all the jobs that have been run, but not yet returned as updated."""

        self._checkOnDaddy()

        return list(self.jobs.keys())

    def getRunningBatchJobIDs(self) -> Dict[int, float]:

        self._checkOnDaddy()

        now = time.time()
        return {jobID: now - info.time for jobID, info in list(self.runningJobs.items())}

    def shutdown(self) -> None:
        """Terminate cleanly and join daddy thread."""
        if self.daddyThread is not None:
            # Tell the daddy thread to stop.
            self.shuttingDown.set()
            # Wait for it to stop.
            self.daddyThread.join()

        BatchSystemSupport.workerCleanup(self.workerCleanupInfo)

    def getUpdatedBatchJob(self, maxWait: int) -> Optional[UpdatedBatchJobInfo]:
        """Returns a tuple of a no-longer-running job, the return value of its process, and its runtime, or None."""

        self._checkOnDaddy()

        try:
            item = self.outputQueue.get(timeout=maxWait)
        except Empty:
            return None
        self.jobs.pop(item.jobID)
        logger.debug("Ran jobID: %s with exit value: %i", item.jobID, item.exitStatus)
        return item

    @classmethod
    def add_options(cls, parser: Union[ArgumentParser, _ArgumentGroup]) -> None:
        parser.add_argument("--scale", dest="scale", type=float, default=1, action=make_open_interval_action(0.0),
                            help="A scaling factor to change the value of all submitted tasks's submitted cores.  "
                                 "Used in the single_machine batch system. Useful for running workflows on "
                                 "smaller machines than they were designed for, by setting a value less than 1. "
                                 "(default: %(default)s)")

    @classmethod
    def setOptions(cls, setOption: OptionSetter):
        setOption("scale")

class Info:
    """
    Record for a running job.

    Stores the start time of the job, the Popen object representing its child
    (or None), the tuple of (coreFractions, memory, disk) it is using (or
    None), and whether the job is supposed to be being killed.
    """
    # Can't use namedtuple here since killIntended needs to be mutable
    def __init__(self, startTime, popen, resources, killIntended):
        self.time = startTime
        self.popen = popen
        self.resources = resources
        self.killIntended = killIntended


from __future__ import print_function
import re

def to_military(time):
    ampm_regex = re.compile(r'(^\d{2})(:\d{2}:\d{2})([A-Za-z]{2})')
    hours, mins_and_secs, time_period = ampm_regex.findall(time)[0]
    military_time = str(int(hours) % 12 + (12 if time_period == 'PM' else 0)
            ).zfill(2) + mins_and_secs
    return military_time

def hackerrank_solve():
    time = raw_input().strip()
    print(to_military(time))

def main():
    test()

def test():
    cases = [
        '11:32:32AM',
        '04:41:11PM',
        '12:51:21PM',
        '12:51:21AM',
        '11:51:21PM',
        '07:21:41PM',
        '01:41:12AM'
        ]
    for case in cases:
        print(case, ' --> ', to_military(case))

if __name__ == '__main__':
    main()


E_INT, E_FLOAT, E_STR = "INT", "FLOAT", "STR"

class EventGet:

    def __init__(self, val):
        self.val = None
        self.type = {int:E_INT, float:E_FLOAT, str:E_STR}[val]

class EventSet:

    def __init__(self, val):
        self.val = val
        self.type = {int:E_INT, float:E_FLOAT, str:E_STR}[type(val)]

class NullHandler:

    def __init__(self, successor=None):
        self.successor = successor

    def handle(self, obj, event):
        if self.successor is not None:
           return self.successor.handle(obj, event)

class StrHandler(NullHandler):

    def handle(self, obj, event):
        if event.type == E_STR:
            if event.val is None:
                return obj.string_field
            else:
                obj.string_field = event.val
        else:
            return super().handle(obj, event)

class FloatHandler(NullHandler):

    def handle(self, obj, event):
        if event.type == E_FLOAT:
            if event.val is None:
                return obj.float_field
            else:
                obj.float_field = event.val
        else:
            return super().handle(obj, event)

class IntHandler(NullHandler):

    def handle(self, obj, event):
        if event.type == E_INT:
            if event.val is None:
                return obj.integer_field
            else:
                obj.integer_field = event.val
        else:
            return super().handle(obj, event)


'''Branch Sums

Write a function that takes in a Binary Tree and returns a list of its branch sums ordered from leftmost branch sum to rightmost branch sum.

A branch sum is the sum of all values in a Binary Tree branch. A Binary Tree branch is a path of nodes in a tree that starts at the root node and ends at any leaf node.
'''

# This is the class of the input root. Do not edit it.
class BinaryTree:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

def branchSums(root):
    # Write your code here.
    arr = []
    return getBranchSums(root, arr, 0)

def getBranchSums(root, arr, curSum):
    if root.left is None and root.right is None:
        curSum += root.value
        arr.append(curSum)
        return arr
    curSum += root.value
    if root.left:
        arr = getBranchSums(root.left, arr, curSum)
    if root.right:
        arr = getBranchSums(root.right, arr, curSum)
    return arr

myTree = BinaryTree(1)
myTree.left = BinaryTree(2)
myTree.right = BinaryTree(3)
myTree.right.right = BinaryTree(7)
myTree.right.left = BinaryTree(6)
myTree.left.left = BinaryTree(4)
myTree.left.right = BinaryTree(5)
myTree.left.right.left = BinaryTree(10)
myTree.left.left.left = BinaryTree(8)
myTree.left.left.right = BinaryTree(9)

print(branchSums(myTree))  # True


import time
import sys

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, \
    WebDriverException, TimeoutException, NoSuchWindowException, \
    StaleElementReferenceException, ElementNotInteractableException
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from regression.feature_utils.locators import QueryToolLocators, \
    NavMenuLocators, ConnectToServerDiv
from regression.feature_utils.tree_area_locators import TreeAreaLocators

class PgadminPage:
    """
    Helper class for interacting with the page, given a selenium driver
    """
    # Common argument passed for scrolling
    js_executor_scrollintoview_arg = "arguments[0].scrollIntoView()"

    def __init__(self, driver, app_config):
        self.driver = driver
        self.app_config = app_config
        self.timeout = 30
        self.app_start_timeout = 90

    def reset_layout(self):
        attempt = 0
        while attempt < 4:
            try:
                self.click_element(self.find_by_partial_link_text("File"))
                break
            except (TimeoutException, NoSuchWindowException):
                self.driver.refresh()
                try:
                    WebDriverWait(self.driver, 3).until(EC.alert_is_present())
                    self.driver.switch_to_alert().accept()
                    attempt = attempt + 1
                except TimeoutException:
                    attempt = attempt + 1

        self.find_by_partial_link_text("Reset Layout").click()
        self.click_modal('OK')
        self.wait_for_reloading_indicator_to_disappear()

    def refresh_page(self):
        self.driver.refresh()

    def click_modal(self, button_text):
        time.sleep(0.5)
        # Find active alertify dialog in case of multiple alertify dialog
        # & click on that dialog
        modal_button = self.find_by_xpath(
            "//div[contains(@class, 'alertify') and "
            "not(contains(@class, 'ajs-hidden'))]//button[.='%s']"
            % button_text)
        self.click_element(modal_button)

    def add_server(self, server_config):
        self.find_by_xpath(
            "//*[@class='aciTreeText' and contains(.,'Servers')]").click()

        if self.driver.name == 'firefox':
            ActionChains(self.driver).context_click(self.find_by_xpath(
                "//*[@class='aciTreeText' and contains(.,'Servers')]"))\
                .perform()
            ActionChains(self.driver).move_to_element(
                self.find_by_xpath("//li/span[text()='Create']")).perform()
            self.find_by_xpath("//li/span[text()='Server...']").click()
        else:
            self.driver.find_element_by_link_text("Object").click()
            ActionChains(self.driver).move_to_element(
                self.driver.find_element_by_link_text("Create")).perform()
            self.find_by_partial_link_text("Server...").click()

        WebDriverWait(self.driver, 5).until(EC.visibility_of_element_located(
            (By.XPATH, "//div[text()='Create - Server']")))

        # After server dialogue opens
        self.fill_input_by_field_name("name", server_config['name'],
                                      loose_focus=True)
        self.find_by_partial_link_text("Connection").click()
        self.fill_input_by_field_name("host", server_config['host'])
        self.fill_input_by_field_name("port", server_config['port'])
        self.fill_input_by_field_name("username", server_config['username'])
        self.fill_input_by_field_name("password", server_config['db_password'])
        WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(
            (By.CSS_SELECTOR, "button[type='save'].btn.btn-primary")))
        self.find_by_css_selector("button[type='save'].btn.btn-primary").\
            click()

        server_tree_xpath = \
            "//*[@id='tree']//*[.='" + server_config['name'] + "']"
        try:
            WebDriverWait(self.driver, 10).until(
                EC.visibility_of_element_located(
                    (By.XPATH, server_tree_xpath)))
        except TimeoutException:
            self.toggle_open_servers_group()
            WebDriverWait(self.driver, 10).until(
                EC.visibility_of_element_located(
                    (By.XPATH, server_tree_xpath)))

    def open_query_tool(self):
        self.driver.find_element_by_link_text("Tools").click()
        tools_menu = self.driver.find_element_by_id('mnu_tools')

        query_tool = tools_menu.find_element_by_id('query_tool')

        self.enable_menu_item(query_tool, 10)

        self.find_by_partial_link_text("Query Tool").click()

    def open_view_data(self, table_name):
        self.driver.find_element_by_link_text("Object").click()
        ActionChains(
            self.driver
        ).move_to_element(
            self.driver.find_element_by_link_text("View/Edit Data")
        ).perform()
        self.find_by_partial_link_text("All Rows").click()
        time.sleep(1)
        # wait until datagrid frame is loaded.

        self.click_tab(table_name)

        WebDriverWait(self.driver, self.timeout).until(
            EC.visibility_of_element_located(
                (By.CSS_SELECTOR, 'iframe')
            ), "Timed out waiting for div element to appear"
        )
        self.driver.switch_to.frame(
            self.driver.find_element_by_tag_name('iframe')
        )

    def enable_menu_item(self, menu_item, wait_time):
        start_time = time.time()
        # wait until menu becomes enabled.
        while time.time() - start_time < wait_time:  # wait_time seconds
            # if menu is disabled then it will have
            # two classes 'dropdown-item disabled'.
            # And if menu is enabled the it will have
            # only one class 'dropdown-item'.

            if 'dropdown-item' == str(menu_item.get_attribute('class')):
                break
            time.sleep(0.1)
        else:
            assert False, "'Tools -> Query Tool' menu did not enable."

    def close_query_tool(self, prompt=True):
        self.driver.switch_to.default_content()
        tab = self.find_by_xpath(
            "//div[@class='wcPanelTab wcPanelTabActive']")
        ActionChains(self.driver).context_click(tab).perform()
        self.find_by_xpath(
            "//li[contains(@class, 'context-menu-item')]/span[contains(text(),"
            " 'Remove Panel')]").click()
        if prompt:
            self.driver.switch_to.frame(
                self.driver.find_elements_by_tag_name("iframe")[0])
            time.sleep(.5)
            self.click_element(self.find_by_xpath(
                '//button[contains(@class, "ajs-button") and '
                'contains(.,"Don\'t save")]'))

            if self.check_if_element_exist_by_xpath(
                    "//button[text()='Rollback']", 1):
                self.click_element(
                    self.find_by_xpath("//button[text()='Rollback']"))
        self.driver.switch_to.default_content()

    def clear_query_tool(self):
        self.click_element(
            self.find_by_css_selector(QueryToolLocators.btn_clear_dropdown)
        )
        ActionChains(self.driver).move_to_element(
            self.find_by_css_selector(QueryToolLocators.btn_clear)).perform()
        self.click_element(
            self.find_by_css_selector(QueryToolLocators.btn_clear)
        )
        self.click_modal('Yes')

    def execute_query(self, query):
        self.fill_codemirror_area_with(query)
        self.click_execute_query_button()

    def click_execute_query_button(self, timeout=20):
        retry = 5
        execute_button = self.find_by_css_selector(
            QueryToolLocators.btn_execute_query_css)
        first_click = execute_button.get_attribute('data-click-counter')
        while retry > 0:
            execute_button.click()
            execute_button = self.find_by_css_selector(
                QueryToolLocators.btn_execute_query_css)
            second_click = execute_button.get_attribute(
                'data-click-counter')
            if first_click != second_click:
                self.wait_for_query_tool_loading_indicator_to_appear()
                break
            else:
                retry -= 1
        self.wait_for_query_tool_loading_indicator_to_disappear(timeout)

    def check_execute_option(self, option):
        """"This function will check auto commit or auto roll back based on
        user input. If button is already checked, no action will be taken"""
        query_options = self.driver.find_element_by_css_selector(
            QueryToolLocators.btn_query_dropdown)
        expanded = query_options.get_attribute("aria-expanded")
        if expanded == "false":
            query_options.click()

        def update_execute_option_setting(
                css_selector_of_option_status, css_selector_of_option,):
            retry = 3
            check_status = self.driver.find_element_by_css_selector(
                css_selector_of_option_status)
            if 'visibility-hidden' in check_status.get_attribute('class'):
                while retry > 0:
                    self.find_by_css_selector(css_selector_of_option).click()
                    time.sleep(0.2)
                    if 'visibility-hidden' not in \
                            check_status.get_attribute('class'):
                        break
                    else:
                        retry -= 1
        if option == 'auto_commit':
            update_execute_option_setting(
                QueryToolLocators.btn_auto_commit_check_status,
                QueryToolLocators.btn_auto_commit)
        if option == 'auto_rollback':
            update_execute_option_setting(
                QueryToolLocators.btn_auto_rollback_check_status,
                QueryToolLocators.btn_auto_rollback)

    def uncheck_execute_option(self, option):
        """"This function will uncheck auto commit or auto roll back based on
        user input. If button is already unchecked, no action will be taken"""
        query_options = self.driver.find_element_by_css_selector(
            QueryToolLocators.btn_query_dropdown)
        expanded = query_options.get_attribute("aria-expanded")
        if expanded == "false":
            query_options.click()

        def update_execute_option_setting(
                css_selector_of_option_status, css_selector_of_option):
            retry = 3
            check_status = self.driver.find_element_by_css_selector(
                css_selector_of_option_status)
            if 'visibility-hidden' not in check_status.get_attribute('class'):
                while retry > 0:
                    self.find_by_css_selector(
                        css_selector_of_option).click()
                    time.sleep(0.2)
                    if 'visibility-hidden' in \
                            check_status.get_attribute('class'):
                        break
                    else:
                        retry -= 1

        if option == 'auto_commit':
            update_execute_option_setting(
                QueryToolLocators.btn_auto_commit_check_status,
                QueryToolLocators.btn_auto_commit)
        if option == 'auto_rollback':
            update_execute_option_setting(
                QueryToolLocators.btn_auto_rollback_check_status,
                QueryToolLocators.btn_auto_rollback)

    def close_data_grid(self):
        self.driver.switch_to_default_content()
        xpath = "//*[@id='dockerContainer']/div/div[3]/div/div[2]/div[1]"
        self.click_element(self.find_by_xpath(xpath))

    def remove_server(self, server_config):
        self.driver.switch_to.default_content()
        server_to_remove = self.find_by_xpath(
            "//*[@id='tree']//*[.='" + server_config['name'] +
            "' and @class='aciTreeItem']")
        self.driver.execute_script(
            self.js_executor_scrollintoview_arg, server_to_remove)
        self.click_element(server_to_remove)
        object_menu_item = self.find_by_partial_link_text("Object")
        self.click_element(object_menu_item)
        delete_menu_item = self.find_by_partial_link_text("Remove Server")
        self.click_element(delete_menu_item)
        self.click_modal('Yes')

    def select_tree_item(self, tree_item_text):
        item = self.find_by_xpath(
            "//*[@id='tree']//*[contains(text(), '" + tree_item_text + "')]"
            "/parent::span[@class='aciTreeItem']")
        self.driver.execute_script(self.js_executor_scrollintoview_arg, item)
        # unexpected exception like element overlapping, click attempts more
        # than one time
        attempts = 3
        while attempts > 0:
            try:
                item.click()
                break
            except Exception as e:
                attempts -= 1
                time.sleep(.4)
                if attempts == 0:
                    raise e

    def click_a_tree_node(self, element_name, list_of_element):
        """It will click a tree node eg. server, schema, table name etc
        will take server name and list of element where this node lies"""
        operation_status = False
        elements = list_of_element = self.find_by_xpath_list(
            list_of_element)
        if len(elements) > 0:
            index_of_element = self.get_index_of_element(
                elements, element_name)
            if index_of_element >= 0:
                self.driver.execute_script(
                    self.js_executor_scrollintoview_arg,
                    list_of_element[index_of_element])
                self.wait_for_elements_to_appear(
                    self.driver, list_of_element[index_of_element])
                time.sleep(1)
                list_of_element[index_of_element].click()
                operation_status = True
            else:
                print("{ERROR} - The required element with name: " + str(
                    element_name) +
                    " is not found in function click_a_tree_node, "
                    "so click operation is not performed")
        else:
            print("{ERROR} - The element list passed to function "
                  "click_a_tree_node seems empty")
        return operation_status

    def toggle_open_servers_group(self):
        """This will open Servers group to display underlying nodes"""
        is_expanded = False
        self.wait_for_spinner_to_disappear()
        if self.check_if_element_exist_by_xpath(
                TreeAreaLocators.server_group_node):
            if self.get_expansion_status_of_node(
                    TreeAreaLocators.server_group_node_exp_status):
                is_expanded = True
            else:
                webdriver.ActionChains(self.driver).double_click(
                    self.find_by_xpath(
                        TreeAreaLocators.
                        server_group_node)).perform()
                if self.check_if_element_exist_by_xpath(
                        TreeAreaLocators.server_group_sub_nodes):
                    is_expanded = True
                else:
                    print(
                        "(toggle_open_servers_group)The Server Group "
                        "node is clicked to expand but it is not expanded",
                        file=sys.stderr)
        else:
            print("The Server Group node is not visible",
                  file=sys.stderr)
        return is_expanded

    def expand_server_node(self, server_name, server_password):
        """will expand a particular server node"""
        server_node_expansion_status = False
        if self.toggle_open_servers_group():
            if self.wait_for_elements_to_appear(
                self.driver,
                    TreeAreaLocators.server_group_sub_nodes):
                subnodes_of_servers = self.find_by_xpath_list(
                    TreeAreaLocators.server_group_sub_nodes)
                subnodes_of_servers_expansion_status = \
                    self.find_by_xpath_list(
                        TreeAreaLocators.
                        server_group_sub_nodes_exp_status)
                index_of_server_node = self.get_index_of_element(
                    subnodes_of_servers, server_name)

                server_node_expansion_status = self.click_expand_server_node(
                    subnodes_of_servers_expansion_status,
                    index_of_server_node,
                    subnodes_of_servers,
                    server_name,
                    server_password)
        else:
            print(
                "(expand_server_node) The Servers node is"
                " not expanded",
                file=sys.stderr)
        return server_node_expansion_status

    def click_expand_server_node(self, subnodes_of_servers_expansion_status,
                                 index_of_server_node, subnodes_of_servers,
                                 server_name, server_password):
        """
        Method actually clicks on server node to expand
        :param subnodes_of_servers_expansion_status:
        :param index_of_server_node:
        :param subnodes_of_servers:
        :param server_name:
        :param server_password:
        :return: True is click action is successful & server node expanded
        """
        server_node_expansion_status = False
        if not self.check_server_is_connected(
                index_of_server_node):
            if self.click_and_connect_server(
                subnodes_of_servers[index_of_server_node],
                    server_password):
                server_node_expansion_status = True
            else:
                print(
                    "(expand_server_node)The server node is "
                    "not expanded",
                    file=sys.stderr)
        else:
            if not self.get_expansion_status_of_node_element(
                subnodes_of_servers_expansion_status[
                    index_of_server_node]):
                webdriver.ActionChains(self.driver).double_click(
                    subnodes_of_servers[
                        index_of_server_node]).perform()
                if self.wait_for_elements_to_appear(
                    self.driver, TreeAreaLocators.
                        sub_nodes_of_a_server_node(server_name),
                        30):
                    server_node_expansion_status = True
            else:
                server_node_expansion_status = True
        return server_node_expansion_status

    def expand_databases_node(self, server_name, server_password):
        """will expand databases node under server node"""
        databases_node_expanded = False
        if self.expand_server_node(server_name, server_password):
            if self.wait_for_elements_to_appear(
                self.driver,
                    TreeAreaLocators.sub_nodes_of_a_server_node(server_name)):
                subnodes_of_server_node = self.find_by_xpath_list(
                    TreeAreaLocators.sub_nodes_of_a_server_node(server_name))
                subnode_of_server_node_exp_status = self.find_by_xpath_list(
                    TreeAreaLocators.sub_nodes_of_a_server_node_exp_status(
                        server_name))
                index_of_databases_node = self.get_index_of_element(
                    subnodes_of_server_node,
                    "Databases")
                time.sleep(2)
                expansion_status = self.get_expansion_status_of_node_element(
                    subnode_of_server_node_exp_status[index_of_databases_node])
                if not expansion_status:
                    databases_node_expanded = \
                        self.click_to_expand_databases_node(
                            subnodes_of_server_node,
                            index_of_databases_node,
                            server_name)
                else:
                    databases_node_expanded = True
        else:
            print("The server/previous nodes not expanded",
                  file=sys.stderr)
        return databases_node_expanded

    def click_to_expand_databases_node(self, subnodes_of_server_node,
                                       index_of_databases_node, server_name):
        """
        Method clicks on databases node of specified server to expand
        :param subnodes_of_server_node:
        :param index_of_databases_node:
        :param server_name:
        :return: True if database node click is successful & expanded
        """
        retry = 5
        databases_node_expanded = False
        while retry > 0:
            webdriver.ActionChains(self.driver).double_click(
                subnodes_of_server_node[
                    index_of_databases_node].find_element_by_xpath(
                    ".//*[@class='aciTreeItem']")
            ).perform()
            if self.wait_for_elements_to_appear(
                self.driver, TreeAreaLocators.
                    sub_nodes_of_databases_node(server_name), 3):
                databases_node_expanded = True
                break
            else:
                retry -= 1
        return databases_node_expanded

    def click_to_expand_database_node(self, sub_nodes_of_databases_node,
                                      index_of_required_db_node,
                                      name_of_database):
        """
        Method clicks on specified database name from expanded databases node
        of server.
        :param sub_nodes_of_databases_node:
        :param index_of_required_db_node:
        :param name_of_database:
        :return: True if particular database click is successful & expanded
        """
        retry = 5
        db_node_expanded_status = False
        self.driver.execute_script(self.js_executor_scrollintoview_arg,
                                   sub_nodes_of_databases_node[
                                       index_of_required_db_node])
        while retry > 0:
            webdriver.ActionChains(self.driver).double_click(
                sub_nodes_of_databases_node[
                    index_of_required_db_node]).perform()
            if self.check_if_element_exist_by_xpath(
                "//div[@class='ajs-header'and text()='INTERNAL SERVER "
                    "ERROR']", 1):
                try:
                    self.click_modal('OK')
                except Exception:
                    pass
                retry -= 1
            else:
                break
        if self.wait_for_elements_to_appear(
            self.driver, TreeAreaLocators.
                sub_nodes_of_database_node(
                name_of_database)):
            db_node_expanded_status = True
        return db_node_expanded_status

    def expand_database_node(self, server_name, server_password,
                             name_of_database):
        """will expand database node under databases node"""
        db_node_expanded_status = False
        if self.expand_databases_node(server_name, server_password):
            sub_nodes_of_databases_node = self.find_by_xpath_list(
                TreeAreaLocators.sub_nodes_of_databases_node(server_name))
            index_of_required_db_node = self.get_index_of_element(
                sub_nodes_of_databases_node,
                name_of_database)
            expansion_status = self.get_expansion_status_of_node_element(
                self.find_by_xpath_list(
                    TreeAreaLocators.
                    sub_nodes_of_databases_node_exp_status(
                        server_name))[
                    index_of_required_db_node])
            if not expansion_status:
                db_node_expanded_status = self.click_to_expand_database_node(
                    sub_nodes_of_databases_node, index_of_required_db_node,
                    name_of_database)
            else:
                db_node_expanded_status = True
        else:
            print("The databases/previous nodes not expanded",
                  file=sys.stderr)
        return db_node_expanded_status

    def toggle_open_schemas_node(self, server_name, server_password,
                                 name_of_database):
        """will expand schemas node under a db node"""
        expansion_status = False
        if self.expand_database_node(server_name, server_password,
                                     name_of_database):
            sub_nodes_db_node = self.find_by_xpath_list(
                TreeAreaLocators.sub_nodes_of_database_node(
                    name_of_database))
            index_of_schemas_node = self.get_index_of_element(
                sub_nodes_db_node, "Schemas")
            expansion_status = self.get_expansion_status_of_node_element(
                self.find_by_xpath_list(
                    TreeAreaLocators.sub_nodes_of_database_node_exp_status(
                        name_of_database))[
                    index_of_schemas_node])
            if not expansion_status:
                self.driver.execute_script(
                    self.js_executor_scrollintoview_arg,
                    sub_nodes_db_node[index_of_schemas_node])
                webdriver.ActionChains(self.driver).double_click(
                    sub_nodes_db_node[index_of_schemas_node]).perform()
                if self.wait_for_elements_to_appear(
                    self.driver, TreeAreaLocators.
                        sub_nodes_of_schemas_node(name_of_database)):
                    expansion_status = True
            else:
                expansion_status = True
        else:
            print(
                "(expand_schemas_node) database/previous nodes "
                "are not expanded",
                file=sys.stderr)
        return expansion_status

    def toggle_open_schema_node(
        self, server_name, server_password,
            name_of_database, name_of_schema_node):
        """will expand schema node under schemas node"""
        expansion_status = False
        if self.toggle_open_schemas_node(
                server_name, server_password, name_of_database):
            sub_nodes_schemas_node = self.find_by_xpath_list(
                TreeAreaLocators.sub_nodes_of_schemas_node(
                    name_of_database))
            index_of_schema_node = self.get_index_of_element(
                sub_nodes_schemas_node,
                name_of_schema_node)
            expansion_status = self.get_expansion_status_of_node_element(
                self.find_by_xpath_list(
                    TreeAreaLocators.sub_nodes_of_schemas_node_exp_status(
                        name_of_database))[
                    index_of_schema_node])
            if not expansion_status:
                self.driver.execute_script(
                    self.js_executor_scrollintoview_arg,
                    sub_nodes_schemas_node[index_of_schema_node])
                webdriver.ActionChains(self.driver).double_click(
                    sub_nodes_schemas_node[index_of_schema_node]).perform()
                if self.wait_for_elements_to_appear(
                    self.driver, TreeAreaLocators.
                        sub_nodes_of_schema_node(name_of_database)):
                    expansion_status = True
            else:
                expansion_status = True
        else:
            print(
                "(expand_schema_node) schema/previous nodes are"
                " not expanded",
                file=sys.stderr)
        return expansion_status

    def toggle_open_tables_node(
        self, server_name, server_password,
            name_of_database, name_of_schema_node):
        """will expand tables node under schema node"""
        node_expanded_successfully = False
        if self.toggle_open_schema_node(
            server_name, server_password, name_of_database,
                name_of_schema_node):
            sub_nodes_of_schema_node = self.find_by_xpath_list(
                TreeAreaLocators.sub_nodes_of_schema_node(
                    name_of_database))
            sub_nodes_of_schema_node_exp_status = self.find_by_xpath_list(
                TreeAreaLocators.sub_nodes_of_schema_node_exp_status(
                    name_of_database))
            index_of_tables_node = self.get_index_of_element(
                sub_nodes_of_schema_node, "Tables")
            expansion_status = self.get_expansion_status_of_node_element(
                sub_nodes_of_schema_node_exp_status[index_of_tables_node])
            if not expansion_status:
                self.driver.execute_script(self.js_executor_scrollintoview_arg,
                                           sub_nodes_of_schema_node[
                                               index_of_tables_node])
                webdriver.ActionChains(self.driver).double_click(
                    sub_nodes_of_schema_node[
                        index_of_tables_node]).perform()
                if self.wait_for_elements_to_appear(
                    self.driver, TreeAreaLocators.
                        sub_nodes_of_tables_node):
                    node_expanded_successfully = True
            else:
                node_expanded_successfully = True
        else:
            print(
                "(expand_tables_node) schema/previous nodes "
                "are not expanded",
                file=sys.stderr)
        return node_expanded_successfully

    def toggle_open_function_node(self):
        """The function will be used for opening Functions node only"""
        node_expanded = False
        attempts = 3

        xpath_for_functions_node = \
            "//span[@class='aciTreeText' and starts-with(text()," \
            "'Functions')]"
        xpath_for_exp = "//div[div[div[div[div[div[div[div[span[span[" \
                        "(@class='aciTreeText') and starts-with(text()," \
                        "'Functions')]]]]]]]]]]"
        xpath_for_button = "//div[span[span[(@class='aciTreeText') " \
                           "and starts-with(text(),'Functions')]]]" \
                           "/span[@class='aciTreeButton']"

        while node_expanded is not True and attempts > 0:
            # get the element which contains 'aria-expanded' info

            xpath_for_refresh_btn = "//li[@class='context-menu-item']" \
                                    "/span[text()='Refresh...']"

            # add code to refresh button, sometime the the collapsing button
            #  is not visible even if there is sub node.
            functions_node_ele = self.find_by_xpath(xpath_for_functions_node)

            webdriver.ActionChains(self.driver).move_to_element(
                functions_node_ele).context_click().perform()
            refresh_btn = self.find_by_xpath(xpath_for_refresh_btn)
            refresh_btn.click()
            time.sleep(.5)

            # get the expansion status
            function_expansion_ele = self.find_by_xpath(xpath_for_exp)

            # look into the attribute and check if it is already expanded or
            #  not
            if function_expansion_ele.get_attribute('aria-expanded') \
                    == 'false':
                # button element of the Function node to open it
                item_button = self.find_by_xpath(xpath_for_button)
                ActionChains(self.driver).click(item_button).perform()
                # Expansion of element on GUI takes sometime, so put small
                # sleep
                time.sleep(.5)
                function_expansion_ele = self.find_by_xpath(
                    xpath_for_exp)
                if function_expansion_ele.get_attribute('aria-expanded') \
                        == 'true':
                    break
                else:
                    attempts -= 1
            else:
                node_expanded = True

    def check_server_is_connected(self, index_of_server):
        """This will check connected status of a server, as connection
        status is contained either in span or div element so checking it"""
        server_connected = False
        try:
            connection_status_elements = self.find_by_xpath_list(
                TreeAreaLocators.server_group_sub_nodes_connected_status)
            span_elements = connection_status_elements[
                index_of_server].find_elements_by_tag_name("span")
            div_elements = connection_status_elements[
                index_of_server].find_elements_by_tag_name("div")

            span_value_of_class_att = ""
            div_value_of_class_att = ""

            if len(span_elements) > 0:
                span_value_of_class_att = \
                    span_elements[0].get_attribute('class')
            if len(div_elements) > 0:
                div_value_of_class_att = \
                    div_elements[0].get_attribute('class')
            if (("aciTreeIcon icon-pg" in span_value_of_class_att or
                 "aciTreeIcon icon-pg" in div_value_of_class_att or
                 "aciTreeIcon icon-ppas" in
                 span_value_of_class_att or
                 "aciTreeIcon icon-ppas" in div_value_of_class_att) and
                    ("aciTreeIcon icon-server-not-connected" not in
                        span_value_of_class_att or
                        "aciTreeIcon icon-server-not-connected" not in
                        div_value_of_class_att)):
                server_connected = True
        except Exception as e:
            print("There is some exception thrown in the function "
                  "check_server_is_connected and is: " + str(e),
                  file=sys.stderr)
        return server_connected

    def click_and_connect_server(self, server_element, password):
        """will connect a server node, will provide the password in the
        respective window"""
        server_connection_status = False
        try:
            webdriver.ActionChains(self.driver).double_click(
                server_element).perform()
            if self.check_if_element_exist_by_xpath(
                    ConnectToServerDiv.ok_button):
                field = self.find_by_xpath(
                    ConnectToServerDiv.password_field)
                self.fill_input(field, password)
                self.find_by_xpath(ConnectToServerDiv.ok_button).click()
                self.wait_for_element_to_disappear(
                    lambda driver: driver.find_element_by_xpath(
                        ConnectToServerDiv.ok_button))
                if self.check_if_element_exist_by_xpath(
                        ConnectToServerDiv.error_message, 2):
                    print(
                        "While entering password in click_and_connect_server "
                        "function, error is occurred : " + str(
                            self.find_by_xpath(
                                ConnectToServerDiv.error_message).text),
                        file=sys.stderr)
                else:
                    server_connection_status = True
        except Exception as e:
            print(
                "There is some exception thrown click_and_connect_server "
                "and is: " + str(
                    e), file=sys.stderr)
        return server_connection_status

    def get_expansion_status_of_node(self, xpath_node):
        """get the expansion status for a node through xpath"""
        node_is_expanded = False
        element = self.find_by_xpath(xpath_node)
        if element.get_attribute("aria-expanded") == 'true':
            node_is_expanded = True
        return node_is_expanded

    def get_expansion_status_of_node_element(self, element):
        """get the expansion status for an element"""
        node_is_expanded = False
        try:
            if element.get_attribute("aria-expanded") == 'true':
                node_is_expanded = True
        except Exception as e:
            print(
                "There is some exception thrown in the function "
                "get_expansion_status_of_node_element and is: " + str(
                    e), file=sys.stderr)
        return node_is_expanded

    def toggle_open_tree_item(self, tree_item_text):
        # 'sleep' here helps in cases where underlying nodes are auto opened.
        # Otherwise, encountered situations where False value is returned
        # even if the underlying node to be clicked was Opened.
        time.sleep(.6)
        item_with_text = self.find_by_xpath(
            TreeAreaLocators.specified_tree_node.format(tree_item_text))

        self.driver.execute_script(self.js_executor_scrollintoview_arg,
                                   item_with_text)

        if item_with_text.find_element_by_xpath(
            ".//ancestor::*[@class='aciTreeLine']").get_attribute(
                "aria-expanded") == 'false':
            item = item_with_text.find_element_by_xpath(
                ".//parent::*[@class='aciTreeItem']")
            ActionChains(self.driver).double_click(item).perform()
        retry = 3
        while retry > 0:
            try:
                WebDriverWait(self.driver, 5).until((lambda item_with_text: (
                    item_with_text.find_element_by_xpath(
                        ".//ancestor::*[@class='aciTreeLine']").
                    get_attribute("aria-expanded") == 'true')))
                break
            except TimeoutException:
                retry -= 1

    def toggle_open_server(self, tree_item_text):
        def check_for_password_dialog_or_tree_open(driver):
            try:
                dialog = driver.find_element_by_id("frmPassword")
            except WebDriverException:
                dialog = None
            try:
                database_node = driver.find_element_by_xpath(
                    "//*[@id='tree']//*[.='Databases']"
                    "/../*[@class='aciTreeButton']")
            except WebDriverException:
                database_node = None

            return dialog is not None or database_node is not None

        self.toggle_open_tree_item(tree_item_text)
        self._wait_for("Waiting for password dialog or tree to open",
                       check_for_password_dialog_or_tree_open)

        try:
            self.driver.find_element_by_id("frmPassword")
            # Enter password here if needed
            self.click_modal('OK')
        except WebDriverException:
            return

    def find_by_xpath(self, xpath):
        return self.wait_for_element(
            lambda driver: driver.find_element_by_xpath(xpath)
        )

    def find_by_id(self, element_id):
        return self.wait_for_element(
            lambda driver: driver.find_element_by_id(element_id)
        )

    def find_by_css_selector(self, css_selector):
        return self.wait_for_element(
            lambda driver: driver.find_element_by_css_selector(css_selector)
        )

    def find_by_partial_link_text(self, link_text):
        return self._wait_for(
            'link with text "{0}"'.format(link_text),
            EC.element_to_be_clickable((By.PARTIAL_LINK_TEXT, link_text))
        )

    def click_element(self, element):
        # driver must be here to adhere to the method contract in
        # selenium.webdriver.support.wait.WebDriverWait.until()
        def click_succeeded(driver):
            try:
                element.click()
                return True
            except WebDriverException:
                return False

        return self._wait_for(
            "clicking the element not to throw an exception", click_succeeded
        )

    def js_send_key(self, field, sele_key):
        keycode = None

        if sele_key in (Keys.RETURN, Keys.ENTER):
            keycode = 13
        elif sele_key == Keys.ARROW_DOWN:
            keycode = 40

        self.driver.execute_script(
            "arguments[0].dispatchEvent(new KeyboardEvent('keydown', "
            "{'keyCode':arguments[1], 'which':arguments[1]}));"
            "arguments[0].dispatchEvent(new KeyboardEvent('keypress', "
            "{'keyCode':arguments[1], 'which':arguments[1]}));"
            "arguments[0].dispatchEvent(new KeyboardEvent('keyup', "
            "{'keyCode':arguments[1], 'which':arguments[1]}));"
            "arguments[0].dispatchEvent(new Event('input'));"
            "arguments[0].dispatchEvent(new Event('change'));",
            field, keycode)

    def js_loose_focus(self, field):
        self.driver.execute_script(
            "arguments[0].dispatchEvent(new Event('blur'));", field)

    def fill_input(self, field, field_content, input_keys=False,
                   key_after_input=Keys.ARROW_DOWN):
        try:
            attempt = 0
            for attempt in range(0, 3):
                field.click()
                break
        except Exception as e:
            time.sleep(.2)
            if attempt == 2:
                raise e
        # Use send keys if input_keys true, else use javascript to set content
        if input_keys:
            backspaces = [Keys.BACKSPACE] * len(field.get_attribute('value'))
            field.send_keys(backspaces)
            field.send_keys(str(field_content))
            self.wait_for_input_by_element(field, field_content)
        else:
            self.driver.execute_script("arguments[0].value = arguments[1]",
                                       field, field_content)
            # keycode 40 is for arrow down
            self.js_send_key(field, Keys.ARROW_DOWN)

            if key_after_input:
                self.js_send_key(field, key_after_input)

    def fill_input_by_field_name(self, field_name, field_content,
                                 input_keys=False,
                                 key_after_input=Keys.ARROW_DOWN,
                                 loose_focus=False):
        field = self.find_by_css_selector(
            "input[name='" + field_name + "']:not(:disabled)")
        self.fill_input(field, field_content, input_keys=input_keys,
                        key_after_input=key_after_input)

        if loose_focus:
            self.js_loose_focus(field)

    def fill_input_by_css_selector(self, css_selector, field_content,
                                   input_keys=False,
                                   key_after_input=Keys.ARROW_DOWN,
                                   loose_focus=False):
        field = self.find_by_css_selector(css_selector)
        self.fill_input(field, field_content, input_keys=input_keys,
                        key_after_input=key_after_input)

        if loose_focus:
            self.js_loose_focus(field)

    def fill_codemirror_area_with(self, field_content, input_keys=False):
        def find_codemirror(driver):
            try:
                driver.switch_to.default_content()
                driver.switch_to_frame(
                    driver.find_element_by_tag_name("iframe"))
                element = driver.find_element_by_css_selector(
                    "#output-panel .CodeMirror")
                if element.is_displayed() and element.is_enabled():
                    return element
            except (NoSuchElementException, WebDriverException):
                return False
        time.sleep(1)
        self.wait_for_query_tool_loading_indicator_to_disappear(12)

        retry = 2
        while retry > 0:
            try:
                self.driver.switch_to.default_content()
                WebDriverWait(self.driver, 10).until(
                    EC.frame_to_be_available_and_switch_to_it(
                        (By.TAG_NAME, "iframe")))
                self.find_by_xpath("//a[text()='Query Editor']").click()

                codemirror_ele = WebDriverWait(
                    self.driver, timeout=self.timeout, poll_frequency=0.01) \
                    .until(find_codemirror,
                           "Timed out waiting for codemirror to appear")
                codemirror_ele.click()
                retry = 0
            except WebDriverException as e:
                print("Exception in filling code mirror {0} ".format(retry),
                      file=sys.stderr)
                print(str(e))
                if retry == 1:
                    raise e
                retry -= 1

        # Use send keys if input_keys true, else use javascript to set content
        if input_keys:
            action = ActionChains(self.driver)
            action.send_keys(field_content)
            action.perform()
        else:
            self.driver.execute_script(
                "arguments[0].CodeMirror.setValue(arguments[1]);"
                "arguments[0].CodeMirror.setCursor("
                "arguments[0].CodeMirror.lineCount(),0);",
                codemirror_ele, field_content)

    def click_tab(self, tab_name):
        WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(
            (By.XPATH, NavMenuLocators.select_tab_xpath.format(tab_name))))
        click_tab = True
        while click_tab:
            tab = self.find_by_xpath(
                NavMenuLocators.select_tab_xpath.format(tab_name))
            self.click_element(tab)
            if 'wcPanelTabActive' in tab.get_attribute('class'):
                break

    def wait_for_input_by_element(self, element, content):
        def input_field_has_content(driver):
            return str(content) == element.get_attribute('value')

        return self._wait_for(
            "field to contain '" + str(content) + "'", input_field_has_content
        )

    def wait_for_input_field_content(self, field_name, content, wait=1):
        def input_field_has_content(driver):
            element = driver.find_element_by_xpath(
                "//input[@name='" + field_name + "']")

            return str(content) == element.get_attribute('value')

        return self._wait_for(
            "field to contain '" + str(content) + "'", input_field_has_content,
            wait)

    def check_if_element_exist_by_xpath(self, xpath, timeout=5):
        """This function will verify if an element exist and on that basis
        will return True or False. Will handle exception internally"""
        element_found = False
        try:
            WebDriverWait(self.driver, timeout, .01).until(
                EC.visibility_of_element_located((By.XPATH, xpath)))
            element_found = True
        except Exception:
            pass
        return element_found

    def wait_for_element(self, find_method_with_args):
        def element_if_it_exists(driver):
            try:
                element = find_method_with_args(driver)
                if element.is_displayed():
                    return True
            except (NoSuchElementException, StaleElementReferenceException):
                return False

        self._wait_for("element to exist", element_if_it_exists)
        return find_method_with_args(self.driver)

    def wait_for_element_to_disappear(self, find_method_with_args, timeout=5):
        def element_if_it_disappears(driver):
            try:
                element = find_method_with_args(driver)
                if element.is_displayed() and element.is_enabled():
                    return False

                return True
            except (NoSuchElementException, StaleElementReferenceException):
                return True

        return self._wait_for("element to disappear",
                              element_if_it_disappears, timeout)

    def wait_for_reloading_indicator_to_disappear(self):
        def reloading_indicator_has_disappeared(driver):
            try:
                driver.find_element_by_id("reloading-indicator")
                return False
            except NoSuchElementException:
                return True

        self._wait_for("reloading indicator to disappear",
                       reloading_indicator_has_disappeared)

    def wait_for_spinner_to_disappear(self):
        def spinner_has_disappeared(driver):
            try:
                driver.find_element_by_id("pg-spinner")
                return False
            except NoSuchElementException:
                return True

        self._wait_for("spinner to disappear", spinner_has_disappeared, 20)

    def wait_for_query_tool_loading_indicator_to_disappear(self, timeout=20):
        def spinner_has_disappeared(driver):
            try:
                spinner = driver.find_element_by_css_selector(
                    "#editor-panel .pg-sp-container"
                )
                return "d-none" in spinner.get_attribute("class")
            except NoSuchElementException:
                # wait for loading indicator disappear animation to complete.
                time.sleep(0.5)
                return True

        self._wait_for(
            "spinner to disappear", spinner_has_disappeared, timeout)

    def wait_for_query_tool_loading_indicator_to_appear(self):
        status = self.check_if_element_exist_by_xpath(
            "//div[@id='editor-panel']//"
            "div[@class='pg-sp-container sql-editor-busy-fetching']", 1)
        return status

    def wait_for_app(self):
        def page_shows_app(driver):
            if driver.title == self.app_config.APP_NAME:
                return True
            else:
                driver.refresh()
                return False

        self._wait_for("app to start", page_shows_app, self.app_start_timeout)

    def wait_for_element_to_reload(self, element_selector):
        WebDriverWait(self.driver, 20) \
            .until(EC.staleness_of(element_selector(self.driver)))
        WebDriverWait(self.driver, 20) \
            .until_not(EC.staleness_of(element_selector(self.driver)))

        return element_selector(self.driver)

    def _wait_for(self, waiting_for_message, condition_met_function,
                  timeout=5):
        if timeout is None:
            timeout = self.timeout
        return WebDriverWait(self.driver, timeout, 0.01).until(
            condition_met_function,
            "Timed out waiting for " + waiting_for_message
        )

    def wait_for_elements(self, find_method_with_args):
        """Using xpath, it will wait for elements"""
        def element_if_it_exists(driver):
            try:
                element = find_method_with_args(driver)
                if len(element) > 0 and element[0].is_displayed() and element[
                        0].is_enabled():
                    return element
            except NoSuchElementException:
                return False

        return self._wait_for("element to exist", element_if_it_exists)

    def wait_for_elements_to_appear(self, driver, locator, time_value=20):
        """This will wait until list of elements or an element is visible,
        The time out value is userdefined"""
        elements_located_status = False
        try:
            if WebDriverWait(driver, time_value).until(
                    EC.visibility_of_any_elements_located((
                        By.XPATH, locator))):
                elements_located_status = True
        except Exception:
            pass
        return elements_located_status

    def find_by_xpath_list(self, xpath):
        """This will find out list of elements through a single xpath"""
        return self.wait_for_elements(
            lambda driver: driver.find_elements_by_xpath(xpath))

    def get_index_of_element(self, element_list, target_string):
        """it will return index of an element from provided element list"""
        index_of_required_server = -1
        if len(element_list) > 0:
            for index, element in enumerate(element_list):
                if element.text.startswith(target_string) and (
                        target_string in element.text):
                    index_of_required_server = index
                    break
        else:
            print("There seems no record in the provided element list")
        return index_of_required_server

    def set_switch_box_status(self, switch_box, required_status):
        """it will change switch box status to required one. Two elements
        of the switch boxes are to be provided i) button which is needed to
         toggle ii) Yes for True or No for False"""
        status_changed_successfully = False
        switch_box_element = self.find_by_xpath(switch_box)

        if required_status == 'Yes':
            status_changed_successfully = \
                self.toggle_switch_box(switch_box_element,
                                       expected_attr_in_class_tag='success',
                                       unexpected_attr_in_class_tag='off')
        else:
            status_changed_successfully = \
                self.toggle_switch_box(switch_box_element,
                                       expected_attr_in_class_tag='off',
                                       unexpected_attr_in_class_tag='success')
        return status_changed_successfully

    def toggle_switch_box(self, switch_box_ele, expected_attr_in_class_tag,
                          unexpected_attr_in_class_tag):
        """
        Method toggles switch box status using attributes from class tag
        :param switch_box_ele:
        :param expected_attr_in_class_tag: e.g. 'off', success
        :param unexpected_attr_in_class_tag: e.g. 'off', success
        :return: True if class tag attribute is to expected attribute value
        in class tag.
        """
        status_changed = False
        if unexpected_attr_in_class_tag in switch_box_ele.get_attribute(
                "class"):
            switch_box_ele.click()
            time.sleep(1)
            if expected_attr_in_class_tag in switch_box_ele.get_attribute(
                    "class"):
                status_changed = True
            else:
                print(
                    "(set_switch_box_status)Clicked the "
                    "element to change its status but "
                    "it did not changed",
                    file=sys.stderr)
        elif expected_attr_in_class_tag in switch_box_ele.get_attribute(
                "class"):
            status_changed = True
        return status_changed

    def retry_click(self, click_locator, verify_locator):
        click_status = False
        attempt = 0

        while click_status is not True and attempt < 10:
            try:
                element = self.driver.find_element(*click_locator)
                element.click()
                WebDriverWait(self.driver, 10).until(
                    EC.visibility_of_element_located(verify_locator))
                click_status = True
            except Exception:
                attempt += 1
        return click_status

    def paste_values(self, el=None):
        """
        Function paste values in scratch pad
        :param el:
        """
        actions = ActionChains(self.driver)
        if "platform" in self.driver.capabilities:
            platform = (self.driver.capabilities["platform"]).lower()
        elif "platformName" in self.driver.capabilities:
            platform = (self.driver.capabilities["platformName"]).lower()
        if el:
            # Must step
            el.click()
            if 'mac' in platform:
                # FF step
                el.send_keys(Keys.COMMAND + "v")
                # Chrome Step
                actions.key_down(Keys.SHIFT)
                actions.send_keys(Keys.INSERT)
                actions.key_up(Keys.SHIFT)
                actions.perform()
            else:
                el.send_keys(Keys.CONTROL + "v")

    def wait_for_element_to_be_visible(self, driver, xpath, time_value=20):
        """This will wait until an element is visible on page"""
        element_located_status = False
        try:
            if WebDriverWait(driver, time_value).until(
                    EC.visibility_of_element_located((By.XPATH, xpath))):
                element_located_status = True
        except TimeoutException:
            element_located_status = False
        return element_located_status


import socket  # importing all the necessary network modules

HOST = "127.0.0.1"  # allows the machine to communicate with itself (i.e., localhost)
PORT = 65432  # port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:  # AF_INET is an address family to tell
    # what type of address the socket can communicate with
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print(f"Connected by {addr}")
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)


while 1:
    string = input()
    if string == "# 0 0":
        break
    else:
        name, age, weight = string.split()
        if int(age) > 17 or int(weight) >= 80:
            print("%s Senior" % name)
        else:
            print("%s Junior" % name)