import os
import glob
import click
import pickle
import shutil
import ftplib
import sentry_sdk
from automator_settings import SENTRY_DSN
from amrsummary import before_send
from externalretrieve import upload_to_ftp
from automator_settings import FTP_USERNAME, FTP_PASSWORD
from nastools.nastools import retrieve_nas_files

@click.command()
@click.option('--redmine_instance', help='Path to pickled Redmine API instance')
@click.option('--issue', help='Path to pickled Redmine issue')
@click.option('--work_dir', help='Path to Redmine issue work directory')
@click.option('--description', help='Path to pickled Redmine description')
def drep_redmine(redmine_instance, issue, work_dir, description):
    sentry_sdk.init(SENTRY_DSN, before_send=before_send)
    # Unpickle Redmine objects
    redmine_instance = pickle.load(open(redmine_instance, 'rb'))
    issue = pickle.load(open(issue, 'rb'))
    description = pickle.load(open(description, 'rb'))
    #analyses supported by the automator
    analyses = [
        'custom', 'enterobacterales', 'listeriaceae', 'enterobacter'
    ]
    #commands supported by automator
    commands = [
        'compare', 'dereplicate'
    ]
    #clustering algorithms supported by the automator
    clusteralgorithms = [
        'median', 'weighted', 'single', 'complete', 'average', 'ward', 'centroid'
    ]

    #algorithm for secondary clustering comparisons:
    comparisonalgorithms = [
        'fastANI', 'ANImf', 'ANIn', 'gANI', 'goANI'
    ]

    #variable to hold supplied arguments
    argument_dict = {
        'analysis': str(),
        'command': 'compare',
        'debug': 'False',
        'comparisonalgorithm': 'ANImf',
        'clusteralgorithm': 'average',
        'P_ANI': 0.9,
        'S_ANI': 0.99,
        'coverage_threshold': 0.1,
    }

    #parse description to figure out analysis type, and find fastas
    try:
        #parse description
        seqids = list()
        for item in description:
            item = item.upper().rstrip()
            if 'ANALYSIS' in item:
               argument_dict['analysis'] = item.split('=')[1].lower()
               continue
            if 'COMMAND' in item:
                argument_dict['command'] = item.split('=')[1].lower()
                continue
            if 'DEBUG' in item:
                argument_dict['debug'] = item.split('=')[1].capitalize()
                continue
            if 'CLUSTERALGORITHM' in item:
                argument_dict['clusteralgorithm'] = item.split('=')[1].lower()
                continue
            if 'COMPARISONALGORITHM' in item:
               argument_dict['comparisonalgorithm'] = item.split('=')[1].lower()
               continue
#            if 'MASH_ANI_THRESHOLD' in item:
#               argument_dict['P_ANI'] = int(float(item.split('=')[1].lower()))
#               continue
            if 'MASH_ANI_THRESHOLD' in item:
               argument_dict['P_ANI'] = str(float(item.split('=')[1].lower()))
               continue
            if 'S_ANI' in item:
               argument_dict['S_ANI'] = str(float(item.split('=')[1].lower()))
               continue
            if 'COVERAGE_THRESHOLD' in item:
               argument_dict['coverage_threshold'] = str(float(item.split('=')[1].lower()))
               continue
            # Otherwise the item should be a SEQID
            seqids.append(item)

        # Ensure that the analysis type is provided
        if not argument_dict['analysis']:
            redmine_instance.issue.update(resource_id=issue.id,
                                          notes='WARNING: Could not identify an analysis type. '
                                                'Please ensure that the first line of the issue contains one'
                                                ' of the following keywords: {ats}'.format(ats=', '.join(analyses)),
                                          status_id=4)
            return
        elif argument_dict['analysis'] not in analyses:
            redmine_instance.issue.update(resource_id=issue.id,
                                          notes='WARNING: supplied analysis type {at} current not in the supported '
                                                'list of analyses: {ats}'.format(at=argument_dict['analysis'],
                                                                                ats=', '.join(analyses)),
                                          status_id=4)
            return
        # Ensure that the command type is acceptable
        if argument_dict['command'] not in commands:
            redmine_instance.issue.update(resource_id=issue.id,
                                          notes='WARNING: supplied command type {at} current not in the supported '
                                                'list of commands: {ats}'.format(at=argument_dict['command'],
                                                                                ats=', '.join(commands)),
                                          status_id=4)
            return
        # Ensure that the comparison algorithm type is acceptable
        if argument_dict['comparisonalgorithm'] not in comparisonalgorithms:
            redmine_instance.issue.update(resource_id=issue.id,
                                          notes='WARNING: supplied comparison algorithm type {at} current not in the supported '
                                                'list of algorithms: {ats}'.format(at=argument_dict['comparisonalgorithm'],
                                                                                ats=', '.join(comparisonalgorithms)),
                                          status_id=4)
            return
        # Ensure that the cluster algorithm type is acceptable
        if argument_dict['clusteralgorithm'] not in clusteralgorithms:
            redmine_instance.issue.update(resource_id=issue.id,
                                          notes='WARNING: supplied cluster algorithm type {at} current not in the supported '
                                                'list of algorithms: {ats}'.format(at=argument_dict['clusteralgorithm'],
                                                                                ats=', '.join(clusteralgorithms)),
                                          status_id=4)
            return
    # link to order enterobacterales fasta files if analysis = enterobacterales
        if argument_dict['analysis'] == 'enterobacterales':
            src = '/mnt/nas2/processed_sequence_data/ncbi/enterobacterales_references/BestAssemblies/'
            fasta_dir = os.path.join(work_dir, 'fastas')
            if not os.path.isdir(fasta_dir):
                os.makedirs(fasta_dir)
            dst = fasta_dir
            lncmd = 'ln -s {src}*.fasta {dst}'.format(src=src,
                                                      dst=dst)
            os.system(lncmd)

    #if listeriaceae, link to listeria reference sequences
        if argument_dict['analysis'] == 'listeriaceae':
            src = '/mnt/nas2/processed_sequence_data/ncbi/listeriaceae_references/BestAssemblies/'
            fasta_dir = os.path.join(work_dir, 'fastas')
            if not os.path.isdir(fasta_dir):
                os.makedirs(fasta_dir)
            dst = fasta_dir
            lncmd = 'ln -s {src}*.fasta {dst}'.format(src=src,
                                                      dst=dst)
            os.system(lncmd)

    #if enterobacter, link to enterobacter reference sequences
        if argument_dict['analysis'] == 'enterobacter':
            src = '/mnt/nas2/processed_sequence_data/ncbi/enterobacter_references/BestAssemblies/'
            fasta_dir = os.path.join(work_dir, 'fastas')
            if not os.path.isdir(fasta_dir):
                os.makedirs(fasta_dir)
            dst = fasta_dir
            lncmd = 'ln -s {src}*.fasta {dst}'.format(src=src,
                                                      dst=dst)
            os.system(lncmd)
            src2 = '/mnt/nas2/processed_sequence_data/ncbi/enterobacterales_references/BestAssemblies/'
            fasta_dir = os.path.join(work_dir, 'fastas')
            if not os.path.isdir(fasta_dir):
                os.makedirs(fasta_dir)
            dst = fasta_dir
            lncmd2 = 'ln -s {src}ENTEROBACTER_*.fasta {dst}'.format(src=src2,
                                                                    dst=dst)
            os.system(lncmd2)

    #if just a custom analysis, create the fastas folder
        if argument_dict['analysis'] == 'custom':
            # Make fasta file directory
            fasta_dir = os.path.join(work_dir, 'fastas')
            if not os.path.isdir(fasta_dir):
                os.makedirs(fasta_dir)

    # Rest of Description should just be a list of SEQIDs. Get the fasta files associated with them extracted
	# Run file linker and then make sure that all FASTA files requested are present. Warn user if they
        # requested things that we don't have.
        retrieve_nas_files(seqids=seqids,
                           outdir=fasta_dir,
                           filetype='fasta',
                           copyflag=False)
        missing_fastas = verify_fasta_files_present(seqids, fasta_dir)

	# Verify that specified fasta files are actually there, warn user if they aren't.
        missing_fastas = verify_fasta_files_present(seqid_list=seqids,
                                                    fasta_dir=os.path.join(work_dir, 'fastas'))
        if len(missing_fastas) > 0:
            redmine_instance.issue.update(resource_id=issue.id,
                                          notes='WARNING: Could not find the following requested SEQIDs on'
                                                ' the OLC NAS: {}'.format(missing_fastas))
	# Make output dir
        output_dir = os.path.join(work_dir, 'dRep_results')
        if not os.path.isdir(output_dir):
            os.makedirs(output_dir)

        # These unfortunate hard coded paths appear to be necessary
        activate = 'source /home/ubuntu/miniconda3/bin/activate /mnt/nas2/virtual_environments/dRep'
        # Run dRep compare with the necessary arguments
        if argument_dict['command'] == 'compare':
            dRep_cmd = 'dRep {command} {outpath} -p 10 -g {seqpath}/*.fasta -pa {P_ANI} -sa {S_ANI} -nc {cov_thresh} ' \
                       '--clusterAlg {clusteralgorithm} --S_algorithm {comparisonalgorithm}'.format(command=argument_dict['command'],
                                                                                seqpath=fasta_dir,
                                                                                P_ANI=argument_dict['P_ANI'],
                                                                                S_ANI=argument_dict['S_ANI'],
                                                                                cov_thresh=argument_dict['coverage_threshold'],
                                                                                clusteralgorithm=argument_dict['clusteralgorithm'],
                                                                                comparisonalgorithm=argument_dict['comparisonalgorithm'],
                                                                                outpath=output_dir)

            #create another shell script to execute within the dRep conda environment
            template = "#!/bin/bash\n{} && {}".format(activate, dRep_cmd)
            dRep_script = os.path.join(work_dir, 'run_dRep.sh')
            with open(dRep_script, 'w+') as file:
                file.write(template)

        # Run dRep dreplicate with the necessary arguments
        if argument_dict['command'] == 'dereplicate':
            dRep_cmd = 'dRep {command} {outpath} -p 10 -g {seqpath}/*.fasta -pa {P_ANI} -sa {S_ANI} -nc {cov_thresh} ' \
                       '--clusterAlg {clusteralgorithm} --S_algorithm {comparisonalgorithm}'.format(command=argument_dict['command'],
                                                                                seqpath=fasta_dir,
                                                                                P_ANI=argument_dict['P_ANI'],
                                                                                S_ANI=argument_dict['S_ANI'],
                                                                                cov_thresh=argument_dict['coverage_threshold'],
                                                                                clusteralgorithm=argument_dict['clusteralgorithm'],
                                                                                comparisonalgorithm=argument_dict['comparisonalgorithm'],
                                                                                outpath=output_dir)

            #create another shell script to execute within the dRep conda environment
            template = "#!/bin/bash\n{} && {}".format(activate, dRep_cmd)
            dRep_script = os.path.join(work_dir, 'run_dRep.sh')
            with open(dRep_script, 'w+') as file:
                file.write(template)

        # Modify the permissions of the script to allow it to be run on the node
        make_executable(dRep_script)
        # Run shell script
        os.system(dRep_script)

	# Zip output folder and upload to the FTP, as sometimes files are >10Mb
        shutil.make_archive(root_dir=output_dir,
                            format='zip',
                            base_name=os.path.join(work_dir, str(issue.id)))

        upload_successful = upload_to_ftp(local_file=os.path.join(work_dir, str(issue.id) + '.zip'))

        if upload_successful:
            redmine_instance.issue.update(resource_id=issue.id, status_id=4,
                                          notes='dRep complete!\n\n'
                                                'Results are available at the following FTP address:\n'
                                                'ftp://ftp.agr.gc.ca/outgoing/cfia-ac/{}'.format(str(issue.id) + '.zip'))
        else:
            redmine_instance.issue.update(resource_id=issue.id, status_id=4,
                                          notes='Upload of result files was unsuccessful due to FTP connectivity issues. '
                                                'Please try again later.')

        # Create a list of all the folders - will be used to clean up the working directory
        folders = glob.glob(os.path.join(work_dir, '*/'))
        # Remove all the folders
        for folder in folders:
            if os.path.isdir(folder):
                shutil.rmtree(folder)

        os.remove(os.path.join(work_dir, str(issue.id) + '.zip')) #delete zip file
        # Wrap up issue
#        redmine_instance.issue.update(resource_id=issue.id,
#                                      uploads=output_list,
#                                      status_id=4,
#                                      notes='{at} analysis with dRep complete!'.format(at=argument_dict['analysis'].lower()))
    except Exception as e:
        redmine_instance.issue.update(resource_id=issue.id,
                                      notes='Something went wrong! Send this error traceback to your friendly '
                                            'neighborhood bioinformatician: {}'.format(e))

def verify_fasta_files_present(seqid_list, fasta_dir):
    """
    Makes sure that FASTQ files specified in seqid_list have been successfully copied/linked to directory specified
    by fastq_dir.
    :param seqid_list: List with SEQIDs.
    :param fasta_dir: Directory to which FASTA files should have been linked
    :return: List of SEQIDs that did not have files associated with them.
    """
    missing_fastas = list()
    for seqid in seqid_list:
        # Check forward.
        if len(glob.glob(os.path.join(fasta_dir, '{seqid}*fasta*'.format(seqid=seqid)))) == 0:
            missing_fastas.append(seqid)
    return missing_fastas

def make_executable(path):
    """
    Takes a shell script and makes it executable (chmod +x)
    :param path: path to shell script
    """
    mode = os.stat(path).st_mode
    mode |= (mode & 0o444) >> 2
    os.chmod(path, mode)

def zip_folder(results_path, output_dir, output_filename):
    """
    Compress a folder
    :param results_path: The path of the folder to be compressed
    :param output_dir: The output directory
    :param output_filename: The output file name
    :return:
    """
    output_path = os.path.join(output_dir, output_filename)
    shutil.make_archive(output_path, 'zip', results_path)
    return output_path

if __name__ == '__main__':
    drep_redmine()


from twisted.internet.protocol import ReconnectingClientFactory
from autobahn.twisted.websocket import WebSocketClientProtocol, WebSocketClientFactory
import json
import base64
import subprocess
from twisted.internet.task import LoopingCall
from twisted.internet import reactor

server = "ritaportal.udistrital.edu.co"  # Server IP Address or domain eg: tabvn.com
port = 10207  # Server Port

class AppProtocol(WebSocketClientProtocol):
    INTERVAL=1
    def onConnect(self, response):
        self._loop = LoopingCall(self.envioImage)
        print("server conectado")

    def onConnecting(self, transport_details):
        return None  # ask for defaults

    def onMessage(self, payload, isBinary):
        text_data_json = json.loads(payload.decode('utf8'))

        if(text_data_json['type']=='MC'):
                print("MC")
                self._loop.start(self.INTERVAL)
        elif(text_data_json['type']=='MD'):
                print("MD")
                if _loop.running:
                    _loop.stop()
        else:
            print("No entiendo")

    async def onClose(self, wasClean, code, reason):
        print("WebSocket connection closed: {0}".format(reason))

    def Send(self):
            print("Sending")
            self.sendMessage(json.dumps({'userFrom':'2','userTo': '1','type':'data','message':'hello'}).encode('utf8'))

class AppFactory(WebSocketClientFactory, ReconnectingClientFactory):
    protocol = AppProtocol

    def clientConnectionFailed(self, connector, reason):
        print("Unable connect to the server {0}".format(reason))
        self.retry(connector)

    def clientConnectionLost(self, connector, reason):
        print("Lost connection and retrying... {0}".format(reason))
        self.retry(connector)

if __name__ == '__main__':
    import sys
    from twisted.python import log
    from twisted.internet import reactor
    log.startLogging(sys.stdout)
    factory = AppFactory(u"ws://ritaportal.udistrital.edu.co:10207/jordan")
    reactor.connectTCP(server, port, factory)
    reactor.run()


"""Module containing functions dealing with target input files."""

load(
    "@build_bazel_rules_apple//apple:providers.bzl",
    "AppleResourceBundleInfo",
    "AppleResourceInfo",
)
load("@build_bazel_rules_swift//swift:swift.bzl", "SwiftInfo", "SwiftProtoInfo")
load(":compilation_providers.bzl", "compilation_providers")
load(
    ":files.bzl",
    "FRAMEWORK_EXTENSIONS",
    "RESOURCES_FOLDER_TYPE_EXTENSIONS",
    "normalized_file_path",
)
load(":indexstore_filelists.bzl", "indexstore_filelists")
load(":legacy_target_properties.bzl", "should_include_non_xcode_outputs")
load(":linker_input_files.bzl", "linker_input_files")
load(
    ":memory_efficiency.bzl",
    "EMPTY_DEPSET",
    "EMPTY_DICT",
    "EMPTY_LIST",
    "EMPTY_STRING",
    "memory_efficient_depset",
)
load(":output_files.bzl", "parse_swift_info_module", "swift_to_outputs")
load(":resources.bzl", "collect_resources")
load(":xcodeprojinfo.bzl", "XcodeProjInfo")

# Utility

def _collect_transitive_uncategorized(info):
    if info.xcode_target:
        return EMPTY_DEPSET
    return info.inputs.uncategorized

_IGNORE_ATTR = {
    "to_json": None,
    "to_proto": None,
}

def _should_ignore_input_attr(attr):
    return (
        # We don't want to include implicit dependencies
        attr[0] == "_" or
        # These are actually Starklark methods, so ignore them
        attr in _IGNORE_ATTR
    )

def _process_cc_info_headers(headers, *, exclude_headers, generated):
    def _process_header(header):
        exclude_headers[header] = None
        if not header.is_source:
            generated.append(header)
        return normalized_file_path(
            header,
            folder_type_extensions = FRAMEWORK_EXTENSIONS,
        )

    return [
        _process_header(header)
        for header in headers
        if header not in exclude_headers
    ]

def _transform_into_label_to_resources(resources):
    """Helper function to transform resources for easier consumption

    Args:
        resources: A `list` of tuples (`owner`, `file_path`) of resources that should be
            added to the target's bundle.

    Returns:
        A `list` of tuples (`owner`, depset([`resource`]))
    """
    label_to_resources_depset = {}
    for (owner, resource) in resources:
        label_to_resources_depset.setdefault(owner, {})[resource] = None
    return [
        (owner, depset(resources.keys()))
        for owner, resources in label_to_resources_depset.items()
    ]

# API

C_EXTENSIONS = {
    "c": None,
    "m": None,
}

CXX_EXTENSIONS = {
    "C": None,
    "c++": None,
    "cc": None,
    "cl": None,
    "cpp": None,
    "cu": None,
    "cxx": None,
    "mm": None,
}

def _collect_input_files(
        *,
        ctx,
        target,
        attrs,
        rule_attr,
        unfocused = False,
        id,
        platform,
        is_bundle,
        product,
        linker_inputs,
        automatic_target_info,
        additional_files = EMPTY_LIST,
        modulemaps = None,
        transitive_infos,
        avoid_deps = EMPTY_LIST):
    """Collects all of the inputs of a target.

    Args:
        ctx: The aspect context.
        target: The `Target` to collect inputs from.
        attrs: `dir(ctx.rule.attr)` (as a performance optimization).
        rule_attr: `ctx.rule.attr`.
        unfocused: Whether the target is unfocused. If `None`, it will be
            determined automatically (this should only be the case for
            `non_xcode_target`s).
        id: A unique identifier for the target. Will be `None` for non-Xcode
            targets.
        platform: A value from `platforms.collect`.
        is_bundle: Whether `target` is a bundle.
        product: A value from `process_product`.
        linker_inputs: A value from `linker_file_inputs.collect`.
        automatic_target_info: The `XcodeProjAutomaticTargetProcessingInfo` for
            `target`.
        additional_files: A `list` of `File`s to add to the inputs. This can
            be used to add files to the `generated` and `extra_files` fields
            (e.g. modulemaps or BUILD files).
        modulemaps: A `depset` of `File`s that are modulemap dependencies for
            `target`, or `None`.
        transitive_infos: A `list` of `XcodeProjInfo`s for the transitive
            dependencies of `target`.
        avoid_deps: A `list` of the targets that already consumed resources, and
            their resources shouldn't be bundled with `target`.

    Returns:
        A `tuple` with two elements:

        *   A `struct`, which will only be used within the aspect, with the
            following fields:

            *   `generated`: A `depset` of generated `File`s that are inputs to
                `target` or its transitive dependencies.
            *   `hdrs`: A `list` of `File`s that are inputs to `target`'s
                `hdrs`-like attributes.
            *   `non_arc_srcs`: A `list` of `File`s that are inputs to
                `target`'s `non_arc_srcs`-like attributes.
            *   `resources`: A `depset` of `FilePath`s that are inputs to
                `target`'s `resources`-like and `structured_resources`-like
                attributes.
            *   `srcs`: A `list` of `File`s that are inputs to `target`'s
                `srcs`-like attributes.

        *   A `struct`, which will end up in `XcodeProjInfo.inputs`, with the
            following fields:

            *   `xccurrentversions`: A `depset` of `.xccurrentversion` `File`s
                that are in `resources`.
            *   `generated`: A `depset` of generated `File`s that are inputs to
                `target` or its transitive dependencies.
            *   `important_generated`: A `depset` of important generated `File`s
                that are inputs to `target` or its transitive dependencies.
                These differ from `generated` in that they will be generated as
                part of project generation, to ensure they are created before
                Xcode is opened. Entitlements are an example of this, as Xcode
                won't even start a build if they are missing.
            *   `extra_files`: A `depset` of `FilePath`s that should be included
                in the project, but aren't necessarily inputs to the target.
                This also includes some categorized files of transitive
                dependencies that didn't create an Xcode target.
            *   `uncategorized`: A `depset` of `FilePath`s that are inputs to
                `target` didn't fall into one of the more specific (e.g. `srcs`)
                categories. These will only be included in the Xcode project if
                this target becomes an input to another target's categorized
                attribute.
    """
    entitlements = []
    c_sources = {}
    cxx_sources = {}
    hdrs = []
    non_arc_srcs = []
    pch = []
    srcs = []
    uncategorized = []

    generated = [file for file in additional_files if not file.is_source]
    extra_files = [file.path for file in additional_files]

    label = target.label

    # Include BUILD files for the project but not for external repos
    if not label.workspace_root:
        extra_files.append(ctx.build_file_path)

    # buildifier: disable=uninitialized
    def _handle_srcs_file(file):
        srcs.append(file)
        extension = file.extension
        if extension in C_EXTENSIONS:
            c_sources[file.path] = None
        elif extension in CXX_EXTENSIONS:
            cxx_sources[file.path] = None

    # buildifier: disable=uninitialized
    def _handle_non_arc_srcs_file(file):
        non_arc_srcs.append(file)
        extension = file.extension
        if extension in C_EXTENSIONS:
            c_sources[file.path] = None
        elif extension in CXX_EXTENSIONS:
            cxx_sources[file.path] = None

    # buildifier: disable=uninitialized
    def _handle_hdrs_file(file):
        hdrs.append(file)

    # buildifier: disable=uninitialized
    def _handle_pch_file(file):
        # We use `append` instead of setting a single value because
        # assigning to `pch` creates a new local variable instead of
        # assigning to the existing variable
        pch.append(file)
        extra_files.append(file.path)

    # buildifier: disable=uninitialized
    def _handle_entitlements_file(file):
        # We use `append` instead of setting a single value because
        # assigning to `entitlements` creates a new local variable instead
        # of assigning to the existing variable
        entitlements.append(file)
        extra_files.append(file.path)

    # buildifier: disable=uninitialized
    def _handle_extrafiles_file(file):
        extra_files.append(file.path)

    file_handlers = {}

    if id:
        for attr in automatic_target_info.srcs:
            file_handlers[attr] = _handle_srcs_file
        for attr in automatic_target_info.non_arc_srcs:
            file_handlers[attr] = _handle_non_arc_srcs_file
        for attr in automatic_target_info.hdrs:
            file_handlers[attr] = _handle_hdrs_file
    else:
        # Turn source files into extra files for unsupported targets
        for attr in automatic_target_info.srcs:
            file_handlers[attr] = _handle_extrafiles_file
        for attr in automatic_target_info.non_arc_srcs:
            file_handlers[attr] = _handle_extrafiles_file
        for attr in automatic_target_info.hdrs:
            file_handlers[attr] = _handle_extrafiles_file

    if automatic_target_info.pch:
        file_handlers[automatic_target_info.pch] = _handle_pch_file
    for attr in automatic_target_info.infoplists:
        file_handlers[attr] = _handle_extrafiles_file
    for attr in automatic_target_info.launchdplists:
        file_handlers[attr] = _handle_extrafiles_file
    if automatic_target_info.alternate_icons:
        file_handlers[automatic_target_info.alternate_icons] = (
            _handle_extrafiles_file
        )
    if automatic_target_info.entitlements:
        file_handlers[automatic_target_info.entitlements] = (
            _handle_entitlements_file
        )
    for attr in automatic_target_info.exported_symbols_lists:
        file_handlers[attr] = _handle_extrafiles_file

    categorized_files = {}
    xccurrentversions = []

    # buildifier: disable=uninitialized
    def _handle_file(file, *, handler):
        if file == None:
            return

        if handler:
            handler(file)
            categorized = True
            categorized_files[file] = None
        else:
            categorized = False

        if file.is_source:
            if not categorized:
                if (file.basename == ".xccurrentversion" and
                    file.dirname.endswith(".xcdatamodeld")):
                    # rules_ios's `precompiled_apple_resource_bundle` rule
                    # exposes its resources as inputs, so we have to have the
                    # same check here for the `.xccurrentversion` file as we
                    # do in `resources.bzl`
                    xccurrentversions.append(file)
                else:
                    uncategorized.append(
                        normalized_file_path(
                            file,
                            folder_type_extensions = (
                                RESOURCES_FOLDER_TYPE_EXTENSIONS
                            ),
                        ),
                    )
        elif categorized:
            generated.append(file)

    transitive_extra_files = []

    # buildifier: disable=uninitialized
    def _handle_dep(dep):
        # This allows the transitive uncategorized files for target of a
        # categorized attribute to be included in the project
        if XcodeProjInfo not in dep:
            return
        transitive_extra_files.append(dep[XcodeProjInfo].inputs.uncategorized)

    collect_uncategorized_files = (
        automatic_target_info.collect_uncategorized_files
    )

    for attr in dir(ctx.rule.files):
        if _should_ignore_input_attr(attr):
            continue

        handler = file_handlers.get(attr, None)

        if not collect_uncategorized_files and not handler:
            continue

        for file in getattr(ctx.rule.files, attr):
            _handle_file(file, handler = handler)

    for attr in dir(ctx.rule.file):
        if _should_ignore_input_attr(attr):
            continue

        handler = file_handlers.get(attr, None)

        if not collect_uncategorized_files and not handler:
            continue

        _handle_file(getattr(ctx.rule.file, attr), handler = handler)

    if SwiftProtoInfo in target:
        for file in target[SwiftProtoInfo].pbswift_files.to_list():
            _handle_file(file, handler = _handle_srcs_file)

    for attr in attrs:
        if _should_ignore_input_attr(attr):
            continue

        if attr not in file_handlers:
            # Only attributes in `file_handlers` are categorized
            continue

        dep = getattr(rule_attr, attr)

        dep_type = type(dep)
        if dep_type == "Target":
            _handle_dep(dep)
        elif dep_type == "list":
            if not dep or type(dep[0]) != "Target":
                continue
            for list_dep in dep:
                _handle_dep(list_dep)

    product_framework_files = memory_efficient_depset(
        transitive = [
            info.inputs._product_framework_files
            for info in transitive_infos
        ] + ([product.framework_files] if product else []),
    )

    linker_input_additional_files = linker_input_files.to_input_files(
        linker_inputs,
    )
    if linker_input_additional_files:
        framework_files = {f: None for f in product_framework_files.to_list()}
        linker_input_additional_files = [
            file
            for file in linker_input_additional_files
            if file not in framework_files
        ]
        generated.extend([
            file
            for file in linker_input_additional_files
            if not file.is_source
        ])
        for file in linker_input_additional_files:
            extra_files.append(
                normalized_file_path(
                    file,
                    folder_type_extensions = FRAMEWORK_EXTENSIONS,
                ),
            )

    is_resource_bundle_consuming = is_bundle and AppleResourceInfo in target

    resources = None
    folder_resources = None
    resource_bundles = None
    resource_bundle_dependencies = None
    if is_resource_bundle_consuming:
        resources_result = collect_resources(
            platform = platform,
            resource_info = target[AppleResourceInfo],
            avoid_resource_infos = [
                dep[AppleResourceInfo]
                for dep in avoid_deps
                if AppleResourceInfo in dep
            ],
        )

        generated.extend(resources_result.generated)
        xccurrentversions.extend(resources_result.xccurrentversions)

        bundle_labels_list = [
            bundle.label
            for bundle in resources_result.bundles
        ]
        resource_bundle_labels = memory_efficient_depset(
            bundle_labels_list,
            transitive = [
                dep[XcodeProjInfo].inputs._resource_bundle_labels
                for dep in avoid_deps
            ],
        )
        bundle_labels = {
            label: None
            for label in resource_bundle_labels.to_list()
        }

        transitive_extra_files.extend([
            depset([(label, d)])
            for label, d in depset(
                transitive = [
                    info.inputs._resource_bundle_uncategorized
                    for info in transitive_infos
                ],
            ).to_list()
            if label not in bundle_labels
        ])

        extra_files.extend(resources_result.extra_files)
        resource_bundles = resources_result.bundles
        if resources_result.dependencies:
            resource_bundle_dependencies = resources_result.dependencies
        if resources_result.resources:
            resources = depset(
                _transform_into_label_to_resources(resources_result.resources),
            )
        if resources_result.folder_resources:
            folder_resources = depset(
                _transform_into_label_to_resources(
                    resources_result.folder_resources,
                ),
            )
    else:
        resource_bundle_labels = memory_efficient_depset(
            transitive = [
                info.inputs._resource_bundle_labels
                for info in transitive_infos
            ],
        )

    is_bwx = ctx.attr._build_mode == "xcode"

    # Generically handle CcInfo providing rules. This allows us to pick up
    # headers from `objc_import` and the like.
    if is_bwx and SwiftInfo in target:
        swift_info = target[SwiftInfo]
        for module in swift_info.direct_modules:
            clang = module.clang
            if not clang:
                continue
            for header in clang.compilation_context.direct_public_headers:
                # Exclude Swift generated header, because we don't use it in
                # BwX mode
                categorized_files[header] = None
    if CcInfo in target:
        compilation_context = target[CcInfo].compilation_context
        extra_files.extend(_process_cc_info_headers(
            (compilation_context.direct_private_headers +
             compilation_context.direct_public_headers +
             compilation_context.direct_textual_headers),
            exclude_headers = categorized_files,
            generated = generated,
        ))

    should_produce_output_groups = is_bwx

    # Collect unfocused target info
    indexstores = []
    indexstore_overrides = []
    bwx_unfocused_libraries = None
    if should_include_non_xcode_outputs(ctx = ctx):
        if unfocused == None:
            (dep_compilation_providers, _) = compilation_providers.merge(
                propagate_providers = False,
                transitive_compilation_providers = [
                    (info.xcode_target, info.compilation_providers)
                    for info in transitive_infos
                ],
            )
            (
                direct_libraries,
                transitive_libraries,
            ) = linker_input_files.get_library_static_libraries_for_bwx(
                linker_inputs = linker_inputs,
                dep_compilation_providers = dep_compilation_providers,
            )

            unfocused_generated_linking = transitive_libraries

            unfocused = bool(direct_libraries)
            if unfocused:
                generated.extend(transitive_libraries)
                bwx_unfocused_libraries = memory_efficient_depset(
                    [
                        file.path
                        for file in transitive_libraries
                    ],
                )
        else:
            unfocused_generated_linking = (
                linker_input_files.get_transitive_static_libraries_for_bwx(
                    linker_inputs = linker_inputs,
                )
            )

        if unfocused_generated_linking:
            unfocused_generated_linking = tuple(unfocused_generated_linking)
        else:
            unfocused_generated_linking = None

        is_swift = SwiftInfo in target

        if unfocused and is_swift:
            non_target_swift_info_modules = target[SwiftInfo].transitive_modules
        else:
            non_target_swift_info_modules = memory_efficient_depset(
                transitive = [
                    info.inputs._non_target_swift_info_modules
                    for info in transitive_infos
                ],
            )
        for module in non_target_swift_info_modules.to_list():
            swiftmodules, indexstore = swift_to_outputs(
                parse_swift_info_module(module),
            )
            generated.extend(swiftmodules)
            if should_produce_output_groups and indexstore:
                indexstore_overrides.append((indexstore, EMPTY_STRING))
                indexstores.append(indexstore)

        if is_swift:
            unfocused_swift_info_modules = target[SwiftInfo].transitive_modules
        else:
            unfocused_swift_info_modules = non_target_swift_info_modules

        unfocused_generated_compiling = []
        unfocused_generated_indexstores = []
        for module in unfocused_swift_info_modules.to_list():
            swiftmodules, indexstore = swift_to_outputs(
                parse_swift_info_module(module),
            )

            unfocused_generated_compiling.extend(swiftmodules)
            if indexstore:
                unfocused_generated_indexstores.append(indexstore)

        if unfocused_generated_compiling:
            unfocused_generated_compiling = tuple(unfocused_generated_compiling)
        else:
            unfocused_generated_compiling = None
        if unfocused_generated_indexstores:
            unfocused_generated_indexstores = tuple(
                unfocused_generated_indexstores,
            )
        else:
            unfocused_generated_indexstores = None
    else:
        non_target_swift_info_modules = EMPTY_DEPSET
        unfocused_generated_compiling = None
        unfocused_generated_indexstores = None
        unfocused_generated_linking = None

        # Set non-`None` to prevent hitting the
        # `if not bwx_unfocused_libraries:` check and subsequent calculation
        # below
        bwx_unfocused_libraries = EMPTY_DEPSET

    important_generated = [
        file
        for file in entitlements
        if not file.is_source
    ]

    generated_depset = memory_efficient_depset(
        generated,
        transitive = [
            info.inputs.generated
            for info in transitive_infos
        ],
    )

    if should_produce_output_groups:
        transitive_indexstore_overrides = memory_efficient_depset(
            indexstore_overrides,
            transitive = [
                info.inputs._indexstore_overrides
                for info in transitive_infos
            ],
        )
        transitive_indexstores = memory_efficient_depset(
            indexstores,
            transitive = [
                info.inputs._indexstores
                for info in transitive_infos
            ],
        )
    else:
        transitive_indexstore_overrides = EMPTY_DEPSET
        transitive_indexstores = EMPTY_DEPSET

    # We need to collect transitive modulemaps, because some are private to
    # dependent targets, but we still need them for the final output group
    modulemaps_depset = memory_efficient_depset(
        [f for f in modulemaps if not f.is_source] if modulemaps else None,
        transitive = [
            info.inputs._modulemaps
            for info in transitive_infos
        ],
    )

    # Purposeful flattening to work around large BEP issue.
    # This is because we only get modulemaps already flattened. Ideally we
    # would get a `depset` for the modulemaps, so they would be properly
    # represented in the BEP.
    modulemaps = modulemaps_depset.to_list()

    if id:
        compiling_files = memory_efficient_depset(
            modulemaps,
            transitive = [generated_depset],
        )
    else:
        compiling_files = generated_depset

    if id and should_produce_output_groups:
        compiling_output_group_name = "xc {}".format(id)
        indexstores_output_group_name = "xi {}".format(id)
        linking_output_group_name = "xl {}".format(id)

        indexstores_filelist = indexstore_filelists.write(
            actions = ctx.actions,
            indexstore_and_target_overrides = transitive_indexstore_overrides,
            indexstores = transitive_indexstores,
            name = "xi",
            rule_name = rule_attr.name,
        )

        # We don't want to declare indexstore files as outputs, because they
        # expand to individual files and blow up the BEP
        indexstores_files = depset([indexstores_filelist])

        direct_group_list = [
            (compiling_output_group_name, False, compiling_files),
            (indexstores_output_group_name, True, indexstores_files),
            (linking_output_group_name, False, EMPTY_DEPSET),
        ]
    else:
        compiling_output_group_name = None
        indexstores_output_group_name = None
        linking_output_group_name = None
        direct_group_list = None

    if not bwx_unfocused_libraries:
        bwx_unfocused_libraries = memory_efficient_depset(
            transitive = [
                info.inputs.bwx_unfocused_libraries
                for info in transitive_infos
            ],
        )

    if is_resource_bundle_consuming:
        # We've consumed them above
        resource_bundle_uncategorized = EMPTY_DEPSET
    else:
        if (AppleResourceBundleInfo in target and
            automatic_target_info.bundle_id):
            resource_bundle_uncategorized = uncategorized
            uncategorized = None
        else:
            resource_bundle_uncategorized = None

        if resource_bundle_uncategorized:
            resource_bundle_uncategorized_direct = [
                (
                    label,
                    memory_efficient_depset(resource_bundle_uncategorized),
                ),
            ]
        else:
            resource_bundle_uncategorized_direct = None

        resource_bundle_uncategorized = memory_efficient_depset(
            resource_bundle_uncategorized_direct,
            transitive = [
                info.inputs._resource_bundle_uncategorized
                for info in transitive_infos
            ],
        )

    has_generated_files = bool(generated)
    if not has_generated_files:
        for info in transitive_infos:
            if info.inputs.has_generated_files:
                has_generated_files = True
                break

    return (
        struct(
            compiling_files = compiling_files,
            compiling_output_group_name = compiling_output_group_name,
            entitlements = entitlements[0] if entitlements else None,
            folder_resources = folder_resources,
            generated = generated_depset,
            c_sources = c_sources,
            cxx_sources = cxx_sources,
            hdrs = hdrs,
            indexstores = transitive_indexstores,
            indexstores_output_group_name = indexstores_output_group_name,
            linking_output_group_name = linking_output_group_name,
            non_arc_srcs = non_arc_srcs,
            pch = pch[0] if pch else None,
            resource_bundle_dependencies = memory_efficient_depset(
                resource_bundle_dependencies,
            ),
            resources = resources,
            srcs = srcs,
            unfocused_generated_compiling = unfocused_generated_compiling,
            unfocused_generated_indexstores = unfocused_generated_indexstores,
            unfocused_generated_linking = unfocused_generated_linking,
        ),
        struct(
            _indexstore_overrides = transitive_indexstore_overrides,
            _indexstores = transitive_indexstores,
            _modulemaps = modulemaps_depset,
            _non_target_swift_info_modules = non_target_swift_info_modules,
            _output_group_list = memory_efficient_depset(
                direct_group_list,
                transitive = [
                    info.inputs._output_group_list
                    for info in transitive_infos
                ],
            ) if should_produce_output_groups else EMPTY_DEPSET,
            _product_framework_files = product_framework_files,
            _resource_bundle_labels = resource_bundle_labels,
            _resource_bundle_uncategorized = resource_bundle_uncategorized,
            resource_bundles = memory_efficient_depset(
                resource_bundles,
                transitive = [
                    info.inputs.resource_bundles
                    for info in transitive_infos
                ],
            ),
            xccurrentversions = memory_efficient_depset(
                [(label, tuple(xccurrentversions))] if xccurrentversions else None,
                transitive = [
                    info.inputs.xccurrentversions
                    for info in transitive_infos
                ],
            ),
            generated = generated_depset,
            important_generated = memory_efficient_depset(
                important_generated,
                transitive = [
                    info.inputs.important_generated
                    for info in transitive_infos
                ],
            ),
            has_generated_files = has_generated_files,
            extra_files = memory_efficient_depset(
                [(label, depset(extra_files))] if extra_files else None,
                transitive = [
                    depset(transitive = [info.inputs.extra_files])
                    for info in transitive_infos
                ] + transitive_extra_files,
            ),
            uncategorized = memory_efficient_depset(
                [(label, depset(uncategorized))] if uncategorized else None,
                transitive = [
                    _collect_transitive_uncategorized(info)
                    for info in transitive_infos
                ],
            ),
            bwx_unfocused_libraries = bwx_unfocused_libraries,
        ),
    )

def _from_resource_bundle(bundle):
    return struct(
        compiling_output_group_name = None,
        entitlements = None,
        folder_resources = depset(
            _transform_into_label_to_resources(bundle.folder_resources),
        ),
        generated = EMPTY_DEPSET,
        c_sources = EMPTY_DICT,
        cxx_sources = EMPTY_DICT,
        hdrs = EMPTY_LIST,
        indexstores = EMPTY_DEPSET,
        indexstores_output_group_name = None,
        linking_output_group_name = None,
        non_arc_srcs = EMPTY_LIST,
        pch = None,
        resource_bundle_dependencies = bundle.dependencies,
        resources = depset(
            _transform_into_label_to_resources(bundle.resources),
        ),
        srcs = EMPTY_LIST,
        unfocused_generated_compiling = None,
        unfocused_generated_indexstores = None,
        unfocused_generated_linking = None,
    )

def _merge_input_files(*, transitive_infos, extra_generated = None):
    """Creates merged inputs.

    Args:
        transitive_infos: A `list` of `XcodeProjInfo`s for the transitive
            dependencies of the current target.
        extra_generated: An optional `list` of `File`s to added to `generated`.

    Returns:
        A value similar to the one returned from `input_files.collect`. The
        values potentially include the inputs of the transitive dependencies,
        via `transitive_infos` (e.g. `generated` and `extra_files`).
    """
    has_generated_files = False
    for info in transitive_infos:
        if info.inputs.has_generated_files:
            has_generated_files = True
            break

    return struct(
        _indexstore_overrides = memory_efficient_depset(
            transitive = [
                info.inputs._indexstore_overrides
                for info in transitive_infos
            ],
        ),
        _indexstores = memory_efficient_depset(
            transitive = [
                info.inputs._indexstores
                for info in transitive_infos
            ],
        ),
        _modulemaps = memory_efficient_depset(
            transitive = [
                info.inputs._modulemaps
                for info in transitive_infos
            ],
        ),
        _non_target_swift_info_modules = memory_efficient_depset(
            transitive = [
                info.inputs._non_target_swift_info_modules
                for info in transitive_infos
            ],
        ),
        _output_group_list = memory_efficient_depset(
            transitive = [
                info.inputs._output_group_list
                for info in transitive_infos
            ],
        ),
        _product_framework_files = memory_efficient_depset(
            transitive = [
                info.inputs._product_framework_files
                for info in transitive_infos
            ],
        ),
        _resource_bundle_labels = memory_efficient_depset(
            transitive = [
                info.inputs._resource_bundle_labels
                for info in transitive_infos
            ],
        ),
        _resource_bundle_uncategorized = memory_efficient_depset(
            transitive = [
                info.inputs._resource_bundle_uncategorized
                for info in transitive_infos
            ],
        ),
        resource_bundles = memory_efficient_depset(
            transitive = [
                info.inputs.resource_bundles
                for info in transitive_infos
            ],
        ),
        xccurrentversions = memory_efficient_depset(
            transitive = [
                info.inputs.xccurrentversions
                for info in transitive_infos
            ],
        ),
        generated = memory_efficient_depset(
            extra_generated,
            transitive = [
                info.inputs.generated
                for info in transitive_infos
            ],
        ),
        important_generated = memory_efficient_depset(
            transitive = [
                info.inputs.important_generated
                for info in transitive_infos
            ],
        ),
        has_generated_files = has_generated_files,
        extra_files = memory_efficient_depset(
            transitive = [
                info.inputs.extra_files
                for info in transitive_infos
            ],
        ),
        uncategorized = memory_efficient_depset(
            transitive = [
                info.inputs.uncategorized
                for info in transitive_infos
            ],
        ),
        bwx_unfocused_libraries = memory_efficient_depset(
            transitive = [
                info.inputs.bwx_unfocused_libraries
                for info in transitive_infos
            ],
        ),
    )

def _process_output_group_files(
        *,
        files,
        is_indexstores,
        output_group_name,
        additional_bwx_generated,
        index_import):
    # `list` copy is needed for some reason to prevent depset from changing
    # underneath us. Without this it's nondeterministic which files are in it.
    generated_depsets = list(
        additional_bwx_generated.get(output_group_name, []),
    )

    if is_indexstores:
        direct = [index_import]
    else:
        direct = None

    return memory_efficient_depset(
        direct,
        transitive = generated_depsets + [files],
    )

def _to_output_groups_fields(
        *,
        inputs,
        additional_bwx_generated = {},
        index_import):
    """Generates a dictionary to be splatted into `OutputGroupInfo`.

    Args:
        inputs: A value from `input_files.collect`.
        additional_bwx_generated: A `dict` that maps the output group name of
            targets to a `list` of `depset`s of `File`s that should be merged
            into the output group map for that output group name.
        index_import: A `File` for `index-import`.

    Returns:
        A `dict` where the keys are output group names and the values are
        `depset` of `File`s.
    """
    output_groups = {
        name: _process_output_group_files(
            files = files,
            is_indexstores = is_indexstores,
            output_group_name = name,
            additional_bwx_generated = additional_bwx_generated,
            index_import = index_import,
        )
        for name, is_indexstores, files in inputs._output_group_list.to_list()
    }

    output_groups["all_xc"] = memory_efficient_depset(
        transitive = [
            files
            for name, files in output_groups.items()
            if name.startswith("xc ")
        ],
    )
    output_groups["all_xi"] = memory_efficient_depset(
        transitive = [
            files
            for name, files in output_groups.items()
            if name.startswith("xi ")
        ],
    )
    output_groups["all_xl"] = memory_efficient_depset(
        transitive = [
            files
            for name, files in output_groups.items()
            if name.startswith("xl ")
        ],
    )

    return output_groups

input_files = struct(
    collect = _collect_input_files,
    from_resource_bundle = _from_resource_bundle,
    merge = _merge_input_files,
    to_output_groups_fields = _to_output_groups_fields,
)


""" Module for manipulating raster data in different coordinate reference
systems (crs) """

import os
from typing import List, Tuple, Union
import numpy as np
import rasterio as rs
from rasterio.warp import transform, reproject
from rasterio.crs import CRS

def get_raster_in_projected_crs(
    fpath: str,
    proj_bounds: Tuple[float, float, float, float],
    proj_gridsize: Tuple[int, int],
    proj_res: Union[float, Tuple[float, float]],
    proj_crs_string: str
):
    """ Get raster data from fpath in projected crs """

    # get the CRS object from projected crs
    proj_crs = get_rasterio_crs_object(proj_crs_string)
    assert proj_crs.is_projected, f'{proj_crs_string} is not a projected crs!'
    check_if_raster_file_exists(fpath)

    # Compute the Affine transform in projected crs
    proj_dx = proj_res if isinstance(proj_res, float) else proj_res[0]
    proj_dy = proj_res if isinstance(proj_res, float) else proj_res[1]
    assert (proj_dx > 0. and proj_dy > 0.), f'{proj_res} invalid resolution!'
    proj_transform = rs.transform.from_origin(
        proj_bounds[0], proj_bounds[3], proj_dx, proj_dy)

    # reproject the data into projeted crs
    resampling_method = rs.warp.Resampling.bilinear
    band = 1
    proj_dem = np.empty(proj_gridsize)
    try:
        with rs.open(fpath) as src_img:
            reproject(source=src_img.read(band),
                      destination=proj_dem,
                      src_transform=src_img.transform,
                      src_crs=src_img.crs,
                      dst_transform=proj_transform,
                      dst_crs=proj_crs,
                      resampling=resampling_method,
                      num_threads=1)
    except FileNotFoundError as _:
        raise FileNotFoundError(f'Need to download {fpath} first!') from None
    return np.flipud(proj_dem)

def transform_bounds(
        src_bounds: Tuple[float, float, float, float],
        src_crs_string: str,
        dest_crs_string: str,
        pad: float = 0.
) -> Tuple[float, float, float, float]:
    """ Compute bounds of the rectangular region in destination coordinate
    reference system (crs) that is contained within the bounds in source crs

    Parameters:
    ------
    src_bounds: Tuple[float, float, float, float]
        Defines the bounds = (min_lon, min_lat, max_lon, max_lat) of the
        rectangular region in source crs
    src_crs_string: str
        EPSG, ESRI, PROJ4, or WKT string that defines the source crs
    dest_crs_string: str
        EPSG, ESRI, PROJ4, or WKT string that defines the destination crs

    Returns:
    -------
    dest_bounds: Tuple[float, float, float, float]
        (min_x, min_y, max_x, max_y) in destination crs
    """
    src_crs = get_rasterio_crs_object(src_crs_string)
    dest_crs = get_rasterio_crs_object(dest_crs_string)
    src_pts = get_corner_points_from_bounds(src_bounds)
    dest_x, dest_y = transform_coordinates(
        src_crs, dest_crs, src_pts[0], src_pts[1])
    dest_bounds = (min(dest_x), min(dest_y), max(dest_x), max(dest_y))
    padding = [-pad, -pad, pad, pad]
    dest_bounds = [ix + iy for ix, iy in zip(dest_bounds, padding)]
    return dest_bounds

def transform_coordinates(
    in_crs: Union[str, rs.crs.CRS],
    out_crs: Union[str, rs.crs.CRS],
    in_x: Union[float, List[float], np.ndarray],
    in_y: Union[float, List[float], np.ndarray]
):
    """Transform points from source crs to destination crs

    Parameters:
    ----------
    in_crs: string or rasterio.crs.CRS object
        Defines the coordinate ref system for the input
    out_crs: string or rasterio.crs.CRS object
        Defines the coordinate ref system for the output
    in_x: float or List or numpy array
        input values in x direction (easting)
    out_x: float or List or numpy array
        output values in y direction (northing)

    Returns:
    -------
    out_x: list of
    """

    # convert floats into list
    in_x = [in_x] if isinstance(in_x, float) else in_x
    in_y = [in_y] if isinstance(in_y, float) else in_y

    # convert multi dimensional numpy array to 1D array that transform needs
    if isinstance(in_x, np.ndarray):
        out_shape = in_x.shape
        in_x = in_x.flatten()
        in_y = in_y.flatten()
    assert len(in_x) == len(in_y)

    # Get CRS object for input crs
    if not isinstance(in_crs, rs.crs.CRS):
        in_crs = get_rasterio_crs_object(in_crs)
    if not isinstance(out_crs, rs.crs.CRS):
        out_crs = get_rasterio_crs_object(out_crs)
    assert out_crs.is_valid

    # Perform the transformation
    while True:
        try:
            out_x, out_y = transform(in_crs, out_crs, in_x, in_y)
        except:
            continue
        else:
            break
    # except Exception as _:
    #     raise ValueError(f'Invalid coordinates {in_x}, {in_y}!') from None

    # reshape output to match the input numpy array
    if 'out_shape' in locals():
        out_x = np.reshape(out_x, out_shape)
        out_y = np.reshape(out_y, out_shape)
    return out_x, out_y

def get_raster_data(fpath: str, band: int = 1) -> np.ndarray:
    """ Get data saved in fpath (GeoTif file) in source crs """
    check_if_raster_file_exists(fpath)
    with rs.open(fpath) as src_img:
        src_data = src_img.read(band)
    return np.flipud(src_data)

def get_raster_bounds(fpath: str) -> Tuple[float, float, float, float]:
    """ Get bounds of the raster data in fpath (GeoTif file) """
    check_if_raster_file_exists(fpath)
    with rs.open(fpath) as src_img:
        src_bnds = [round(ix, 8) for ix in src_img.bounds]
    return src_bnds

def check_if_raster_file_exists(fpath: str):
    """ Check if GeoTif file exists """
    if not os.path.isfile(fpath):
        raise FileNotFoundError(f'{fpath} not found!\nRun download()')

def get_rasterio_crs_object(crs_string: str) -> rs.crs.CRS:
    """ Returns a valid rasterio CRS object from given EPSG, ESRI, PROJ4,
    or WKT string """
    err_string = (f'Rasterio: {crs_string} is an invalid crs!\n'
                  'Choose a valid EPSG, ESRI, PROJ4, or WKT string')
    try:
        proj_crs = CRS.from_user_input(crs_string)
    except rs.errors.CRSError as _:
        raise ValueError(err_string) from None
    else:
        if not proj_crs.is_valid:
            raise ValueError(err_string)
    return proj_crs

def get_utm_string(west_lon: float) -> str:
    """ Returns PROJ4 string for UTM projection for a given zone, computed
    using the west (longitude) value

    Parameters:
    ----------
    west_lon : float
        Longitude for the western most point of the domain

    Returns:
    -------
    PROJ4 string for the UTM zone
    """

    datum = 'WGS84'
    ellps = 'WGS84'
    zone_number = int((west_lon + 180) / 6) + 1
    proj_string = (f'+proj=utm +zone={zone_number} +datum={datum} +units=m'
                   f'+no_defs +ellps={ellps} +towgs84=0,0,0')
    return proj_string

def get_corner_points_from_bounds(bounds: Tuple[float, float, float, float]):
    """ get coordinates of corner points from bounds list """
    xord = [bounds[0], bounds[0], bounds[2], bounds[2]]
    yord = [bounds[1], bounds[3], bounds[1], bounds[3]]
    return [xord, yord]

# Junk
# def is_bounds_within_bounds(
#     bounds_a: Tuple[float, float, float, float],
#     bounds_b: Tuple[float, float, float, float]
# ) -> bool:
#     """ Check if one bounds are within (subset) of other bounds"""
#     return_bool = False
#     if (
#         (bounds_a[0] >= bounds_b[0]) &
#         (bounds_a[1] >= bounds_b[1]) &
#         (bounds_a[2] <= bounds_b[2]) &
#         (bounds_a[3] <= bounds_b[3])
#     ):
#         return_bool = True
#     return return_bool

    # def compute_slope_and_aspect(
    #     tr_elev: np.ndarray,
    #     tr_res: float
    # ) -> Tuple[np.ndarray, np.ndarray]:
    #     """Computes terrain slope and aspect using terrain elevation with a
    #     given grid spacing"""

    #     print('Computing terrain slope and aspect...', end="", flush=True)
    #     grid_size = tr_elev.shape
    #     mask_reach = 1
    #     mask_center = np.array((mask_reach, mask_reach))
    #     mask_width = mask_reach * 2 + 1
    #     mask_size = np.array((mask_width, mask_width))
    #     mask_inverse_distances = np.empty(mask_size)
    #     mask_aspects = np.empty(mask_size)
    #     for r in range(mask_size[0]):
    #         for c in range(mask_size[1]):
    #             pos = np.array((r, c)) - mask_center
    #             if r == mask_reach and c == mask_reach:
    #                 mask_inverse_distances[r, c] = 0.0
    #                 mask_aspects[r, c] = 0.0
    #             else:
    #                 mask_inverse_distances[r, c] = 1000.0 / \
    #                     (tr_res * np.linalg.norm(pos))
    #                 # CCW angle relative to north
    #                 mask_aspects[r, c] = np.arctan2(pos[1], pos[0])
    #     mask_aspects_modified = np.flip(mask_aspects, axis=0) + np.pi
    #     # print(mask_inverse_distances,'\n',mask_aspects_modified*180/np.pi)
    #     tr_hgt_padded = np.pad(
    #         tr_elev, (mask_center, mask_center), mode='edge')
    #     terrain_slope = np.empty(grid_size)
    #     terrain_aspect = np.empty(grid_size)
    #     for r in range(grid_size[0]):
    #         for c in range(grid_size[1]):
    #             pos = np.array((r, c))
    #             center = pos + mask_center
    #             end = pos + mask_size
    #             source_region = np.flip(tr_hgt_padded[r:end[0], c:end[1]], axis=0)
    #             center_alt = tr_hgt_padded[center[0], center[1]]
    #             slopes = np.arctan((source_region - center_alt)
    #                                * mask_inverse_distances)
    #             max_index = np.unravel_index(np.argmax(slopes), slopes.shape)
    #             terrain_slope[r, c] = slopes[max_index]
    #             terrain_aspect[r, c] = mask_aspects_modified[max_index]
    #     print('done')
    #     return terrain_slope.astype(np.float32), terrain_aspect.astype(np.float32)

# def compute_proj_bounds_from_lonlat_bounds(
#         lonlat_bounds: Tuple[float, float, float, float],
#         proj_crs_string: str,
#         margin_km: float = 0.0
# ) -> List[float]:
#     """ Compute bounds of the rectangular region in projected coordinate
#     reference system (crs) that is contained within the bounds in geographical
#     (lon/lat) crs

#     Parameters:
#     ------
#     lonlat_bounds: Tuple[float, float, float, float]
#         Defines the bounds = (min_lon, min_lat, max_lon, max_lat) of the
#         rectangular region in lon/lat crs
#     proj_crs_string: str
#         EPSG, ESRI, PROJ4, or WKT string that defines the projected crs
#     margin_km: float, defaults to 0
#         padding at the corners of the rectangular region in projected crs

#     Returns:
#     -------
#     bounds: (min_lon, min_lat, max_lon, max_lat) in projected crs in meters
#     """
#     lonlat_crs = CRS.from_user_input('EPSG:4326')
#     proj_crs = CRS.from_user_input(proj_crs_string)
#     assert proj_crs.is_projected
#     lonlat_pts = get_corner_points_from_bounds(lonlat_bounds)
#     proj_xs, proj_ys = transform_crs(lonlat_crs, proj_crs,
#                                      lonlat_pts[0], lonlat_pts[1])
#     pad = [margin_km, margin_km, -margin_km, -margin_km]
#     proj_bounds = [min(proj_xs), min(proj_ys), max(proj_xs), max(proj_ys)]
#     return [ix + iy * 1000. for ix, iy in zip(proj_bounds, pad)]

# def compute_lonlat_bounds_from_proj_bounds(
#         proj_bounds: Tuple[float, float, float, float],
#         proj_crs_string: str,
#         margin_km: float = 0.0
# ) -> List[float]:
#     """ Set bounds in destination crs for the target region

#     Parameters:
#     -----------
#     dest_bounds: Tuple[float, float, float, float]
#         Defines the (min_x, min_y, max_x, max_y) of the target region in
#         destination crs where all values are in meters
#     margin_km: float, optional
#         Margin for ensuring target region in destination crs is subset of
#         bounds in lon/lat crs
#     """
#     lonlat_crs = CRS.from_user_input('EPSG:4326')
#     proj_crs = CRS.from_user_input(proj_crs_string)
#     assert proj_crs.is_projected
#     pad = [-margin_km, -margin_km, margin_km, margin_km]
#     pad_proj_bounds = [ix + iy * 1000. for ix, iy in zip(proj_bounds, pad)]
#     proj_pts = get_corner_points_from_bounds(pad_proj_bounds)
#     lons, lats = transform_crs(proj_crs, lonlat_crs, proj_pts[0], proj_pts[1])
#     lonlat_bounds = [min(lons), min(lats), max(lons), max(lats)]
#     return [round(ix, 8) for ix in lonlat_bounds]

    # def set_bounds_in_src_crs(
    #         self,
    #         src_bounds: Tuple[float, float, float, float]
    # ) -> None:
    #     """ Set bounds in source crs for the target region

    #     Parameters:
    #     -----------

    #     src_bounds: Tuple[float, float, float, float]
    #         Defines the (min_lon, min_lat, max_lon, max_lat) of the target
    #         region in lon/lat coordinate system
    #     """

    #     print('Terrain: Setting bounds in source crs..')
    #     src_pts = get_corner_points_from_bounds(src_bounds)
    #     dest_xs, dest_ys = transform_crs(self.src_crs, self.dest_crs,
    #                                      src_pts[0], src_pts[1])
    #     self.dest_bounds = [min(dest_xs), min(dest_ys),
    #                         max(dest_xs), max(dest_ys)]
    #     self.src_bounds = src_bounds

    # def set_bounds_in_dest_crs(
    #         self,
    #         dest_bounds: Tuple[float, float, float, float],
    #         margin_km: float = 0.1
    # ) -> None:
    #     """ Set bounds in destination crs for the target region

    #     Parameters:
    #     -----------
    #     dest_bounds: Tuple[float, float, float, float]
    #         Defines the (min_x, min_y, max_x, max_y) of the target region in
    #         destination crs where all values are in meters
    #     margin_km: float, optional
    #         Margin for ensuring target region in destination crs is subset of
    #         bounds in lon/lat crs
    #     """

    #     print('Terrain: Setting bounds in destination crs..')
    #     pad = [-margin_km, -margin_km, margin_km, margin_km]
    #     pad_dest_bounds = [ix + iy * 1000. for ix, iy in zip(dest_bounds, pad)]
    #     pad_dest_pts = get_corner_points_from_bounds(pad_dest_bounds)
    #     src_lons, src_lats = transform_crs(
    #         self.dest_crs, self.src_crs, pad_dest_pts[0], pad_dest_pts[1])
    #     self.src_bounds = [min(src_lons), min(src_lats),
    #                        max(src_lons), max(src_lats)]
    #     self.src_bounds = [round(ix, 8) for ix in self.src_bounds]
    #     self.dest_bounds = dest_bounds

    # def set_bounds_southwest_and_sizeinkm_dest(
    #     self,
    #     southwest_src: Tuple[float, float],
    #     sizeinkm_dest: Tuple[float, float]
    # ) -> None:
    #     """ Set bounds using southwest point in source crs and width and height
    #     in kms in destination crs

    #     Parameters:
    #     -----------
    #     southwest_src: Tuple[float, float]
    #         Defines the (lon, lat) of the southwest point for the target region
    #     sizeinkm_dest: Tuple[float, float]
    #         Defines the size (xsize, ysize) of region in kilometers
    #     """

    #     print('Terrain: Setting bounds using southwest_lonlat and size_km..')
    #     src_west, src_south = southwest_src
    #     dest_west, dest_south = transform_crs(
    #         self.src_crs, self.dest_crs, src_west, src_south)
    #     dest_west = dest_west[0]
    #     dest_south = dest_south[0]
    #     dest_east = dest_west + sizeinkm_dest[0] * 1000.
    #     dest_north = dest_south + sizeinkm_dest[1] * 1000.
    #     dest_bounds = (dest_west, dest_south, dest_east, dest_north)
    #     self.set_bounds_in_dest_crs(dest_bounds, 0.25)

# Junk
    # def download(self) -> None:
    #     """ Download data from the WMS server """

    #     # figure out the grid size of from bounds and resolution
    #     width = int(round((self.bnds[2] - self.bnds[0]) / self.res))
    #     height = int(round((self.bnds[3] - self.bnds[1]) / self.res))
    #     count_xtiles = int(width // self.max_gridsize) + 1
    #     count_ytiles = int(height // self.max_gridsize) + 1
    #     xbnds = np.linspace(self.bnds[0], self.bnds[2], count_xtiles + 1)
    #     ybnds = np.linspace(self.bnds[1], self.bnds[3], count_ytiles + 1)

    #     # break the region into smaller tiles
    #     k = 0
    #     fname_list = []
    #     bnds_list = []
    #     width_list = []
    #     height_list = []
    #     for i in range(count_xtiles):
    #         for j in range(count_ytiles):
    #             k_bnd = [xbnds[i], ybnds[j], xbnds[i + 1], ybnds[j + 1]]
    #             bnds_list.append(k_bnd)
    #             width_list.append(int(round((k_bnd[2] - k_bnd[0]) / self.res)))
    #             height_list.append(
    #                 int(round((k_bnd[3] - k_bnd[1]) / self.res)))
    #             fname_list.append(f'{self.out_dir}/wms_raw_{k}.tif')
    #             k += 1

    #     # tried requesting data using pathos but slower compared to serial
    #     # start = time.time()
    #     # with mp.Pool(2) as pool:
    #     #     imaps = pool.map(lambda i: self.wms.getmap(
    #     #         layers=[self.layer],
    #     #         srs=self.crsys,
    #     #         bbox=bnds_list[i],
    #     #         format=self.fmt,
    #     #         size=(width_list[i], height_list[i])
    #     #     ), range(len(bnds_list)))
    #     # for i, imap in enumerate(imaps):
    #     #     with open(fname_list[i], 'wb') as out_file:
    #     #         out_file.write(imap.read())
    #     # end = time.time()
    #     # print(f'parallel: took {round(end - start,2)} seconds')

    #     print(f'WMS: Downloading {count_xtiles}*{count_ytiles}={k} tiles..')
    #     for b, fp, w, h in zip(bnds_list, fname_list, width_list, height_list):
    #         imap = self.wms.getmap(
    #             layers=[self.layer],
    #             srs=self.crsys,
    #             bbox=b,
    #             format=self.fmt,
    #             size=(w, h)
    #         )
    #         with open(fp, 'wb') as out_file:
    #             out_file.write(imap.read())

    #     # merge tiled data and save it to a file
    #     print('WMS: Merging tiles..')
    #     src_files_to_mosaic = []
    #     for fname in fname_list:
    #         src = rs.open(fname)
    #         src_files_to_mosaic.append(src)
    #     mosaic, out_trans = merge(src_files_to_mosaic)
    #     out_meta = src.meta.copy()
    #     out_meta.update({"driver": "GTiff",
    #                      "height": mosaic.shape[1],
    #                      "width": mosaic.shape[2],
    #                      "transform": out_trans,
    #                      "crs": self.crsys
    #                      })
    #     print(f'WMS: Shape is ({mosaic.shape[1]},{mosaic.shape[2]})')
    #     with rs.open(self.fpath, "w", **out_meta) as dest:
    #         dest.write(mosaic)

    #     # close the opened raster files and do cleanup
    #     for src in src_files_to_mosaic:
    #         src.close()
    #         if self.cleanup:
    #             try:
    #                 os.remove(src.name)
    #             except OSError as e_obj:
    #                 if e_obj.errno != errno.ENOENT:
    #                     raise

    # def get_raw_raster(self):
    #     """ Get raw raster object for debugging """

    #     src = rs.open(self.fpath)
    #     return src

# def compute_bounds_using_southwest_and_size(
#     southwest_lonlat: Tuple[float, float],
#     sizeinkm: Tuple[float, float],
#     proj_crs_string: str,
#     padding: float = 0.001
# ):
#     """ Compute bounds in projected crs using southwest point in lon/lat crs
#     and width and height of the rect region in kms in a projected crs

#     Parameters:
#     ------
#     southwest_lonlat: Tuple[float, float]
#         Defines the (lon, lat) of the southwest point for the target region
#     sizeinkm: Tuple[float, float]
#         Defines the size (xsize, ysize) of region in kilometers
#     proj_crs_string: str
#         EPSG, ESRI, PROJ4, or WKT string that defines the projected crs

#     Returns:
#     -------
#     lonlat_bounds: Tuple[float, float, float, float]
#         (min_x, min_y, max_x, max_y) in lonlat crs
#     proj_bounds: Tuple[float, float, float, float]
#         (min_x, min_y, max_x, max_y) in projected crs
#     """

#     # get the CRS object for lonlat and projected crs
#     lonlat_crs = get_rasterio_crs_object('EPSG:4326')
#     proj_crs = get_rasterio_crs_object(proj_crs_string)
#     assert proj_crs.is_projected, f'{proj_crs_string} not a projected crs!'

#     # compute the bounds in projected crs
#     lon_west, lat_south = southwest_lonlat
#     proj_west, proj_south = transform_crs(lonlat_crs, proj_crs,
#                                           lon_west, lat_south)
#     proj_east = proj_west[0] + sizeinkm[0] * 1000.
#     proj_north = proj_south[0] + sizeinkm[1] * 1000.
#     proj_bounds = (proj_west[0], proj_south[0], proj_east, proj_north)
#     lonlat_bounds = transform_bounds(proj_bounds, proj_crs_string, 'EPSG:4236')
#     pad = [-padding, -padding, padding, padding]
#     lonlat_bounds = [round(ix + iy, 4) for ix, iy in zip(lonlat_bounds, pad)]
#     return lonlat_bounds, proj_bounds

# def get_raster_bounds_and_size(
#     bnds: Tuple[float, float, float, float],
#     proj_dx: float,
#     proj_dy: float
# ):
#     """  Get bounds compatible with width, height for a given resolution """
#     proj_west, proj_south, proj_east, proj_north = bnds
#     width = int(round(((proj_east - proj_west) / proj_dx)))
#     height = int(round(((proj_north - proj_south) / proj_dy)))
#     new_east = proj_west + width * proj_dx
#     new_north = proj_south + height * proj_dy
#     return (proj_west, proj_south, new_east, new_north), width, height


import config
import telebot
import db
from smtplib import SMTP
from threading import Timer
from time import strftime, localtime, time
from os import mkdir
from os.path import isfile, isdir
from email.utils import formataddr
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from requests import get
from shutil import disk_usage
from platform import system
from socket import gethostname, gethostbyname_ex

SUPPORTS = [i['chat_id'] for sub in (config.supports,) for i in sub]
SUPPORTGROUPS = [i['chat_id'] for sub in (config.supportgroups,) for i in sub]

bot = telebot.TeleBot(config.token)

def send_authcode(addr, regcode):
    subj = 'Код регистрации в Службе технической поддержки'
    _from =  formataddr((str(Header('Сервисный бот', 'utf-8')), config.email_address))
    to = addr
    body = 'Добрый день!\n\n\nВы получили это письмо, потому что отправили обращение Telegram-боту ' + \
           'службы технической поддержки.\n\nВам следует сообщить боту код аутентификации: ' + str(regcode)
    send_email(_from, to, subj, body)

def send_email(from_field=None, to_field=None, subject_field=None, msg_body=None):

    server = SMTP(config.mail_server, config.mail_port)
    server.starttls()
    server.login(config.email_login, config.email_password)
    msg = MIMEMultipart()
    msg['From'] = from_field
    msg['To'] = to_field
    msg['Subject'] = subject_field
    body = MIMEText(msg_body, 'plain', 'utf-8')
    msg.attach(body)
    server.sendmail(config.email_address, to_field, msg.as_string())

def print_userinfo(chat_id, with_log=False):

    user = db.get_user_namedtuple(chat_id)
    userinfo = ('Username: ' + user.first_name + ' ' + user.last_name + '\n'
                'Chat_id: ' + user.chat_id + '\n'
                'Email: ' + user.email_address + '\n'
               )
    if(with_log):
        userinfo += 'Message_log:\n' + user.message_log + '\n'
    return userinfo

def get_support_name_by_chat_id(chat_id):

    n = [i['name'] for i in config.supports if i['chat_id'] == chat_id]
    return n[0] if n[0] else None

def get_keyboard(buttons):

    keyboard = telebot.types.InlineKeyboardMarkup()
    for k in buttons:
        callback_button = telebot.types.InlineKeyboardButton(text=k[0], callback_data=k[1])
        keyboard.add(callback_button)
    return keyboard

# Таймер проверки времени с момента ответа специалиста
def timer(func, interval):

    func()
    Timer(interval, timer, (func, interval)).start()

def check_user_noreply_interval():

    communics = db.get_all_users_namedtuple()
    if communics:
        for u in communics:
            if u.last_msg_time_from_user != 0 and u.last_msg_time_from_specialist != 0 and u.current_communic_responsible != 0:
                if u.last_msg_time_from_user < u.last_msg_time_from_specialist and time() - u.last_msg_time_from_specialist > config.timeout_noreply:
                    archive_communic_from_chat_id(u.chat_id, timeout=True)
                    db.set_user(u.chat_id, has_active_communics=0, current_communic_is_appointed=0, current_communic_responsible=0, message_log = '\'' + config.REPLY['communic_closed'] + '\'')
                    bot.send_message(u.current_communic_responsible, 'Коммуникация, закрепленная ранее за Вами, автоматически закрыта по таймауту.')
                    for gr in SUPPORTGROUPS:
                        bot.send_message(gr, 'Коммуникация от пользователя: \n\n' + print_userinfo(u.chat_id, with_log=False) + '\nбыла автоматически закрыта по таймауту.')

def take_open_communic_by_button(support_chat_id, user_chat_id):

    user = db.get_user_namedtuple(user_chat_id)
    specialist_has_communics = (False if db.get_user_chat_id_by_responsible(support_chat_id) == None else True)
    if not specialist_has_communics and user.current_communic_is_appointed == 0 and user.has_active_communics == 1:
        db.append_message_to_history(user_chat_id, '[ServiceBot]: ' + get_support_name_by_chat_id(support_chat_id) + ' перевел на себя коммуникацию')
        db.set_user(user_chat_id, last_msg_time_from_specialist=int(time()))
        reply_to_group = ('{0} взял коммуникацию с пользователем: \n\n' + print_userinfo(user_chat_id, with_log=False))
        reply_to_specialist = ('{0}, Вы перевели на себя коммуникацию с пользователем: \n\n' + print_userinfo(user_chat_id, with_log=True) + '\nСообщения, которые Вы получаете от бота в личном чате, '
                               'перенаправляются Вам в том виде, в каком пользователь направляет их боту.\n\nСообщения, отправляемые Вами боту '
                               'в личном чате, перенаправляются пользователю.\n\nВсе сообщения попадают в историю коммуникации и после закрытия коммуникации архивируются.\n\nВам следует убедиться, что по данной коммуникации '
                               'еще не зарегистрирована заявка в сервисдеске, и зарегистрировать ее самостоятельно.\n\nДля открытия сервисного меню нажмите кнопку "/" и выберите позицию "/service" во всплывающем меню.\n\n'
                               'Для закрытия коммуникации нажмите кнопку "Закрыть текущую коммуникацию" в сервисном меню.'
                              )
        reply_to_user = ('Ваша коммуникация была переведена на специалиста. Имя специалиста: ' + get_support_name_by_chat_id(support_chat_id) + '.\n'
                         'Сообщения, отправляемые Вами боту, перенаправляются специалисту.')
        for gr in SUPPORTGROUPS:
            bot.send_message(gr, reply_to_group.format(get_support_name_by_chat_id(support_chat_id)))
        db.set_user(user_chat_id, current_communic_is_appointed=1)
        send_history(support_chat_id, reply_to_specialist.format(get_support_name_by_chat_id(support_chat_id)))
        db.set_user(user_chat_id, current_communic_responsible=support_chat_id)
        bot.send_message(user_chat_id, reply_to_user)
    elif not specialist_has_communics and user.current_communic_is_appointed == 0 and user.has_active_communics == 0:
        reply_to_specialist_1 = ('{0}, Вы хотели перевести на себя коммуникацию с пользователем {1}. Эта коммуникация уже закрыта. Пользователь не имеет активных коммуникаций.')
        bot.send_message(support_chat_id, reply_to_specialist_1.format(get_support_name_by_chat_id(support_chat_id), user.first_name + user.last_name))
    elif user.current_communic_is_appointed == 1 and user.has_active_communics == 1 and support_chat_id == user.current_communic_responsible:
        reply_to_specialist_2 = ('{0}, Вы хотели перевести на себя коммуникацию с пользователем {1}. Эта коммуникация уже закреплена за Вами. Вы можете общаться с пользователем через личный чат с ботом.')
        bot.send_message(support_chat_id, reply_to_specialist_2.format(get_support_name_by_chat_id(support_chat_id), user.first_name + user.last_name))
    elif user.current_communic_is_appointed == 1 and user.has_active_communics == 1 and support_chat_id != user.current_communic_responsible:
        reply_to_specialist_3 = ('{0}, Вы хотели перевести на себя коммуникацию с пользователем {1}. Эта коммуникация уже закреплена за другим специалистом.')
        bot.send_message(support_chat_id, reply_to_specialist_3.format(get_support_name_by_chat_id(support_chat_id), user.first_name + user.last_name))
    elif specialist_has_communics:
        reply_to_specialist_4 = ('{0}, Вы хотели перевести на себя коммуникацию с пользователем {1}. У Вас есть текущая коммуникация. Для перевода на Вас новой коммуникации следует сначала закрыть текущую.')
        bot.send_message(support_chat_id, reply_to_specialist_4.format(get_support_name_by_chat_id(support_chat_id), user.first_name + user.last_name))
    else:
        bot.send_message(support_chat_id, config.REPLY['incorrect_request'])

def close_current_communic_by_button(id_, support_chat_id, prefix):

    user_chat_id = db.get_user_chat_id_by_responsible(support_chat_id)
    u = db.get_user_namedtuple(user_chat_id)
    if user_chat_id != None:
        archive_communic_from_chat_id(user_chat_id)
        db.set_user(user_chat_id, current_communic_is_appointed=0, current_communic_responsible=0, has_active_communics=0, message_log='\'' + config.REPLY['communic_closed'] + '\'')
        reply_to_group = (('Специалист {0} закрыл коммуникацию с пользователем: \n\n' + print_userinfo(user_chat_id, with_log=False)).format(get_support_name_by_chat_id(support_chat_id)))
        reply_to_specialist = ('[ServiceBot] Вы закрыли коммуникацию с пользователем: \n\n' + print_userinfo(user_chat_id, with_log=False))
        bot.send_message(support_chat_id, prefix + reply_to_specialist)
        for gr in SUPPORTGROUPS:
            bot.send_message(gr, reply_to_group)
    elif user_chat_id == None:
        bot.answer_callback_query(id_, prefix + config.REPLY['no_active_communics'], show_alert=True)

def archive_communic_from_chat_id(chat_id, timeout=False):

    if chat_id != None:
        u = db.get_user_namedtuple(chat_id)
        if timeout:
            db.append_message_to_history(chat_id, '[ServiceBot]: Коммуникация была автоматически закрыта по таймауту')
        else:
            db.append_message_to_history(chat_id, '[ServiceBot]: ' + get_support_name_by_chat_id(u.current_communic_responsible) + ' закрыл коммуникацию')
        dir_log = config.archive + str(chat_id)
        file_log = config.archive + str(chat_id) + '/' + str(chat_id) + r'.txt'
        if(isdir(dir_log) == False):
            mkdir(dir_log)
        if(isfile(file_log) == False):
            f = open(file_log, 'w+')
            f.write(50*'#' + '\n\n' + 'Архив сообщений по пользователю' + '\n\n' + print_userinfo(chat_id) + '\n' + 50*'#' + '\n\n')
            f.close()
        f = open(file_log, 'a')
        f.write(60*'_' + '\n\n' + 'Запись от ' + strftime('%d.%m.%Y %H:%M:%S', localtime()) + ' (локальное время сервера)' + '\n' + 60*'_')

        # workaround for emoji logging
        res = ''
        for s in db.get_message_log_by_id(u.id):
            if (ord(s) > 0xfff):
                k = '[U+{:X}]'.format(ord(s))
                res += k
            else:
                res += s
        f.write('\n\n' + str(res) + '\n' + 60*'_' + '\n\n')
        f.close()

def get_prefix_for_servicebutton_reply(string):

    q = [k[0] for k in config.SUPPORTS_KEYBOARD or config.CONTROLLERS_KEYBOARD if k[1] == string]
    return 'Q: ' + q[0] + '\n\n' if q[0] else None

def process_content(message):

    dir_log = config.archive + str(message.chat.id)
    if(isdir(dir_log) == False):
            mkdir(dir_log)
    if message.photo:
        content = get_file_photo_id(message)
        filename_suff = content[-10:] + '.jpg'
        a = 'фото'
        b = '. Подпись к фото: ' + message.caption if message.caption else ''
    elif message.document:
        content = message.document.file_id
        filename_suff = message.document.file_name
        a = 'файл ' + message.document.file_name
        b = '. Подпись к файлу: ' + message.caption if message.caption else ''
    if message.chat.id in SUPPORTS:
        h = db.get_user_chat_id_by_responsible(message.chat.id)
        c = 'Специалист'
    else:
        h = message.chat.id
        c = 'Пользователь'
    filename = dir_log + '/' + strftime('%Y%m%d_%H%M%S', localtime()) + '__' + filename_suff
    db.append_message_to_history(h, config.REPLY['sent_content'].format(c, a, b, filename))
    download_content(content, filename)

def download_content(cont, filename):

    if system() == 'Linux':
        du = disk_usage('/')
    elif system() == 'Windows':
        du = disk_usage('C:')
    free_space_rate = du.free / du.total * 100
    if free_space_rate <= 10:
        for gr in SUPPORTGROUPS:
            bot.send_message(gr, '[WARNING] Free disk space on bot-server ' + gethostname() + ' is less than 10%\n\nBot-server info (hostname, aliaslist, ipaddrlist):\n\n' + \
                                  str(gethostbyname_ex(gethostname())) + '\n\nThis message was generated at file saving procedure.')
        send_warning('support@depit.ru')
    file_info = bot.get_file(cont)
    file = get('https://api.telegram.org/file/bot{0}/{1}'.format(config.token, file_info.file_path))
    new_file = open(filename, 'wb')
    new_file.write(file.content)
    new_file.close()

def send_warning(addr):

    subj = '[WARNING] Free disk space on bot-server ' + gethostname() + ' is less than 10%'
    _from =  formataddr((str(Header('Сервисный бот', 'utf-8')), config.email_address))
    to = addr
    body = '[WARNING] Free disk space on bot-server ' + gethostname() + ' is less than 10%\n\nBot-server info (hostname, aliaslist, ipaddrlist):\n\n' + \
            str(gethostbyname_ex(gethostname())) + '\n\nThis message was generated at file saving procedure.'
    send_email(_from, to, subj, body)

def get_file_photo_id(message):

    if message.photo:
        it = [i.file_size for i in message.photo]
        return message.photo[it.index(max(it))].file_id

# workaround for Telegram maximum message size
def break_history(h):
    s = 4096
    if len(h) > s:
        hist_list = []
        n = len(h) // s
        for i in range(1, n+1):
            hist_list.append(h[s*(i-1):s*i])
        hist_list.append(h[s*n:])
        print('hist_list', hist_list)
        return hist_list
    else:
        print('h', h)
        return h

def send_history(chat_id, h, **kwargs):

    if len(h) > 4096:
        k = 1
        while len(h) > k*4096:
            z = h[4096*(k-1):4096*k]
            bot.send_message(chat_id, z)
            k += 1
        if 'reply_markup' in kwargs:
            bot.send_message(chat_id, h[4096*(k-1):], reply_markup=kwargs['reply_markup'])
        else:
            bot.send_message(chat_id, h[4096*(k-1):])
    else:
        if 'reply_markup' in kwargs:
            bot.send_message(chat_id, h, reply_markup=kwargs['reply_markup'])
        else:
            bot.send_message(chat_id, h)


try:

    f = open('workfromhome.txt','r')
    f.write("This is test for exception!!")

except:
    print("Error Made by Nitin and it is wrong")

# else:
#     print('Success!')
#     f.close()
finally:
    print("Finally done")
print("Done")


import torch

def update_mask(mask, dynamic, chosen_idx=None):
    loads = dynamic.data[:, 0]
    demands = dynamic.data[:, 1]

    if demands.eq(0).all():
        return demands * 0.

    new_mask = demands.ne(0) * demands.lt(loads)

    repeat_home = chosen_idx.ne(0)

    if repeat_home.any():
        new_mask[repeat_home.nonzero(), 0] = 1.
    if torch.logical_not(repeat_home).any():
        mask_indices = torch.logical_not(repeat_home).nonzero().squeeze()
        new_mask[mask_indices, 0] = 0.0

    has_no_load = loads[:, 0].eq(0).float()
    has_no_demand = demands[:, 1:].sum(1).eq(0).float()

    combined = (has_no_load + has_no_demand).gt(0)
    if combined.any():
        new_mask[combined.nonzero(), 0] = 1.
        new_mask[combined.nonzero(), 1:] = 0.

    return new_mask.float()


from dataclasses import dataclass
from typing import Optional, TypeVar

import graphql
from graphql.utilities import print_schema

from apischema.graphql import graphql_schema, relay, resolver

Cursor = int  # let's use an integer cursor in all our connection
Node = TypeVar("Node")
Connection = relay.Connection[Node, Cursor, relay.Edge[Node, Cursor]]
# Connection can now be used just like Connection[Ship] or Connection[Faction | None]

@dataclass
class Ship:
    name: str

@dataclass
class Faction:
    @resolver
    def ships(
        self, first: int | None, after: Cursor | None
    ) -> Connection[Optional[Ship]] | None:
        edges = [relay.Edge(Ship("X-Wing"), 0), relay.Edge(Ship("B-Wing"), 1)]
        return Connection(edges, relay.PageInfo.from_edges(edges))

def faction() -> Faction | None:
    return Faction()

schema = graphql_schema(query=[faction])
schema_str = """\
type Query {
  faction: Faction
}

type Faction {
  ships(first: Int, after: Int): ShipConnection
}

type ShipConnection {
  edges: [ShipEdge]
  pageInfo: PageInfo!
}

type ShipEdge {
  node: Ship
  cursor: Int!
}

type Ship {
  name: String!
}

type PageInfo {
  hasPreviousPage: Boolean!
  hasNextPage: Boolean!
  startCursor: Int
  endCursor: Int
}"""
assert print_schema(schema) == schema_str
query = """
{
    faction {
        ships {
            pageInfo {
                endCursor
                hasNextPage
            }
            edges {
                cursor
                node {
                    name
                }
            }
        }
    }
}"""
assert graphql.graphql_sync(schema, query).data == {
    "faction": {
        "ships": {
            "pageInfo": {"endCursor": 1, "hasNextPage": False},
            "edges": [
                {"cursor": 0, "node": {"name": "X-Wing"}},
                {"cursor": 1, "node": {"name": "B-Wing"}},
            ],
        }
    }
}


import multiprocessing
import requests
img_urls = [
    'https://cdn.pixabay.com/photo/2016/01/05/17/51/maltese-1123016_960_720.jpg',
    'https://cdn.pixabay.com/photo/2016/07/15/15/55/dachshund-1519374_960_720.jpg'

]

def download_image(img_url):
    img_bytes = requests.get(img_url).content
    img_name = img_url.split('/')[9]
    with open(img_name, 'wb') as img_file:
        img_file.write(img_bytes)
        print(f"{img_name} was downloaded")

if __name__ == '__main__':
    for i in range(len(img_urls)):
        pi = multiprocessing.Process(target=download_image, args=[img_urls[i]])
        pi.start()


'''
Created on April 8, 2020

@author: GGM
'''
import enum
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from Students.models import Student, StudentAnswerHints
from Students.views.utils import studentInitialContextDict
from Instructors.models import ChallengesQuestions, Questions
from Badges.models import CourseConfigParams

@login_required
def hintInfoView(request):
    context_dict, currentCourse = studentInitialContextDict(request)
    ccparams = CourseConfigParams.objects.get(courseID=currentCourse)

    context_dict['weakHint'] = ccparams.weightBasicHint
    context_dict['strongHint'] = ccparams.weightStrongHint
    return render(request,'Students/HintsInfo.html',context_dict)

@login_required
def hintsUsed(request):
    ##this is used to track how many times the student clicks class average
    ##we use ajax to track the information, otherwise they'd get the page refreshed on them
    ##and it would be "wrong".

    context_dict, currentCourse = studentInitialContextDict(request)

    hints = {}
    response = {}
    #dict['hintsUsed'] = {}

    if request.POST:
        student = Student.objects.get(user=request.user)
        if 'challengeQuestionID' in request.POST:
            hintType = convertToEnum(request.POST['type'])
            studentHintObjectID = createStudentHint(request.POST['challengeQuestionID'], hintType, student).studentAnswerHintsID
            hint = obtainHint(request.POST['challengeQuestionID'], hintType)
            return JsonResponse(
                {
                "hintID" : studentHintObjectID ,
                "hint": hint
                }
            )

def createStudentHint(challengeQuestionsID, typeOfHint, student):
    #first determine if the hint already exists
    studentAnswerHint = StudentAnswerHints.objects.filter(challengeQuestionID=challengeQuestionsID, studentID=student, studentChallengeQuestionID__isnull=True)

    if studentAnswerHint.exists():
        studentAnswerHint = StudentAnswerHints.objects.get(challengeQuestionID=challengeQuestionsID, studentID=student, studentChallengeQuestionID__isnull=True)
    else:
        studentAnswerHint = StudentAnswerHints()
    studentAnswerHint.challengeQuestionID = int(challengeQuestionsID)
    studentAnswerHint.studentID = student
    if(typeOfHint == HintType.basic):
        studentAnswerHint.usedBasicHint = True
    if (typeOfHint == HintType.strong):
        studentAnswerHint.usedStrongHint = True
    studentAnswerHint.save()
    return studentAnswerHint

def obtainHint(challengeQuestionsID, typeOfHint):
    challengeQuestion = ChallengesQuestions.objects.get(pk=int(challengeQuestionsID))
    question = challengeQuestion.questionID
    if(typeOfHint == HintType.basic):
        return question.basicHint
    if(typeOfHint == HintType.strong):
        return question.strongHint

def convertToEnum(typeOfHint):
    typeOfHint = int(typeOfHint)
    if(typeOfHint == 0):
        return HintType.basic
    if(typeOfHint == 1):
        return HintType.strong

class HintType(enum.Enum):
   basic = 0
   strong = 1