from __future__ import print_function

import importlib
import hashlib
import os
import random
import string
import sys
import time
from collections import defaultdict

import conllu_dataset
import utilities

class Options:

    def __init__(self):
        path = sys.argv[0]
        if '/' not in path:
            path = './' + path
        self.scriptdir, self.scriptname = path.rsplit('/', 1)
        need_help = self.read_options()
        if need_help:
            self.print_usage()
            sys.exit()

    def print_usage(self):
        print('Usage: %s [options]' %self.scriptname)
        print("""
Options:

    --final-test            Also make predictions for the test sets of each
                            data set.
                            (Default: only report development set results)

    --all-heuristics        Allow configurations that use any of the
                            available copy-to-enhanced heuristics.
                            (Default: restrict configurations to those
                            combinations of heuristics that are configured
                            in this script)

    --best-basic            Only use configurations that use a best basic
                            parser as specified in the language-specific
                            configurations in this script.
                            (Default: try all basic parser combinations)

    --use-allennlp          Allow configurations that use the allennlp
                            semantic parser for enhanced parsing.
                            (Default: only try the copy-to-enhanced
                            heuristic as a first step to find out which
                            basic parsers are best)

    --skip-training  NAME   Skip training models for component NAME.
                            Can be one of segmenter, basic_parser and
                            enhanced_parser, and can be repeated to skip
                            training for more than one component.
                            Specifying a space- or colon-separated list
                            of components is also supported.
                            The special name ALL is expanded to all 3
                            component types.
                            Skipping can be restricted to a subcompontent
                            with a dot, e.g. "basic_parser.plain_udpf".
                            (Default: train all missing models)

    --final-run             Short-hand for --final-test --all-heuristics
                            --best-basic --use-allennlp --skip-training ALL

    --training-only         Quit after training tasks have been submitted.
                            (Default: Wait for tasks to finish and continue
                            with prediction and development set
                            evaluation.)

    --workdir  DIR          Path to working directory.
                            Note that some modules require absolute paths.
                            (Default: $PWD/data)

    --taskdir  DIR          Path to shared task data containing a UD-like
                            folder structure with training and dev files.
                            With --final-test, we assume that test files
                            follow the naming and structure of the dev data
                            and can be merged into the train-dev folder
                            provided by the shared task.
                            (Default: workdir + /train-dev)

    --ud25dir  DIR          Path to the UD v2.5 folder
                            (Default: workdir + /ud-treebanks-v2.5)

    --tempdir  DIR          Path to directory for intermediate files
                            (Default: workdir + /temp)

    --modeldir  DIR         Path to model directory
                            (Default: workdir + /models)

    --predictdir  DIR       Path to prediction directory
                            (Default: workdir + /predictions)

    --init-seed  STRING     Initialise random number generator with STRING.
                            If STRING is empty, use system seed.
                            Will be passed to basic parsers for training
                            with a
                            2-digit suffix. If, for example, the parser can
                            only accepts seeds up to 32767 this seed must
                            not exceed 327, assuming no more than 67 models
                            per treebank are trained, or 326 otherwise.
                            (Default: 42)

    --verbose               More detailed log output

    --stopfile  FILE        Stop processing if FILE exists, tested after
                            every enhanced prediction
                            (default: stop.me in the current folder)

""")

    def read_options(self):
        self.final_test = False
        self.modules_not_to_train = set()
        self.training_only = False
        self.all_heuristics = False
        self.best_basic     = False
        self.use_allennlp   = False
        self.workdir    = None
        self.taskdir    = None
        self.ud25dir    = None
        self.tempdir    = None
        self.modeldir   = None
        self.predictdir = None
        self.init_seed  = 42
        self.verbose    = False
        self.debug      = True
        self.stopfile   = 'stop.me'
        while len(sys.argv) >= 2 and sys.argv[1][:1] == '-':
            option = sys.argv[1]
            option = option.replace('_', '-')
            del sys.argv[1]
            if option in ('--help', '-h'):
                return True
            elif option == '--final-test':
                self.final_test = True
            elif option == '--skip-training':
                names = sys.argv[1].replace(':', ' ').split()
                if 'ALL' in names:
                    names = 'segmenter basic_parser enhanced_parser'.split()
                for name in names:
                    self.modules_not_to_train.add(name)
                del sys.argv[1]
            elif option == '--workdir':
                self.workdir = sys.argv[1]
                del sys.argv[1]
            elif option == '--taskdir':
                self.taskdir = sys.argv[1]
                del sys.argv[1]
            elif option == '--tempdir':
                self.tempdir = sys.argv[1]
                del sys.argv[1]
            elif option == '--modeldir':
                self.modeldir = sys.argv[1]
                del sys.argv[1]
            elif option == '--predictdir':
                self.predictdir = sys.argv[1]
                del sys.argv[1]
            elif option == '--init-seed':
                self.init_seed = sys.argv[1]
                del sys.argv[1]
            elif option == '--training-only':
                self.training_only = True
            elif option in ('--best-basic', '--restrict-to-best-basic-parsers'):
                self.best_basic = True
            elif option in ('--all-heuristics', '--do-not-restrict-heuristics'):
                self.all_heuristics = True
            elif option in ('--use-allennlp', '--do-not-exclude-allennlp'):
                self.use_allennlp = True
            elif option == '--final-run':
                sys.argv.append('--final-test')
                sys.argv.append('--all-heuristics')
                sys.argv.append('--best-basic')
                sys.argv.append('--use-allennlp')
                sys.argv.append('--skip-training')
                sys.argv.append('ALL')
            elif option == '--stopfile':
                self.stopfile = sys.argv[1]
                del sys.argv[1]
            elif option == '--verbose':
                self.verbose = True
            elif option == '--debug':
                self.debug = True
            else:
                print('Unsupported or not yet implemented option %s' %option)
                return True
        if self.debug:
            self.verbose = True
        if self.workdir is None:
            self.workdir = '/'.join((os.getcwd(), 'data'))
        if self.taskdir is None:
            self.taskdir = '/'.join((self.workdir, 'train-dev'))
        if self.ud25dir is None:
            self.ud25dir = '/'.join((self.workdir, 'ud-treebanks-v2.5'))
        if self.tempdir is None:
            self.tempdir = '/'.join((self.workdir, 'temp'))
        if self.modeldir is None:
            self.modeldir = '/'.join((self.workdir, 'models'))
        if self.predictdir is None:
            self.predictdir = '/'.join((self.workdir, 'predictions'))
        if len(sys.argv) != 1:
            return True
        return False

    def scan_ud_or_task_folder(self, folder_path, folder_type):
        retval = {}
        for tbname in os.listdir(folder_path):
            if not tbname.startswith('UD_'):
                if self.verbose:
                    print('Skipping non-UD entry in %s: %s' %(folder_type, tbname))
                continue
            # Scan the treebank folder
            tbdir = '/'.join((folder_path, tbname))
            tbid_needs_models = False
            tbid = None
            training_data = None
            prediction_tasks = []
            dev_conllu = None
            for filename in os.listdir(tbdir):
                path = '/'.join((tbdir, filename))
                new_tbid = None
                dataset_type, dataset_format = None, None
                if '-ud-' in filename:
                    new_tbid, _, suffix = filename.rsplit('-', 2)
                    dataset_type, dataset_format = suffix.split('.')
                elif filename == 'tbid.txt':
                    f = open('%s/%s' %(tbdir, filename), 'r')
                    new_tbid = f.readline().strip()
                    f.close()
                    tbid_needs_models = True
                if new_tbid:
                    if '-' in new_tbid:
                        if self.verbose:
                            print('Dash in TBID %s replaced with underscore' %new_tbid)
                        new_tbid = new_tbid.replace('-', '_')
                    if tbid is not None and new_tbid != tbid:
                        raise ValueError('Mismatching TBIDs in %s' %tbdir)
                    tbid = new_tbid
                if dataset_format == 'txt' and dataset_type in ('dev', 'test'):
                    # found a prediction task
                    tbid_needs_models = True
                    prediction_tasks.append((path, dataset_type))
                if (dataset_type, dataset_format) == ('train', 'conllu'):
                    # found training data
                    training_data = path
                if (dataset_type, dataset_format) == ('dev', 'conllu'):
                    dev_conllu = path
            if tbid is None:
                print('Warning: No TBID found for %s %s. Please add `tbid.txt`.' %(folder_type, tbname))
            else:
                if tbid in retval:
                    print('Warning: Duplicate TBID %s causing information loss in %s' %(tbid, folder_path))
                retval[tbid] = (
                    tbdir, training_data, tbid_needs_models, prediction_tasks,
                    dev_conllu,
                )
        return retval

    def get_tasks_and_configs(self):
        self.task_treebanks = self.scan_ud_or_task_folder(
            self.taskdir, 'taskdir'
        )
        self.configs = {}
        for tbid in sorted(list(self.task_treebanks.keys())):
            tb_info = self.task_treebanks[tbid]
            _, _, tbid_needs_models, _, _ = tb_info
            if not tbid_needs_models:
                continue
            if self.debug:
                print('Preparing configurations for TBID', tbid)
                prediction_tasks = tb_info[3]
                print('%d prediction tasks:' %len(prediction_tasks))
                for item in sorted(prediction_tasks):
                    print('\t\t', item)
                training_data = tb_info[1]
                print('Training data:', training_data)
                dev_conllu = tb_info[4]
                print('Dev data:', dev_conllu)
            lcode = tbid.split('_')[0]
            # Find the most specific config available
            # TODO: Add language family as a layer between
            #       lcode and default.
            config_class = None
            for name in [tbid, lcode, 'default']:
                classname = 'Config_' + name
                try:
                    config_class = globals()[classname]
                    break
                except:
                    pass
            if config_class is None:
                raise ValueError('No config found in %r' %(globals().keys()))
            # create all config variants for this tbid
            # (we will produce exactly one prediction for each
            # test set and config variant)
            config_variants = config_class(
                tbid, lcode,
                options = self,
            ).get_variants()
            configs_for_tbid = []
            for variant in config_variants:
                if self.debug:
                    print('Preparing variant %s for TBID %s' %(variant, tbid))
                configs_for_tbid.append(config_class(
                    tbid, lcode,
                    options = self,
                    variant = variant,
                ))
            self.configs[tbid] = configs_for_tbid
        if self.debug:
            print('%d TBIDs configured' %len(self.configs))
            for key in sorted(list(self.configs.keys())):
                items = self.configs[key]
                print('\t', key, len(items))
                for item in sorted(items):
                    print('\t\t', item)

    def train_missing_models(self):
        if self.verbose:
            print('\n== Training Models ==\n')
        tasks = []
        self.in_progress = set()
        for tbid in sorted(list(self.configs.keys())):
            for config in self.configs[tbid]:
                if os.path.exists(self.stopfile):
                    return
                if not config.is_operational():
                    print('Not training %r as it is not operational' %config)
                    continue
                if not config.skip(self.modules_not_to_train):
                    tasks += config.train_missing_models()
                else:
                    print('Not training %r as user requested to skip it' %config)
        if self.verbose:
            print('Submitted %d training task(s)' %(len(tasks)-tasks.count(None)))
        if self.training_only:
            return
        utilities.wait_for_tasks(tasks)

    def scan_ud25(self):
        self.ud25_treebanks = self.scan_ud_or_task_folder(
            self.ud25dir, 'UD v2.5'
        )

    def predict_and_evaluate(self):
        if self.verbose:
            print('\n== Prediction and Evaluation ==\n')
        segment_dir = '/'.join((self.tempdir, 'segmented'))
        utilities.makedirs(segment_dir)
        basic_p_dir = '/'.join((self.tempdir, 'basic-parses'))
        utilities.makedirs(basic_p_dir)
        enhanced_dir = self.predictdir
        utilities.makedirs(enhanced_dir)
        for tbid in sorted(list(self.task_treebanks.keys())):
            prediction_tasks = self.task_treebanks[tbid][3]
            for input_path, prediction_dataset_type in prediction_tasks:
                print('\n=== %s ===\n' %input_path)
                if prediction_dataset_type == 'test' \
                and not self.final_test:
                    # no need to make prediction for test sets unless
                    # specifically requested with --final-test
                    continue
                tb_dir, text_filename = input_path.rsplit('/', 1)
                assert text_filename.startswith(tbid)
                assert text_filename.endswith('.txt')
                prediction_name = text_filename[:-4]
                for config in self.configs[tbid]:
                    print('\n==== %r ====\n' %config)
                    # segmentation
                    if config.segmenter_id is None:
                        print('Segmenter not ready, e.g. missing model')
                        continue
                    segmented_path = '%s/%s-for-%s.conllu' %(
                        segment_dir,
                        config.segmenter_id,
                        prediction_name
                    )
                    if not os.path.exists(segmented_path):
                        config.segment(input_path, segmented_path)
                    else:
                        print('Reusing existing segmentation')
                    if not os.path.exists(segmented_path):
                        print('Warning: Failure to produce %s' %segmented_path)
                        continue
                    # basic parsing
                    if config.basic_parser_id is None:
                        print('Basic parser not ready, e.g. missing model')
                        continue
                    basic_p_path = '%s/%s-%s-for-%s.conllu' %(
                        basic_p_dir,
                        config.segmenter_id,
                        config.basic_parser_id,
                        prediction_name
                    )
                    if not os.path.exists(basic_p_path):
                        config.parse(segmented_path, basic_p_path)
                    else:
                        print('Reusing existing basic parse')
                    if not os.path.exists(basic_p_path):
                        print('Warning: Failure to produce %s' %basic_p_path)
                        continue
                    # enhanced parsing
                    if config.enhanced_parser_id is None:
                        print('Enhanced parser not ready, e.g. missing model')
                        continue
                    enhanced_path = '%s/%s-%s-%s-for-%s.conllu' %(
                        enhanced_dir,
                        config.segmenter_id,
                        config.basic_parser_id,
                        config.enhanced_parser_id,
                        prediction_name
                    )
                    if not os.path.exists(enhanced_path):
                        config.enhance(basic_p_path, enhanced_path)
                    else:
                        print('Reusing existing enhanced parse')
                    if not os.path.exists(enhanced_path):
                        print('Warning: Failure to produce %s' %enhanced_path)
                        continue
                    # TODO: add post-processor here
                    # --
                    if os.path.exists(self.stopfile):
                        return
                    # evaluate: gold file is assumed to be in the same folder
                    # as the input text file with .conllu instead of .txt
                    if prediction_dataset_type == 'test':
                        continue
                    gold_path = '%s/%s.conllu' %(tb_dir, prediction_name)
                    conllu_dataset.evaluate(enhanced_path, gold_path, self)

class Config_default:

    def __init__(self, tbid, lcode, options,
        variant = None,
    ):
        self.tbid = tbid
        self.lcode = lcode
        self.variant = variant
        self.options = options
        self.segmenter = None
        self.basic_parsers = []
        self.enhanced_parser = None
        if variant is not None:
            self.init_segmenter()
            self.init_basic_parsers()
            self.init_enhanced_parser()

    def __repr__(self):
        return '<Default config for %r with variant %r>' %(
            self.tbid, self.variant
        )

    def is_operational(self):
        if self.segmenter is None:
            return False
        if not self.basic_parsers:
            return False
        if self.enhanced_parser is None:
            return False
        return True

    def get_variants(self):
        # Sub-classes can add parameters to the module name separated by a
        # colon (':'), e.g. to get variants that use the same module for
        #       a component but different training data, e.g.
        #       udpipe_standard with english-ewt and
        #       udpipe_standard with english-gum
        for segmenter in self.get_segmenters():
            basic_parsers = list(self.get_basic_parsers())
            # https://stackoverflow.com/questions/1482308/how-to-get-all-subsets-of-a-set-powerset
            n_basic_parsers = len(basic_parsers)
            combination_indices = []
            if n_basic_parsers > 64:
                for i in range(n_basic_parsers):
                    combination_indices.append(1<<i)
            #elif n_basic_parsers <= 12:
            #    combination_indices = range(1, 1 << n_basic_parsers)
            else:
                for i in range(n_basic_parsers):
                    combination_indices.append(1<<i)
                    for j in range(n_basic_parsers):
                        if i != j:
                            combination_indices.append((1<<i)+(1<<j))
            for combination_index in combination_indices:
                combination = [basic_parsers[j]
                    for j in range(n_basic_parsers)
                    if (combination_index & (1 << j))
                ]
                combination_size = len(combination)
                if combination_size == 2:
                    reverse_combination = (combination[1], combination[0])
                for enhanced_parser in self.get_enhanced_parsers():
                    for ensemble_size in self.get_ensemble_sizes():
                        if combination_size <= ensemble_size:
                            if self.options.best_basic \
                            and not self.uses_best_basic_parser(
                                segmenter, combination, enhanced_parser, ensemble_size
                            ):
                                continue
                            yield (segmenter, combination, enhanced_parser, ensemble_size)
                            if combination_size == 2 and (ensemble_size % 2) == 1:
                                yield (segmenter, reverse_combination, enhanced_parser, ensemble_size)

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        return True

    def get_segmenter_names(self):
        return ['udpipe_standard', 'udpipe_augmented', 'udpipe_polyglot', 'uusegmenter']

    def get_basic_parser_names(self):
        return ['elmo_udpf', 'udify', 'fasttext_udpf', 'plain_udpf',]

    def get_enhanced_parser_names(self):
        retval = set()
        for case_rule in ('', '_encase', '_arcase'):
            for mark_rule in ('', '_mark'):
                for cc_rule in ('', '_cc'):
                    for rel_rule in ('', '_rel'):
                        retval.add('copy_parse%s%s%s%s' %(
                            case_rule, mark_rule, cc_rule, rel_rule
                        ))
        if not self.options.all_heuristics:
            retval = set()
            retval.add('copy_parse_arcase_mark')
            retval.add('copy_parse_arcase_mark_rel')
            retval.add('copy_parse_encase_mark_rel')
            retval.add('copy_parse_encase_mark_cc_rel')
            retval.add('copy_parse_encase_rel')
        if not self.options.use_allennlp:
            return retval
        for allennlp_version in '090 dev'.split():
            for ptype in 'dm kg'.split():
                for bert in 'mbert lbert pbert'.split():
                    for feats in 'u luf lufb lux luxf luxfb'.split():
                        retval.add('allennlp_%s_%s_%s_%s' %(
                            allennlp_version,
                            ptype,
                            bert,
                            feats,
                        ))
        return retval

    def get_ensemble_sizes(self):
        #return (3,5,7)
        return (3,5,6,7,10,14)

    def get_basic_parser_ensemble_size(self):
        return self.variant[3]

    def get_segmenters(self):
        if self.options.debug:
            print('Segmenters for', self.tbid)
        return self.filter_by_lcode_and_add_datasets(
            self.get_segmenter_names()
        )

    def get_basic_parsers(self):
        if self.options.debug:
            print('Basic parsers for', self.tbid)
        return self.filter_by_lcode_and_add_datasets(
            self.get_basic_parser_names()
        )

    def get_enhanced_parsers(self):
        if self.options.debug:
            print('Enhanced parsers for', self.tbid)
        return self.filter_by_lcode_and_add_datasets(
            self.get_enhanced_parser_names(),
            length_limit = 999,
        )

    def filter_by_lcode_and_add_datasets(self, module_names, length_limit = 60):
        if self.options.debug:
            print('\tQuerying datasets for', self.tbid)
            sys.stdout.flush()
        datasets = list(self.get_dataset_names())
        datasets.sort()
        if self.options.debug:
            print('\t-->', datasets)
        retval = []
        external_model_ids_covered = {}
        for module_name in module_names:
            if self.options.debug:
                print('\t\tImporting', module_name)
                sys.stdout.flush()
            try:
                my_module = importlib.import_module(module_name)
            except ImportError:
                print('Warning: module %r not available, skipping' %module_name)
                continue
            if my_module.supports_lcode(self.lcode):
                if self.options.debug:
                    print('\t\tLanguage %s is supported' %self.lcode)
                if not datasets:
                    if my_module.has_default_model_for_lcode(self.lcode):
                        retval.append((0, module_name))
                else:
                    tbid_combinations_covered = {}
                    all_tbids = set()
                    # find number of tbids
                    for dataset in datasets:
                        _, tbid = dataset.split('.', 1)
                        all_tbids.add(tbid)
                    n_all_tbids = len(all_tbids)
                    # https://stackoverflow.com/questions/1482308/how-to-get-all-subsets-of-a-set-powerset
                    n_datasets = len(datasets)
                    combination_indices = []
                    for i in range(n_datasets):
                        combination_indices.append(1<<i)
                    combi_all = (1 << n_datasets) - 1
                    combination_indices.append(combi_all)
                    if n_datasets > 7:
                        print('8 or more datasets. Only considering mono-treebank models and the multi-treebank model using all datasets.')
                    elif n_datasets > 4:
                        print('Between 5 and 7 datasets. Only considering combinations of 1, n-1 and n datasets')
                        for i in range(n_datasets):
                            combination_indices.append(combi_all - (1<<i))
                    else:
                        # explore all 2**n - 1 non-empty combinations (1, 3, 7 or 15)
                        combination_indices = range(1, 1 << n_datasets)
                    for combination_index in combination_indices:
                        combination = [datasets[j]
                            for j in range(n_datasets)
                            if (combination_index & (1 << j))
                        ]
                        if self.options.debug:
                            print('\t\t\tChecking dataset combination', combination)
                        contains_ud25_data = False
                        contains_task_data = False
                        _, first_tbid = combination[0].split('.', 1)
                        first_lcode = first_tbid.split('_')[0]
                        contains_target_lcode = False
                        duplicate_tbid = False
                        data_lcodes = set()
                        data_tbids = set()
                        for dataset_name in combination:
                            if dataset_name.startswith('ud25.'):
                                contains_ud25_data = True
                            if dataset_name.startswith('task.'):
                                contains_task_data = True
                            data_source, tbid = dataset_name.split('.', 1)
                            lcode = tbid.split('_')[0]
                            if lcode == self.lcode:
                                contains_target_lcode = True
                            if tbid in data_tbids:
                                duplicate_tbid = True
                            data_lcodes.add(lcode)
                            data_tbids.add(tbid)
                        prio1 = len(data_tbids)
                        prio2 = n_all_tbids - prio1
                        priority = min(2*prio1-1, 2*prio2)
                        if duplicate_tbid:
                            if self.options.debug:
                                print('\t\t\t--> duplicate tbid, skipping')
                            continue
                        tbid_combi = tuple(sorted(data_tbids))
                        if tbid_combi in tbid_combinations_covered:
                            if self.options.debug:
                                print('\t\t\t-->not adding as already have', tbid_combinations_covered[tbid_combi])
                        if not contains_target_lcode:
                            # dataset combination has no data with the
                            # target lcode --> skip
                            if self.options.debug:
                                print('\t\t\t--> no matching lcode, skipping')
                            continue
                        if my_module.uses_external_models():
                            model_id = my_module.get_model_id(lcode, None, dataset, self.options)
                            if model_id is None:
                                if self.options.debug:
                                    print('\t\t\t--> not adding as no matching model available')
                            elif model_id in external_model_ids_covered:
                                if self.options.debug:
                                    print('\t\t\t--> not adding as already have %s with model ID %s' %(
                                        external_model_ids_covered[model_id], model_id,
                                    ))
                            else:
                                retval.append((priority, ':'.join((module_name, '+'.join(combination)))))
                                external_model_ids_covered[model_id] = retval[-1][1]
                                if self.options.debug:
                                    print('\t\t\t--> added data as module has external model matching it')
                                    print('\t\t\t--> model ID is', model_id)
                            continue
                        is_polyglot = len(data_lcodes) > 1
                        #if is_polyglot:
                        #    TODO: do we want to check that all languages are supported,
                        #          e.g. word embeddings are available, or do we assume
                        #          that modules that support polyglot training use internal
                        #          word embeddings only?
                        if len(combination) == 1 \
                        and contains_ud25_data   \
                        and my_module.has_ud25_model_for_tbid(combination[0][5:]):
                                retval.append((priority, ':'.join((module_name, combination[0]))))
                                tbid_combinations_covered[tbid_combi] = combination
                                if self.options.debug:
                                    print('\t\t\t--> added data as module has a ud25 model ready')
                        if len(combination) == 1 \
                        and contains_task_data   \
                        and my_module.has_task_model_for_tbid(combination[0][5:]):
                                retval.append((priority, ':'.join((module_name, combination[0]))))
                                tbid_combinations_covered[tbid_combi] = combination
                                if self.options.debug:
                                    print('\t\t\t--> added data as module has a task model ready')
                        elif my_module.can_train_on(contains_ud25_data, contains_task_data, is_polyglot):
                                retval.append((priority, ':'.join((module_name, '+'.join(combination)))))
                                tbid_combinations_covered[tbid_combi] = combination
                                if self.options.debug:
                                    print('\t\t\t--> added data as module can train on it')
                        elif self.options.debug:
                            print('\t\t\tNo suitable data found for')
                            print('\t\t\t * contains_ud25_data', contains_ud25_data)
                            print('\t\t\t * contains_task_data', contains_task_data)
                            print('\t\t\t * is_polyglot', is_polyglot)
            elif self.options.debug:
                print('\t\tLanguage %s is not supported' %self.lcode)
        retval.sort()
        if len(retval) > length_limit:
            print('\tPruning too long list of modules', retval)
            retval = retval[:length_limit]
        # remove priority
        retval = map(lambda x: x[1], retval)
        if self.options.debug:
            print('\tFinished checking modules for', self.tbid)
            print('\t-->', retval)
            sys.stdout.flush()
        return retval

    def get_dataset_names(self):
        ''' subclasses should extend this list to include
            appropriate ud25 data
        '''
        for tbid in self.options.task_treebanks:
            lcode = tbid.split('_')[0]
            tb_info = self.options.task_treebanks[tbid]
            training_data = tb_info[1]
            if training_data and self.lcode == lcode :
                yield 'task.' + tbid

    def init_segmenter(self):
        self.segmenter = self.variant[0]
        segmenter_module, datasets = self.segmenter.split(':', 1)
        segmenter = importlib.import_module(segmenter_module)
        self.segmenter_id = segmenter.get_model_id(
            self.lcode,
            self.options.init_seed,
            datasets,
            self.options,
        )

    def init_basic_parsers(self):
        self.basic_parsers = []
        parsers = self.variant[1]
        if not parsers:
            print('Warning: No parser found that supports', self.lcode)
        ensemble_size = self.get_basic_parser_ensemble_size()
        p_name2p_index = defaultdict(lambda: 0)
        for index in range(ensemble_size):
            round_robin_choice = parsers[index % len(parsers)]
            p_index = p_name2p_index[round_robin_choice]
            p_name2p_index[round_robin_choice] = p_index + 1
            self.basic_parsers.append((round_robin_choice, p_index))
        # set short name of combination
        if len(parsers) > ensemble_size:
            parsers = parsers[:ensemble_size]
        name = []
        name.append('e%d' %ensemble_size)
        for parser in parsers:
            # TODO: cover case that the basic parser uses an off-the-shelf model
            #       that ignores the dataset and/or init seed
            name.append(parser.replace(':', '_').replace('.', '_'))
        self.basic_parser_id = '_'.join(name)

    def init_enhanced_parser(self):
        self.enhanced_parser = self.variant[2]
        enhanced_module, datasets = self.enhanced_parser.split(':', 1)
        enhanced_parser = importlib.import_module(enhanced_module)
        self.enhanced_parser_id = enhanced_parser.get_model_id(
            self.lcode,
            self.options.init_seed,
            datasets,
            self.options,
        )

    def skip(self, skip_list):
        segmenter, basic_parsers, enhanced_parser, _ = self.variant
        if self.p_skip('segmenter', segmenter, skip_list):
            return True
        for basic_parser in basic_parsers:
            if self.p_skip('basic_parser', basic_parser, skip_list):
                return True
        if self.p_skip('enhanced_parser', enhanced_parser, skip_list):
            return True
        return False

    def p_skip(self, category, module_name, skip_list):
        if category in skip_list:
            return True
        combined_name = '.'.join((category, module_name))
        if combined_name in skip_list:
            return True
        return False

    def train_missing_models(self):
        tasks = self.train_missing_segmenter_model()
        tasks += self.train_missing_basic_parser_models()
        tasks += self.train_missing_enhanced_parser_model()
        return tasks

    def train_missing_segmenter_model(self):
        segmenter_module, datasets = self.segmenter.split(':', 1)
        segmenter = importlib.import_module(segmenter_module)
        if self.options.debug:
            print('Training %s with seed %s and data %s' %(
                segmenter_module, self.options.init_seed, datasets,
            ))
        tasks = []
        tasks.append(segmenter.train_model_if_missing(
            self.lcode,
            self.options.init_seed,
            datasets,
            self.options,
        ))
        return tasks

    def train_missing_basic_parser_models(self):
        tasks = []
        for p_name, p_index in self.basic_parsers:
            p_module_name, datasets = p_name.split(':', 1)
            basic_parser = importlib.import_module(p_module_name)
            init_seed = '%s%02d' %(self.options.init_seed, p_index)
            if self.options.debug:
                print('Training %s with seed %s and data %s' %(
                    p_module_name, init_seed, datasets,
                ))
            tasks.append(basic_parser.train_model_if_missing(
                self.lcode,
                init_seed,
                datasets,
                self.options,
            ))
        return tasks

    def train_missing_enhanced_parser_model(self):
        enhanced_module, datasets = self.enhanced_parser.split(':', 1)
        enhanced_parser = importlib.import_module(enhanced_module)
        if self.options.debug:
            print('Training %s with seed %s and data %s' %(
                enhanced_module, self.options.init_seed, datasets,
            ))
        tasks = []
        tasks.append(enhanced_parser.train_model_if_missing(
            self.lcode,
            self.options.init_seed,
            datasets,
            self.options,
        ))
        return tasks

    def segment(self, raw_text_input, conllu_output):
        segmenter_module, datasets = self.segmenter.split(':', 1)
        segmenter = importlib.import_module(segmenter_module)
        if self.options.debug:
            print('Predicting segmentation for %s using %s trained on %s' %(
                raw_text_input, segmenter_module, datasets,
            ))
        segmenter.predict(
            self.lcode, self.options.init_seed, datasets, self.options,
            raw_text_input, conllu_output,
        )

    def enhance(self, conllu_input, conllu_output):
        enhancer_module, datasets = self.enhanced_parser.split(':', 1)
        enhancer = importlib.import_module(enhancer_module)
        if self.options.debug:
            print('Enhancing parse for %s using %s trained on %s' %(
                conllu_input, enhancer_module, datasets,
            ))
        enhancer.predict(
            self.lcode, self.options.init_seed, datasets, self.options,
            conllu_input, conllu_output,
        )

    def parse(self, conllu_input, conllu_output):
        votesdir = '%s/basic-parses/votes' %self.options.tempdir
        utilities.makedirs(votesdir)
        ensemble_predictions = []
        _, vote_suffix = conllu_input.rsplit('/', 1)
        n_parses = len(self.basic_parsers)
        pad2freq = defaultdict(lambda: 0)
        for parser_and_dataset, _ in self.basic_parsers:
            pad2freq[parser_and_dataset] += 1
        for parser_and_dataset in sorted(list(pad2freq.keys())):
            parser_module, datasets = parser_and_dataset.split(':', 1)
            parser = importlib.import_module(parser_module)
            number_parses_needed = pad2freq[parser_and_dataset]
            patience_at_start = 5
            patience_left = patience_at_start
            p_index = 0
            while number_parses_needed > 0 and patience_left > 0:
                init_seed = '%s%02d' %(self.options.init_seed, p_index)
                conllu_individual_output = '%s/%s-%s-%s-%s' %(
                    votesdir,
                    parser_module,
                    datasets.replace(':', '_').replace('-', '_'),
                    init_seed,
                    vote_suffix,
                )
                if os.path.exists(conllu_individual_output):
                    action = 'Reusing'
                    is_successful = True
                else:
                    action = 'Predicting'
                if self.options.debug:
                    print('%s basic parse for %s using %s trained on %s with seed %s' %(
                        action,
                        conllu_input, parser_module, datasets,
                        init_seed,
                    ))
                if action == 'Predicting':
                    if '+' in datasets:
                        # multi-treebank mode
                        proxy_tbid = self.tbid
                    else:
                        proxy_tbid = None
                    is_successful = parser.predict(
                        self.lcode, init_seed, datasets, self.options,
                        conllu_input, conllu_individual_output,
                        proxy_tbid = proxy_tbid,
                    )
                if is_successful:
                    ensemble_predictions.append(conllu_individual_output)
                    number_parses_needed -= 1
                    patience_left= patience_at_start
                else:
                    patience_left -= 1
                    if self.options.debug:
                        print('Parse failed.')
                p_index += 1
        if self.options.debug:
            print('%d of %d basic parses are ready' %(len(ensemble_predictions), n_parses))
        if len(ensemble_predictions) < n_parses:
            print('Warning: missing basic parse(s) for ensemble --> skipping')
        elif len(ensemble_predictions) == 1:
            # just 1 parse --> no combination
            link_target = os.path.abspath(ensemble_predictions[0])
            os.symlink(link_target, conllu_output)
        else:
            # run combiner
            conllu_dataset.combine(ensemble_predictions, conllu_output, self.options)

class Config_with_more_datasets(Config_default):

    def get_dataset_names(self):
        tbids_covered = set()
        for dataset in Config_default.get_dataset_names(self):
            if dataset.startswith('task.'):
                tbid = dataset.split('.')[1]
                tbids_covered.add(tbid)
                yield dataset
        for tbid, allow_duplicate_tbid in self.get_additional_dataset_tbids():
            if allow_duplicate_tbid or tbid not in tbids_covered:
                yield 'ud25.' + tbid
                tbids_covered.add(tbid)

    def get_additional_dataset_tbids(self):
        return []

class Config_ar(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0]:
            return True
        return False

class Config_bg(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0]:
            return True
        return False

class Config_cs(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if not self.tbid in basic_parsers[0]:
            return False
        if ensemble_size in (3,7) \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'elmo' in basic_parsers[1] \
        and basic_parsers[0].count('+') == 0 \
        and basic_parsers[1].count('+') == 2 \
        and self.tbid in basic_parsers[1]:
            return True
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0] \
        and basic_parsers[0].count('+') == 2:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('cs_cac', False),
            ('cs_pdt', False),
            ('cs_cltt', False),
        ]

class Config_en(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'elmo' in basic_parsers[1] \
        and 'en_ewt' in basic_parsers[0] \
        and 'en_ewt' in basic_parsers[1] \
        and basic_parsers[0].count('+') == 3 \
        and basic_parsers[1].count('+') == 0 \
        and self.tbid in basic_parsers[1]:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('en_ewt', False),
            ('en_gum', False),
            ('en_lines', False),
            ('en_partut', False),
        ]

class Config_et(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'elmo' in basic_parsers[1] \
        and basic_parsers[0].count('+') == 1 \
        and basic_parsers[1].count('+') == 0:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('et_edt', False),
            ('et_ewt', False),
        ]

class Config_fi(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0] \
        and basic_parsers[0].count('+') == 0:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('fi_ftb', False),
            ('fi_tdt', False),
        ]

class Config_fr(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0] \
        and self.tbid in basic_parsers[0] \
        and basic_parsers[0].count('+') == 3:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('fr_gsd', False),
            ('fr_partut', False),
            ('fr_sequoia', False),
            ('fr_spoken', False),
        ]

class Config_it(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'elmo' in basic_parsers[1] \
        and self.tbid in basic_parsers[1] \
        and 'it_isdt' in basic_parsers[1] \
        and basic_parsers[0].count('+') == 4 \
        and basic_parsers[1].count('+') == 0:
            return True
        if ensemble_size == 7 \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'fasttext' in basic_parsers[1] \
        and self.tbid in basic_parsers[0] \
        and self.tbid in basic_parsers[1] \
        and basic_parsers[0].count('+') == 4 \
        and basic_parsers[1].count('+') == 4:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('it_isdt', False),
            ('it_partut', False),
            ('it_postwita', False),
            ('it_twittiro', False),
            ('it_vit', False),
        ]

class Config_lt(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 5 \
        and len(basic_parsers) == 1 \
        and basic_parsers[0].count('+') == 0:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('lt_alksnis', False),
            ('lt_hse', False),
        ]

class Config_lv(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 5 \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'fasttext' in basic_parsers[1]:
            return True
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0]:
            return True
        return False

class Config_nl(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0] \
        and basic_parsers[0].count('+') == 1:
            return True
        return False

class Config_pl(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 2 \
        and 'plain' in basic_parsers[0] \
        and 'elmo' in basic_parsers[1] \
        and basic_parsers[0].count('+') == 1 \
        and basic_parsers[1].count('+') == 0 \
        and self.tbid in basic_parsers[1]:
            return True
        if ensemble_size == 7 \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'elmo' in basic_parsers[1] \
        and basic_parsers[0].count('+') == 1 \
        and basic_parsers[1].count('+') == 0 \
        and self.tbid in basic_parsers[1]:
            return True
        return False

class Config_ru(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 2 \
        and 'elmo' in basic_parsers[0] \
        and 'elmo' in basic_parsers[1] \
        and basic_parsers[0].count('+') == 2 \
        and basic_parsers[1].count('+') == 0 \
        and self.tbid in basic_parsers[0] \
        and self.tbid in basic_parsers[1]:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('ru_syntagrus', False),
            ('ru_gsd', False),
            ('ru_taiga', False),
        ]

class Config_sk(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0] \
        and basic_parsers[0].count('+') == 0:
            return True
        return False

class Config_sv(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size  == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0] \
        and basic_parsers[0].count('+') == 0:
            return True
        return False

    def get_additional_dataset_tbids(self):
        return [
            ('sv_lines', False),
            ('sv_talbanken', False),
        ]

class Config_ta(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 5 \
        and len(basic_parsers) == 1 \
        and 'plain' in basic_parsers[0] \
        and basic_parsers[0].count('+') == 0:
            return True
        return False

class Config_uk(Config_with_more_datasets):

    def uses_best_basic_parser(self, segmenter, basic_parsers, enhanced_parser, ensemble_size):
        if ensemble_size == 7 \
        and len(basic_parsers) == 1 \
        and 'elmo' in basic_parsers[0] \
        and basic_parsers[0].count('+') == 0:
            return True
        return False

def main():
    options = Options()
    options.get_tasks_and_configs()
    options.scan_ud25()
    options.train_missing_models()  # respects --skip-training
    if options.training_only:
        return
    options.predict_and_evaluate()

if __name__ == "__main__":
    main()


import pygame, color, time, sys
from pygame.constants import QUIT

pygame.init()

y2 = 0
sp = 0
epY = 0
epX = 0
frame = 0
fps = 0
starttime = round(time.time())
hold = False
cut = False
win = False

#scherm grootte, titel en icon
screen = pygame.display.set_mode((1280,720))
pygame.mouse.set_visible(0)
pygame.display.set_caption("cutting game")
icon = pygame.image.load("icon.png").convert_alpha()
pygame.display.set_icon(icon)

#doos
dozen = [pygame.image.load("doos1.png").convert_alpha(),
         pygame.image.load("doos2.png").convert_alpha(),
         pygame.image.load("doos3.png").convert_alpha(),
         pygame.image.load("doos4.png").convert_alpha(),
         pygame.image.load("doos5.png").convert_alpha()]
doosnum = 1

#cutting part
CPO = pygame.image.load("cutting part overlay.png").convert_alpha()

#cursor
cursor = pygame.image.load("stanley-mes.png").convert_alpha()
cursor = pygame.transform.scale(cursor, (200, 200))

#text
text = "cutting game"
titlefont = pygame.font.SysFont('Arial', 100, True)
fpsfont = pygame.font.SysFont('Arial', 30)

#win icon
winicon = pygame.image.load("win icon.png").convert_alpha()

running = True
while running == True:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
            pygame.quit()
            sys.exit()
    screen.fill((color.blue))
    x, y = pygame.mouse.get_pos()
    x -= cursor.get_width()/2
    y -= cursor.get_height()/2
    if hold == False:
        y2 = y
    for event in pygame.event.get():
        if event.type == pygame.MOUSEBUTTONDOWN:
            hold = True
            spX, spY = pygame.mouse.get_pos()
        elif event.type == pygame.MOUSEBUTTONUP:
            hold = False
            cut = True
    if hold == True:
        screen.fill((color.red))
    if win == False:
        screen.blit(dozen[doosnum-1], (390, 550))
    if hold == True:
        epX, epY = pygame.mouse.get_pos()
        pygame.draw.line(screen, (0, 0, 0), (spX, spY), (epX, spY))
    if cut == True and win == False:
        if doosnum == 1:
            if epX < 405 and spX > 870 or epX > 870 and spX < 405:
                if spY > 570 and spY < 595:
                    text = "good cut"
                    cut = False
                    doosnum = 2
                else:
                    text = "bad cut"
                    cut = False
            else:
                text = "bad cut"
                cut = False
        elif doosnum == 2:
            if epX < 407 and spX > 872 or epX > 872 and spX < 407:
                if spY > 570 and spY < 590:
                    text = "good cut"
                    cut = False
                    doosnum = 3
                else:
                    text = "bad cut"
                    cut = False
                    doosnum = 1
            else:
                text = "bad cut"
                cut = False
                doosnum = 1
        elif doosnum == 3:
            if epX < 408 and spX > 868 or epX > 868 and spX < 408:
                if spY > 290 and spY < 590:
                    text = "good cut"
                    cut = False
                    doosnum = 4
                else:
                    text = "bad cut"
                    cut = False
                    doosnum = 1
            else:
                text = "bad cut"
                cut = False
                doosnum = 1
        elif doosnum == 4:
            if epX < 410 and spX > 869 or epX > 869 and spX < 410:
                if spY > 580 and spY < 585:
                    text = "good cut"
                    cut = False
                    doosnum = 5
                else:
                    text = "bad cut"
                    cut = False
                    doosnum = 1
            else:
                text = "bad cut"
                cut = False
                doosnum = 1
        elif doosnum == 5:
            if epX < 412 and spX > 868 or epX > 868 and spX < 412:
                if spY > 579 and spY < 583:
                    text = "game won"
                    cut = False
                    win = True
                else:
                    text = "bad cut"
                    cut = False
                    doosnum = 1
            else:
                text = "bad cut"
                cut = False
                doosnum = 1
    if hold == True:
        screen.blit(CPO, (0, 0))
    screen.blit(cursor, (x, y2))
    if win == True:
        screen.blit(winicon, (385, 150))
    text_r = titlefont.render(text, True, (color.white))
    text_pos = text_r.get_rect(center=(640, 60))
    screen.blit(text_r, text_pos)
    frame += 1
    if round(time.time()) - starttime == 1:
        fps = frame
        frame = 0
        starttime = round(time.time())
    screen.blit(fpsfont.render(str(fps), True, (color.white)), (1200, 650))
    pygame.time.Clock().tick(100)
    pygame.display.update()


from typing import Optional

from fastapi import APIRouter
import requests

router = APIRouter()

@router.post("/")
async def get_all(currency_name: Optional[str] = "BRL", amount: Optional[int] = 1):
    url = f'https://api.exchangerate.host/latest?base={currency_name}&amount={amount}'
    response = requests.get(url)
    data = response.json()
    data.pop("motd")
    return data


import requests
from datetime import timezone, datetime
import winsound
import time
import threading
import pyperclip
import socket
import json
# import RPi.GPIO as GPIO
import os

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
from selenium.webdriver import ActionChains

options = Options()
options.add_argument("user-data-dir=C:\environments\selenium")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)

driver = webdriver.Chrome('C:\\Python39\\chromedriver.exe', options=options)
driver.get('https://web.whatsapp.com')
driver.maximize_window()
# time.sleep(5)

ans = input("Ready? ")

List_18 = []
List_18_2 = []
List_45 = []
maxListLength = 6

groupsTest = ["Notification Number"]
groupsAHM = ["Ahmedabad Vaccination", "1st Dose | AMD - Group 1", "1st Dose | AMD - Group 2",
             "1st Dose | AMD - Group 3"]

groupsAHM2 = ["2nd Dose | AMD - Group 1", "2nd Dose | AMD - Group 2", "2nd Dose | AMD - Group 3"]

# groupsAHM = ["1st Dose"]
# groupsAHM2 = ["2nd dose"]

groups45 = ["Shrey"]  # "Ahmedabad 45"]
groupsGHN = ["Gandhinagar Vaccination 1", "Gandhinagar Vaccination 2"]
# groupsSUR = ["Surat Vaccination 1"]
groupsSUR = ['Surat Vaccination 1']

groupDistricts = {0: groupsTest, 770: groupsAHM, 772: groupsGHN, 153: groupsGHN, 776: groupsSUR, 45: groups45,
                  7702: groupsAHM2}

VSCode = False

if not VSCode:
    from pynput.keyboard import Key, Controller

    keyboard = Controller()

    from pynput.mouse import Button, Controller

    mouse = Controller()

    CONNECT = False
    TYPE = True
else:
    CONNECT = False
    TYPE = False

time.sleep(2)

Already_On = False
DIML = False

RegistrationURL = "https://selfregistration.cowin.gov.in/"

longDelay = {}
foundSpots = {}
foundSpots2 = {}
to_remove = {}

OldData = {}
NewData = {}

BRIGHT = DIM = 17
def Light_On():

    print("On")
    # GPIO.output(ON, False)
    time.sleep(delay)
    # GPIO.output(ON, True)

def Light_Off():
    print("Off")
    # GPIO.output(OFF, False)
    time.sleep(delay)
    # GPIO.output(OFF, True)

def pressSec(pinNum):
    print("Turning", pinNum, "On")
    # GPIO.output(pinNum, False)
    time.sleep(1)
    # GPIO.output(pinNum, True)
    print("Turning", pinNum, "Off")

def singePress(pinNum):
    print("Turning", pinNum, "On")
    # GPIO.output(pinNum, False)
    time.sleep(delay)
    # GPIO.output(pinNum, True)
    print("Turning", pinNum, "Off")

def channelTest(pinNum):
    print("Turning", pinNum, "On")
    # GPIO.output(pinNum, False)
    time.sleep(delay)
    # GPIO.output(pinNum, True)
    print("Turning", pinNum, "Off")

def PressDown(pinNum):
    print("Turning", pinNum, "On")
    # GPIO.output(pinNum, False)

def PressUp(pinNum):
    if pinNum != None:
        print("Turning", pinNum, "Off")
        # GPIO.output(pinNum, True)

# singePress(ALL)

def WhatsApp_Send(ID, messageW):
    global groupDistricts, driver
    # ID = 0
    groups = groupDistricts[ID]

    for name in groups:
        print(name)
        pyperclip.copy(messageW)

        try:
            chat = driver.find_element_by_xpath("//*[@title='" + name + "']")
            actions = ActionChains(driver)
            actions.move_to_element(chat).click()
            chat.click()
            time.sleep(0.5)

            textBox = driver.find_element_by_xpath(
                '/html/body/div/div[1]/div[1]/div[4]/div[1]/footer/div[1]/div[2]/div/div[1]/div')
            time.sleep(0.25)
            # textBox.send_keys(messageW)
            pyperclip.copy(messageW)

            Pasted = False

            while not Pasted:
                textBox.send_keys(Keys.CONTROL, "v")
                time.sleep(0.3)

                try:
                    sendMessage = driver.find_element_by_xpath(
                    ('/html/body/div/div[1]/div[1]/div[4]/div[1]/footer/div[1]/div[2]/div/div[2]/button'))
                    Pasted = True

                except Exception as e:
                    print("Trying to paste again...")
                    time.sleep(0.15)

            while True:
                try:
                    sendMessage.click()

                except Exception as e:
                    break

            print("Sent")

            '''
            # # sendMessage = driver.find_element_by_xpath('/html/body/div/div[1]/div[1]/div[4]/div[1]/footer/div[1]/div[3]/button')
            # # sendMessage.click()
            # time.sleep(0.2)

            # try:
            #     sendMessage = driver.find_element_by_xpath(
            #         ('/html/body/div/div[1]/div[1]/div[4]/div[1]/footer/div[1]/div[2]/div/div[2]/button'))
            #     sendMessage.click()
            # except Exception as e:
            #     time.sleep(0.5)
            #     print("Trying Again ((((((((((((((((((((((((((((((((((((((((())))))))))))))))))))))))))))))))))))))))))))")
            #     textBox.send_keys(Keys.CONTROL, "v")
            #     time.sleep(0.3)

            #     try:
            #         sendMessage = driver.find_element_by_xpath(
            #             ('/html/body/div/div[1]/div[1]/div[4]/div[1]/footer/div[1]/div[2]/div/div[2]/button'))
            #         sendMessage.click()

            #     except Exception as e:
            #         print(str(e), "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
            #         textBox.send_keys(Keys.ENTER)
            # # time.sleep(0.15)
            '''
            time.sleep(0.4)
        except Exception as e:
            print("Error while sending info to", name)
            print(messageW)

            print()
            print(str(e))
            t1 = threading.Thread(target=ringRepeat, args=[5])
            t1.start()
            # ringRepeat(5)
            return

def sendMessages(List_Num, age):
    message = ""
    ID_Group = int(age)
    count = 1
    for (Vaccine_type, name, date, pin_code, capacity1, capacity2, URL, ID, age, cost) in List_Num:
        messageSingle = makeSingleMessage(Vaccine_type, name, date, pin_code, capacity1, capacity2, URL, ID, age, cost,
                                          count)
        message += messageSingle
        message += "\n \n"
        count += 1

        if not (ID_Group == 770 or 7702):
            print("Error in ID", ID_Group)

            ID = 770

    pyperclip.copy(message)
    WhatsApp_Send(ID_Group, message)

def makeSingleMessage(Vaccine_type, name, date, pin_code, capacity1, capacity2, URL, ID, age, cost, count):
    global groupDistricts

    if capacity2 > 4:
        message1 = "Open slots of *" + Vaccine_type + "* found at " + name + " on " + str(
            date)
    else:
        message1 = "Open slots of *" + Vaccine_type + "* found at " + name + " on " + str(
            date)

    message1 = str(count) + ") " + message1

    pinInfo = "Pin Code: *" + str(pin_code) + "*   |  Cost: " + str(cost)

    if capacity2 > 4:

        doseInfo = "Dose 1: " + str(capacity1) + "  |  _*Dose 2: " + str(capacity2) + "*_"
    else:
        doseInfo = "Dose 1: " + str(capacity1) + "  |  Dose 2: " + str(capacity2)

    # count = str(count) + ")"
    messagesList = [message1, doseInfo, pinInfo, URL]
    singleMessage = ''

    for m in messagesList:
        singleMessage += str(m)
        singleMessage += "\n"

    return singleMessage

def makeMessage(Vaccine_type, name, date, pin_code, capacity1, capacity2, URL, ID, age, cost):
    global groupDistricts

    delay1 = 1.2
    delay2 = 1.2
    delay3 = 1.5

    if age == 18:
        message = str(capacity1) + " doses of " + Vaccine_type + " _For 18+_ " + "found at " + name + " on " + str(date)
    else:
        message = str(capacity1) + " doses of " + Vaccine_type + " _For 45+_ " + "found at " + name + " on " + str(date)

    pinInfo = "Pin Code: *" + str(pin_code) + "*   |  Cost: " + str(cost)

    messages = [message, pinInfo, URL]

    # message = Vaccine_type + " Found at " + name + " on " + str(date)
    # doseInfo = "Dose 1: " + str(capacity1) + " Dose 2: " + str(capacity2)
    # pinInfo = "Pin Code: " + str(pin_code)

    # messages = [message, doseInfo, pinInfo, URL]
    print(message, ID, pin_code)

    try:
        time.strftime("%I: %M: %S")

    except Exception as e:
        print("Error printing Time")
        print(str(e))

    groups = groupDistricts[ID]

    for name in groups:
        try:
            chat = driver.find_element_by_xpath("//*[@title='" + name + "']")
            actions = ActionChains(driver)
            actions.move_to_element(chat).click()
            chat.click()
            time.sleep(0.1)

            textBox = driver.find_element_by_xpath(
                '/html/body/div/div[1]/div[1]/div[4]/div[1]/footer/div[1]/div[2]/div/div[1]/div')
            for message in messages:
                textBox.send_keys(message + Keys.CONTROL + Keys.ENTER)

            # sendMessage = driver.find_element_by_xpath('/html/body/div/div[1]/div[1]/div[4]/div[1]/footer/div[1]/div[3]/button')
            # sendMessage.click()
            textBox.send_keys(Keys.ENTER)
            # time.sleep(0.15)
        except Exception as e:
            print("Error while sending info to", name)
            for p in messages:
                print(p)

            print()
            print(str(e))
            t1 = threading.Thread(target=ringRepeat, args=[5])
            t1.start()
            # ringRepeat(5)
        time.sleep(0.15)

def ErrorLights(dely=0.9):
    for i in range(5):
        Light_Off()
        time.sleep(delay)
        if DIML:
            singePress(DIM)
        else:
            Light_On()
        time.sleep(delay)

def ringRepeat(num=5):
    try:
        global Already_On

        for i in range(num):
            winsound.PlaySound("point.wav", winsound.SND_ASYNC)
            time.sleep(0.6)
            winsound.PlaySound("hit.wav", winsound.SND_ASYNC)

        # time.sleep(200)
        Already_On = False
    except Exception as e:
        print(str(e))
        print("RING REPEAT ERROR")

def on_and_off(delay=1.5):
    singePress(BRIGHT)
    try:
        if CONNECT:
            global Already_On

            singePress(BRIGHT)

            for i in range(2):
                Light_Off()
                time.sleep(delay)
                if DIML:
                    singePress(DIM)
                else:
                    Light_On()
                time.sleep(delay)

            time.sleep(0.5)
            Already_On = False

    except Exception as e:
        print(str(e))

def GetSlots(Days1, ID):
    global Already_On, foundSpots, foundSpots2, OldData, NewData, List_45, List_18, List_18_2, longDelay
    Found = False
    counts = 0
    URL = 'https://cdn-api.co-vin.in/api/v2/appointment/sessions/public/findByDistrict?district_id=' + str(
        ID) + '&date=' + Days1
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36'}

    try:
        r = requests.get(URL, headers=headers)
    except ConnectionError:
        print("Connection Error")
        time.sleep(5)
        return Found, counts

    if str(r) == "<Response [429]>":
        t1 = threading.Thread(target=ringRepeat, args=[3])
        t1.start()
        print("ERROORRRRR", ID)
        print(str(r))
        try:

            print(r.headers)
            print(type(r.headers))
        except:
            pass
        time.sleep(5)
        return Found, counts

    if str(r) != "<Response [200]>":
        print("ERROORRRRR", ID)
        print(str(r))
        time.sleep(5)
        return Found, counts

    data = r.json()
    if "calendar" in URL:
        centers = data["centers"]
    elif "find" in URL:
        centers = data["sessions"]
    else:
        print("Could not find proper data")
        print(data)
        return

    NewData = data

    if NewData == OldData:
        return 'Samee', 'Found'

    else:
        OldData = NewData

    for center in centers:

        name = center["name"]
        pin_code = center["pincode"]
        date = center["date"]
        min_age_limit = center["min_age_limit"]
        capacity1 = center["available_capacity_dose1"]
        capacity2 = center["available_capacity_dose2"]
        capacity = capacity2 + capacity1
        Vaccine_type = center["vaccine"]
        cost = center["fee"]
        if str(cost) == "0":
            cost = "Free"

        if capacity1 > 4 and cost == "Free":
            # print(
            #     "Name: " + name + "     capacity1: " + str(capacity1) + "     Date: " + str(
            #         date) + "     Age Limit: " + str(min_age_limit) + "     Pincode: " + str(pin_code))
            # print("Time: " + str(time.strftime("%I: %M: %S")))

            if min_age_limit < 45:

                if (name + Vaccine_type + str(date)) not in foundSpots:  # and (name not in longDelay):
                    Found = True

                    List_18.append((Vaccine_type, name, date, pin_code,
                                    capacity1, capacity2, RegistrationURL, ID, 18, cost))

                    foundSpots[(name + Vaccine_type + str(date))] = capacity1
                    print()
                    print("Added " + (name + Vaccine_type + str(date)) + " " + str(capacity1))
                    print()

                    if len(List_18) == maxListLength:
                        print("Sending 18+ Vacancies1:", len(List_18))
                        sendMessages(List_18, ID)
                        List_18 = []
                    # makeMessage(Vaccine_type, name, date, pin_code,
                    #             capacity1, capacity2, RegistrationURL, ID, 18, cost)

                    if not Already_On:
                        Already_On = True

                elif foundSpots[(name + Vaccine_type + str(date))] + 10 < capacity1:

                    Found = True

                    List_18.append((Vaccine_type, name, date, pin_code,
                                    capacity1, capacity2, RegistrationURL, ID, 18, cost))

                    foundSpots[(name + Vaccine_type + str(date))] = capacity1
                    print()
                    print("Added " + name + Vaccine_type + str(date) + " " + str(capacity1))
                    print()

                    if len(List_18) == maxListLength:
                        print("Sending 18+ Vacancies2:", len(List_18))
                        sendMessages(List_18, ID)
                        List_18 = []
                    # makeMessage(Vaccine_type, name, date, pin_code,
                    #             capacity1, capacity2, RegistrationURL, ID, 18, cost)

                    if not Already_On:
                        Already_On = True

                elif foundSpots[name + Vaccine_type + str(date)] > capacity1:
                    print("Decreased capacity of", name + " " + Vaccine_type + "1", "from",
                          foundSpots[name + Vaccine_type + str(date)], "to", capacity1)
                    foundSpots[name + Vaccine_type + str(date)] = capacity1

        if capacity2 > 4:

            # print(
            #     "Name: " + name + "     Capacity: " + str(capacity) + "     Date: " + str(
            #         date) + "     Age Limit: " + str(min_age_limit) + "     Pincode: " + str(pin_code))
            # print("Time: " + str(time.strftime("%I: %M: %S")))

            if min_age_limit < 45:

                if name + Vaccine_type + str(date) not in foundSpots2:  # and (name not in longDelay):
                    Found = True

                    ID_2 = int(str(ID) + "2")
                    List_18_2.append((Vaccine_type, name, date, pin_code,
                                      capacity1, capacity2, RegistrationURL, ID, 18, cost))

                    foundSpots2[name + Vaccine_type + str(date)] = capacity2
                    print()
                    print("Added " + name + Vaccine_type + str(
                        date) + "SECOND DOSE_______________1________________" + " " + str(
                        capacity2))
                    print()

                    if len(List_18_2) == maxListLength:
                        print("Sending 18+ Vacancies:", len(List_18_2), "for 2nd Dose1")
                        sendMessages(List_18_2, 7702)
                        List_18_2 = []

                    if not Already_On:
                        Already_On = True

                elif foundSpots2[name + Vaccine_type + str(date)] + 10 < capacity2:
                    print()
                    print(name[0:10], capacity1, capacity2, foundSpots2[name + Vaccine_type + str(date)])
                    print()
                    print("New DATA:", capacity2, "Old DATA:", foundSpots2[name + Vaccine_type + str(date)])

                    Found = True

                    ID_2 = int(str(ID) + "2")
                    List_18.append((Vaccine_type, name, date, pin_code,
                                    capacity1, capacity2, RegistrationURL, ID, 18, cost))

                    foundSpots2[name + Vaccine_type + str(date)] = capacity2
                    print()
                    print("Added " + name + Vaccine_type + str(
                        date) + " SECOND DOSE_____________2_________________" + " " + str(capacity1))
                    print()

                    if len(List_18_2) == maxListLength:
                        print("Sending 18+ Vacancies:", len(List_18_2), "for 2nd Dose2")
                        sendMessages(List_18_2, 7702)
                        List_18_2 = []

                    if not Already_On:
                        Already_On = True

                elif foundSpots2[name + Vaccine_type + str(date)] > capacity2:
                    print("1Decreased capacity of", name + " " + Vaccine_type + "2", "from",
                          foundSpots2[name + Vaccine_type + str(date)], "to", capacity2)
                    foundSpots2[name + Vaccine_type + str(date)] = capacity2

            # elif min_age_limit == 45:
            #
            #
            #     if (name not in foundSpots) and (name not in longDelay):
            #         Found = True
            #
            #         List_45.append((Vaccine_type, name, date, pin_code,
            #                         capacity1, capacity2, RegistrationURL, 45, 45, cost))
            #
            #         if cost != "Free" and capacity1 > 250:
            #             longDelay[name] = time.time()
            #             print("Added " + name, "for 45+   [Long Delay]")
            #             print()
            #         else:
            #             foundSpots[name] = time.time()
            #             print("Added " + name, "for 45+")
            #             print()
            #
            #         if len(List_45) == maxListLength:
            #             print("Sending 45+ Vacancies:", len(List_45))
            #             sendMessages(List_45, 45)
            #             List_45 = []
            #
            #         # makeMessage(Vaccine_type, name, date, pin_code,
            #         #             capacity1, capacity2, RegistrationURL, 45, 45, cost)

            # print()
            # print()

        counts += 1

    if len(List_18) != 0:
        print("Sending 18+ Vacancies3:", len(List_18))
        sendMessages(List_18, 770)
        List_18 = []

    if len(List_18_2) != 0:
        print("Sending 18+ Vacancies:", len(List_18_2), "for 2nd Dose3")
        sendMessages(List_18_2, 7702)
        List_18_2 = []

    return Found, counts

# os.environ['TZ'] = 'Asia/Kolkata'
# time.tzset()

AHM = 770
GHN = 772
GHN2 = 153
SUR = 776

loops = 0
delay = 3.4

time.sleep(1)
print("Testing...")

v = "COVISHIELD"
n = "K D HOSPITAL"
d = "Today1"
p = 380000
c1 = 100
c2 = 0
ID = 0
makeMessage(v, n, d, p, c1, c2, RegistrationURL, ID, 18, "1500")

Days1 = "1-06-2021"
Days2 = "3-06-2021"
while True:
    loops += 1

    GotSomething = False

    try:

        today = datetime.now()
        today.replace(tzinfo=timezone.utc)

        # print(today.strftime("%H: %M: %S"))
        HR = int(today.strftime("%H"))
        if HR > 13:
            change = 1
        else:
            change = 0

        Days2 = (str(int(today.strftime("%d")) + change) +
                 (today.strftime("-%m-%y")))

        Days1 = (str(int(today.strftime("%d")) + change + 1) +
                 (today.strftime("-%m-%y")))

        if "31" in Days2:
            Days2 = "01-07-2021"
        if "31" in Days1:
            Days1 = "01-07-2021"

        found, count = GetSlots(Days2, AHM)
        time.sleep(delay)

        # found1, count1 = GetSlots(Days1, AHM)
        # time.sleep(delay)
        #
        # found, count = GetSlots(Days2, AHM)
        # time.sleep(delay)

        # found1, count1 = GetSlots(Days2, GHN)
        # time.sleep(delay)
        # found2, count2 = GetSlots(Days2, GHN2)
        # time.sleep(delay)
        # found3, count3 = GetSlots(Days2, SUR)
        # time.sleep(delay)

        try:
            count = count  # + count1 #+ count3 #count1 + count2 + count3
        except Exception as e:
            print("Sum of count error")
            print(str(e))

        if loops == 10:
            print(Days2, Days1, time.strftime("%I: %M: %S"))
            loops = 0
        GotSomething = True

    except Exception as e:

        print("ERROR OCCURRED")
        print(str(e))
        # ringRepeat(5)
        time.sleep(5)

    if GotSomething:
        p = 0
        # print(found, count, loops)
        '''
        if len(foundSpots) > 0:
            for name in list(foundSpots):

                if time.time() - foundSpots[name] > 1800:
                    print("Removed " + str(name))
                    del foundSpots[name]

        if len(longDelay) > 0:
            for name in list(longDelay):

                if time.time() - longDelay[name] > 7200:
                    print("Removed " + str(name) + " from Long Delay")
                    del longDelay[name]
        '''

        if not count:
            print("Zero Counts", time.strftime("%I: %M: %S"))
            p = 1
            pass

        if (type(found)) == str:
            p = 1
            # print("STRING")
            if "Samee" in found:
                print("Same Data received", str(time.strftime("%I: %M: %S")))

        if not found:
            p = 1
            print("No New Open Slots in " + str(count) +
                  " Spaces. Time: " + str(time.strftime("%I: %M: %S")))

        if found == "False":
            p = 1
            print("No Open Slots in " + str(count) +
                  " Spaces. Time: " + str(time.strftime("%I: %M: %S")))

        # print(time.strftime("%I: %M: %S"))